diff options
author | Dennis Tang <dtang@gitlab.com> | 2018-05-26 00:56:17 +0300 |
---|---|---|
committer | Dennis Tang <dtang@gitlab.com> | 2018-05-26 00:56:17 +0300 |
commit | 95a63a881673533e3fd44243297d1a81e19396b6 (patch) | |
tree | 9a3190b813e2d599043394b30afaa5a5c8f8e565 | |
parent | 48e46f959716c8915f5b59d1314b5e5781f3cd8d (diff) | |
parent | 50c8ed2bf498c69d3d52ba1451274e3fbf438429 (diff) |
Merge remote-tracking branch 'origin/master' into 38759-fetch-available-parameters-directly-from-gke-when-creating-a-cluster
1451 files changed, 13190 insertions, 10459 deletions
diff --git a/.flayignore b/.flayignore index 0c4eee10ffa..7faa6c7bb90 100644 --- a/.flayignore +++ b/.flayignore @@ -10,3 +10,7 @@ lib/gitlab/background_migration/* app/models/project_services/kubernetes_service.rb lib/gitlab/workhorse.rb lib/gitlab/ci/trace/chunked_io.rb +lib/gitlab/gitaly_client/ref_service.rb +lib/gitlab/gitaly_client/commit_service.rb +lib/gitlab/git/commit.rb +lib/gitlab/git/tag.rb diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 05487134cb1..ef263a3f106 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,6 +10,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git paths: - vendor/ruby - .yarn-cache/ + - vendor/gitaly-ruby .push-cache: &push-cache cache: @@ -30,7 +31,6 @@ variables: GIT_SUBMODULE_STRATEGY: "none" GET_SOURCES_ATTEMPTS: "3" KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json - KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/report-suite.json before_script: @@ -126,6 +126,23 @@ stages: <<: *dedicated-no-docs-pull-cache-job <<: *except-docs-and-qa +.single-script-job: &single-script-job + image: ruby:2.4-alpine + before_script: [] + stage: build + cache: {} + dependencies: [] + variables: &single-script-job-variables + GIT_STRATEGY: none + before_script: + # We need to download the script rather than clone the repo since the + # package-and-qa job will not be able to run when the branch gets + # deleted (when merging the MR). + - export SCRIPT_NAME="${SCRIPT_NAME:-$CI_JOB_NAME}" + - apk add --update openssl + - wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/$SCRIPT_NAME + - chmod 755 $SCRIPT_NAME + .rake-exec: &rake-exec <<: *dedicated-no-docs-no-db-pull-cache-job script: @@ -178,46 +195,6 @@ stages: <<: *rspec-metadata-mysql <<: *rails5 -.spinach-metadata: &spinach-metadata - <<: *dedicated-runner - <<: *except-docs-and-qa - <<: *pull-cache - <<: *rails5-variables - stage: test - script: - - JOB_NAME=( $CI_JOB_NAME ) - - export CI_NODE_INDEX=${JOB_NAME[-2]} - - export CI_NODE_TOTAL=${JOB_NAME[-1]} - - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - - export KNAPSACK_GENERATE_REPORT=true - - export CACHE_CLASSES=true - - cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} - - scripts/gitaly-test-spawn - - knapsack spinach "-r rerun" -b || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -b -r rerun $(cat tmp/spinach-rerun.txt)' - artifacts: - expire_in: 31d - when: always - paths: - - coverage/ - - knapsack/ - - tmp/capybara/ - -.spinach-metadata-pg: &spinach-metadata-pg - <<: *spinach-metadata - <<: *use-pg - -.spinach-metadata-pg-rails5: &spinach-metadata-pg-rails5 - <<: *spinach-metadata-pg - <<: *rails5 - -.spinach-metadata-mysql: &spinach-metadata-mysql - <<: *spinach-metadata - <<: *use-mysql - -.spinach-metadata-mysql-rails5: &spinach-metadata-mysql-rails5 - <<: *spinach-metadata-mysql - <<: *rails5 - .only-canonical-masters: &only-canonical-masters only: - master@gitlab-org/gitlab-ce @@ -229,7 +206,7 @@ stages: <<: *dedicated-no-docs-and-no-qa-pull-cache-job <<: *use-pg variables: - CREATE_DB_USER: "true" + SETUP_DB: "false" script: # Manually clone gitlab-test and only seed this project in # db/fixtures/development/04_project.rb thanks to SIZE=1 below @@ -247,19 +224,10 @@ stages: .review-docs: &review-docs <<: *dedicated-runner <<: *except-qa - image: ruby:2.4-alpine - before_script: - - gem install gitlab --no-doc - # We need to download the script rather than clone the repo since the - # review-docs-cleanup job will not be able to run when the branch gets - # deleted (when merging the MR). - - apk add --update openssl - - wget https://gitlab.com/gitlab-org/gitlab-ce/raw/master/scripts/trigger-build-docs - - chmod 755 trigger-build-docs - cache: {} - dependencies: [] + <<: *single-script-job variables: - GIT_STRATEGY: none + <<: *single-script-job-variables + SCRIPT_NAME: trigger-build-docs when: manual only: - branches @@ -273,7 +241,7 @@ stages: .migration-paths: &migration-paths <<: *dedicated-no-docs-and-no-qa-pull-cache-job variables: - CREATE_DB_USER: "true" + SETUP_DB: "false" script: - git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0 - git checkout -f FETCH_HEAD @@ -282,7 +250,7 @@ stages: - cp config/gitlab.yml.example config/gitlab.yml - bundle exec rake db:drop db:create db:schema:load db:seed_fu - date - - git checkout $CI_COMMIT_SHA + - git checkout -f $CI_COMMIT_SHA - bundle install $BUNDLE_INSTALL_FLAGS - date - . scripts/prepare_build.sh @@ -293,23 +261,14 @@ stages: # Trigger a package build in omnibus-gitlab repository # package-and-qa: - image: ruby:2.4-alpine - before_script: [] - stage: build - cache: {} - when: manual + <<: *single-script-job variables: - GIT_STRATEGY: none + <<: *single-script-job-variables + SCRIPT_NAME: trigger-build-omnibus retry: 0 - before_script: - # We need to download the script rather than clone the repo since the - # package-and-qa job will not be able to run when the branch gets - # deleted (when merging the MR). - - apk add --update openssl - - wget https://gitlab.com/$CI_PROJECT_PATH/raw/$CI_COMMIT_SHA/scripts/trigger-build-omnibus - - chmod 755 trigger-build-omnibus script: - - ./trigger-build-omnibus + - ./$SCRIPT_NAME + when: manual only: - //@gitlab-org/gitlab-ce - //@gitlab-org/gitlab-ee @@ -326,7 +285,8 @@ review-docs-deploy: url: http://$DOCS_GITLAB_REPO_SUFFIX-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX on_stop: review-docs-cleanup script: - - ./trigger-build-docs deploy + - gem install gitlab --no-ri --no-rdoc + - ./$SCRIPT_NAME deploy # Cleanup remote environment of gitlab-docs review-docs-cleanup: @@ -336,7 +296,28 @@ review-docs-cleanup: name: review-docs/$CI_COMMIT_REF_NAME action: stop script: - - ./trigger-build-docs cleanup + - gem install gitlab --no-ri --no-rdoc + - ./SCRIPT_NAME cleanup + +## +# Trigger a docker image build in CNG (Cloud Native GitLab) repository +# +cloud-native-image: + image: ruby:2.4-alpine + before_script: [] + stage: build + allow_failure: true + variables: + GIT_DEPTH: "1" + cache: {} + before_script: + - gem install gitlab --no-rdoc --no-ri + - chmod 755 ./scripts/trigger-build-cloud-native + script: + - ./scripts/trigger-build-cloud-native + only: + - tags@gitlab-org/gitlab-ce + - tags@gitlab-org/gitlab-ee # Retrieve knapsack and rspec_flaky reports retrieve-tests-metadata: @@ -349,9 +330,7 @@ retrieve-tests-metadata: script: - mkdir -p knapsack/${CI_PROJECT_NAME}/ - wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $KNAPSACK_RSPEC_SUITE_REPORT_PATH - - wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || rm $KNAPSACK_SPINACH_SUITE_REPORT_PATH - '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}' - - '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}' - mkdir -p rspec_flaky/ - wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH - '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}' @@ -367,12 +346,11 @@ update-tests-metadata: - rspec_flaky/ policy: push script: - - retry gem install fog-aws mime-types activesupport + - retry gem install fog-aws mime-types activesupport --no-ri --no-rdoc - scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json - - scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json - scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json - FLAKY_RSPEC_GENERATE_REPORT=1 scripts/prune-old-flaky-specs ${FLAKY_RSPEC_SUITE_REPORT_PATH} - - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH' + - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH' - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH' - rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json - rm -f rspec_flaky/all_*.json rspec_flaky/new_*.json @@ -438,134 +416,131 @@ setup-test-env: paths: - tmp/tests - config/secrets.yml - -rspec-pg 0 28: *rspec-metadata-pg -rspec-pg 1 28: *rspec-metadata-pg -rspec-pg 2 28: *rspec-metadata-pg -rspec-pg 3 28: *rspec-metadata-pg -rspec-pg 4 28: *rspec-metadata-pg -rspec-pg 5 28: *rspec-metadata-pg -rspec-pg 6 28: *rspec-metadata-pg -rspec-pg 7 28: *rspec-metadata-pg -rspec-pg 8 28: *rspec-metadata-pg -rspec-pg 9 28: *rspec-metadata-pg -rspec-pg 10 28: *rspec-metadata-pg -rspec-pg 11 28: *rspec-metadata-pg -rspec-pg 12 28: *rspec-metadata-pg -rspec-pg 13 28: *rspec-metadata-pg -rspec-pg 14 28: *rspec-metadata-pg -rspec-pg 15 28: *rspec-metadata-pg -rspec-pg 16 28: *rspec-metadata-pg -rspec-pg 17 28: *rspec-metadata-pg -rspec-pg 18 28: *rspec-metadata-pg -rspec-pg 19 28: *rspec-metadata-pg -rspec-pg 20 28: *rspec-metadata-pg -rspec-pg 21 28: *rspec-metadata-pg -rspec-pg 22 28: *rspec-metadata-pg -rspec-pg 23 28: *rspec-metadata-pg -rspec-pg 24 28: *rspec-metadata-pg -rspec-pg 25 28: *rspec-metadata-pg -rspec-pg 26 28: *rspec-metadata-pg -rspec-pg 27 28: *rspec-metadata-pg - -rspec-mysql 0 28: *rspec-metadata-mysql -rspec-mysql 1 28: *rspec-metadata-mysql -rspec-mysql 2 28: *rspec-metadata-mysql -rspec-mysql 3 28: *rspec-metadata-mysql -rspec-mysql 4 28: *rspec-metadata-mysql -rspec-mysql 5 28: *rspec-metadata-mysql -rspec-mysql 6 28: *rspec-metadata-mysql -rspec-mysql 7 28: *rspec-metadata-mysql -rspec-mysql 8 28: *rspec-metadata-mysql -rspec-mysql 9 28: *rspec-metadata-mysql -rspec-mysql 10 28: *rspec-metadata-mysql -rspec-mysql 11 28: *rspec-metadata-mysql -rspec-mysql 12 28: *rspec-metadata-mysql -rspec-mysql 13 28: *rspec-metadata-mysql -rspec-mysql 14 28: *rspec-metadata-mysql -rspec-mysql 15 28: *rspec-metadata-mysql -rspec-mysql 16 28: *rspec-metadata-mysql -rspec-mysql 17 28: *rspec-metadata-mysql -rspec-mysql 18 28: *rspec-metadata-mysql -rspec-mysql 19 28: *rspec-metadata-mysql -rspec-mysql 20 28: *rspec-metadata-mysql -rspec-mysql 21 28: *rspec-metadata-mysql -rspec-mysql 22 28: *rspec-metadata-mysql -rspec-mysql 23 28: *rspec-metadata-mysql -rspec-mysql 24 28: *rspec-metadata-mysql -rspec-mysql 25 28: *rspec-metadata-mysql -rspec-mysql 26 28: *rspec-metadata-mysql -rspec-mysql 27 28: *rspec-metadata-mysql - -spinach-pg 0 2: *spinach-metadata-pg -spinach-pg 1 2: *spinach-metadata-pg - -spinach-mysql 0 2: *spinach-metadata-mysql -spinach-mysql 1 2: *spinach-metadata-mysql - -rspec-pg-rails5 0 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 1 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 2 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 3 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 4 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 5 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 6 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 7 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 8 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 9 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 10 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 11 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 12 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 13 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 14 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 15 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 16 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 17 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 18 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 19 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 20 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 21 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 22 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 23 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 24 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 25 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 26 28: *rspec-metadata-pg-rails5 -rspec-pg-rails5 27 28: *rspec-metadata-pg-rails5 - -rspec-mysql-rails5 0 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 1 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 2 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 3 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 4 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 5 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 6 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 7 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 8 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 9 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 10 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 11 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 12 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 13 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 14 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 15 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 16 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 17 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 18 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 19 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 20 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 21 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 22 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 23 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 24 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 25 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 26 28: *rspec-metadata-mysql-rails5 -rspec-mysql-rails5 27 28: *rspec-metadata-mysql-rails5 - -spinach-pg-rails5 0 2: *spinach-metadata-pg-rails5 -spinach-pg-rails5 1 2: *spinach-metadata-pg-rails5 - -spinach-mysql-rails5 0 2: *spinach-metadata-mysql-rails5 -spinach-mysql-rails5 1 2: *spinach-metadata-mysql-rails5 + - vendor/gitaly-ruby + +rspec-pg 0 30: *rspec-metadata-pg +rspec-pg 1 30: *rspec-metadata-pg +rspec-pg 2 30: *rspec-metadata-pg +rspec-pg 3 30: *rspec-metadata-pg +rspec-pg 4 30: *rspec-metadata-pg +rspec-pg 5 30: *rspec-metadata-pg +rspec-pg 6 30: *rspec-metadata-pg +rspec-pg 7 30: *rspec-metadata-pg +rspec-pg 8 30: *rspec-metadata-pg +rspec-pg 9 30: *rspec-metadata-pg +rspec-pg 10 30: *rspec-metadata-pg +rspec-pg 11 30: *rspec-metadata-pg +rspec-pg 12 30: *rspec-metadata-pg +rspec-pg 13 30: *rspec-metadata-pg +rspec-pg 14 30: *rspec-metadata-pg +rspec-pg 15 30: *rspec-metadata-pg +rspec-pg 16 30: *rspec-metadata-pg +rspec-pg 17 30: *rspec-metadata-pg +rspec-pg 18 30: *rspec-metadata-pg +rspec-pg 19 30: *rspec-metadata-pg +rspec-pg 20 30: *rspec-metadata-pg +rspec-pg 21 30: *rspec-metadata-pg +rspec-pg 22 30: *rspec-metadata-pg +rspec-pg 23 30: *rspec-metadata-pg +rspec-pg 24 30: *rspec-metadata-pg +rspec-pg 25 30: *rspec-metadata-pg +rspec-pg 26 30: *rspec-metadata-pg +rspec-pg 27 30: *rspec-metadata-pg +rspec-pg 28 30: *rspec-metadata-pg +rspec-pg 29 30: *rspec-metadata-pg + +rspec-mysql 0 30: *rspec-metadata-mysql +rspec-mysql 1 30: *rspec-metadata-mysql +rspec-mysql 2 30: *rspec-metadata-mysql +rspec-mysql 3 30: *rspec-metadata-mysql +rspec-mysql 4 30: *rspec-metadata-mysql +rspec-mysql 5 30: *rspec-metadata-mysql +rspec-mysql 6 30: *rspec-metadata-mysql +rspec-mysql 7 30: *rspec-metadata-mysql +rspec-mysql 8 30: *rspec-metadata-mysql +rspec-mysql 9 30: *rspec-metadata-mysql +rspec-mysql 10 30: *rspec-metadata-mysql +rspec-mysql 11 30: *rspec-metadata-mysql +rspec-mysql 12 30: *rspec-metadata-mysql +rspec-mysql 13 30: *rspec-metadata-mysql +rspec-mysql 14 30: *rspec-metadata-mysql +rspec-mysql 15 30: *rspec-metadata-mysql +rspec-mysql 16 30: *rspec-metadata-mysql +rspec-mysql 17 30: *rspec-metadata-mysql +rspec-mysql 18 30: *rspec-metadata-mysql +rspec-mysql 19 30: *rspec-metadata-mysql +rspec-mysql 20 30: *rspec-metadata-mysql +rspec-mysql 21 30: *rspec-metadata-mysql +rspec-mysql 22 30: *rspec-metadata-mysql +rspec-mysql 23 30: *rspec-metadata-mysql +rspec-mysql 24 30: *rspec-metadata-mysql +rspec-mysql 25 30: *rspec-metadata-mysql +rspec-mysql 26 30: *rspec-metadata-mysql +rspec-mysql 27 30: *rspec-metadata-mysql +rspec-mysql 28 30: *rspec-metadata-mysql +rspec-mysql 29 30: *rspec-metadata-mysql + +rspec-pg-rails5 0 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 1 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 2 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 3 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 4 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 5 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 6 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 7 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 8 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 9 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 10 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 11 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 12 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 13 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 14 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 15 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 16 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 17 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 18 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 19 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 20 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 21 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 22 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 23 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 24 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 25 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 26 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 27 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 28 30: *rspec-metadata-pg-rails5 +rspec-pg-rails5 29 30: *rspec-metadata-pg-rails5 + +rspec-mysql-rails5 0 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 1 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 2 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 3 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 4 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 5 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 6 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 7 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 8 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 9 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 10 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 11 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 12 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 13 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 14 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 15 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 16 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 17 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 18 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 19 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 20 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 21 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 22 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 23 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 24 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 25 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 26 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 27 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 28 30: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 29 30: *rspec-metadata-mysql-rails5 static-analysis: <<: *dedicated-no-docs-no-db-pull-cache-job @@ -882,3 +857,15 @@ gitlab_git_test: cache: {} script: - spec/support/prepare-gitlab-git-test-for-commit --check-for-changes + +no_ee_check: + <<: *dedicated-runner + <<: *except-docs-and-qa + variables: + SETUP_DB: "false" + before_script: [] + cache: {} + script: + - scripts/no-ee-check + only: + - //@gitlab-org/gitlab-ce diff --git a/.gitlab/merge_request_templates/Database Changes.md b/.gitlab/merge_request_templates/Database Changes.md index 2bb1f374e98..1c4f30d9320 100644 --- a/.gitlab/merge_request_templates/Database Changes.md +++ b/.gitlab/merge_request_templates/Database Changes.md @@ -37,12 +37,14 @@ When removing columns, tables, indexes or other structures: - [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/doc_styleguide.html) - [ ] API support added - [ ] Tests added for this feature/bug -- Review - - [ ] Has been reviewed by Backend - - [ ] Has been reviewed by Database +- Conform by the [code review guidelines](https://docs.gitlab.com/ee/development/code_review.html) + - [ ] Has been reviewed by a Backend maintainer + - [ ] Has been reviewed by a Database specialist - [ ] Conform by the [merge request performance guides](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html) - [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/CONTRIBUTING.md#style-guides) -- [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) +- [ ] If you have multiple commits, please combine them into a few logically organized commits by [squashing them](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) - [ ] Internationalization required/considered - [ ] If paid feature, have we considered GitLab.com plan and how it works for groups and is there a design for promoting it to users who aren't on the correct plan - [ ] End-to-end tests pass (`package-and-qa` manual pipeline job) + +/label ~database diff --git a/.scss-lint.yml b/.scss-lint.yml index 180d377d6f8..3df66033fa8 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -46,8 +46,9 @@ linters: # - properties # - @include declarations with inner @content # - nested rule sets. + # Disabled to minimize Bootstrap migration footprint DeclarationOrder: - enabled: true + enabled: false # `scss-lint:disable` control comments should be preceded by a comment # explaining why these linters are being disabled for this file. diff --git a/CHANGELOG.md b/CHANGELOG.md index 29047c3ad65..99cf96035d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,204 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 10.8.1 (2018-05-23) + +### Fixed (9 changes) + +- Allow CommitStatus class to use presentable methods. !18979 +- Fix corrupted environment pages with unathorized proxy url. !18989 +- Fixes deploy token variables on Ci::Build. !19047 +- Fix project mirror database inconsistencies when upgrading from EE to CE. !19109 +- Render 404 when prometheus adapter is disabled in Prometheus metrics controller. !19110 +- Fix error when deleting an empty list of refs. +- Fixed U2F login when used with LDAP. +- Bump prometheus-client-mmap to 0.9.3 to fix nil exception error. +- Fix system hook not firing for blocked users when LDAP sign-in is used. + + +## 10.8.0 (2018-05-22) + +### Security (3 changes, 1 of them is from the community) + +- Update faraday_middlewar to 0.12.2. !18397 (Takuya Noguchi) +- Serve archive requests with the correct file in all cases. +- Sanitizes user name to avoid XSS attacks. + +### Fixed (47 changes, 11 of them are from the community) + +- Refactor CSS to eliminate vertical misalignment of login nav. !16275 (Takuya Noguchi) +- Fix pipeline status in branch/tag tree page. !17995 +- Allow group owner to enable runners from subgroups (#41981). !18009 +- Fix template selector menu visibility when toggling preview mode in file edit view. !18118 (Fabian Schneider) +- Fix confirmation modal for deleting a protected branch. !18176 (Paul Bonaud @PaulRbR) +- Triggering custom hooks by Wiki UI edit. !18251 +- Now `rake cache:clear` will also clear pipeline status cache. !18257 +- Fix `joined` information on project members page. !18290 (Fabian Schneider) +- Fix missing namespace for some internal users. !18357 +- Show shared projects on group page. !18390 +- Restore label underline color. !18407 (George Tsiolis) +- Fix undefined `html_escape` method during markdown rendering. !18418 +- Fix unassign slash command preview. !18447 +- Correct text and functionality for delete user / delete user and contributions modal. !18463 (Marc Schwede) +- Fix discussions API setting created_at for notable in a group or notable in a project in a group with owners. !18464 +- Don't include lfs_file_locks data in export bundle. !18495 +- Reset milestone filter when clicking "Any Milestone" in dashboard. !18531 +- Ensure member notifications are sent after the member actual creation/update in the DB. !18538 +- Update links to /ci/lint with ones to project ci/lint. !18539 (Takuya Noguchi) +- Fix tabs container styles to make RSS button clickable. !18559 +- Raise NoRepository error for non-valid repositories when calculating repository checksum. !18594 +- Don't automatically remove artifacts for pages jobs after pages:deploy has run. !18628 +- Increase new issue metadata form margin. !18630 (George Tsiolis) +- Add loading icon padding for pipeline environments. !18631 (George Tsiolis) +- ShaAttribute no longer stops startup if database is missing. !18726 +- Fix close keyboard shortcuts dialog using the keyboard shortcut. !18783 (Lars Greiss) +- Fixes database inconsistencies between Community and Enterprise Edition on import state. !18811 +- Add database foreign key constraint between pipelines and build. !18822 +- Fix finding wiki pages when they have invalidly-encoded content. !18856 +- Fix outdated Web IDE welcome copy. !18861 +- fixed copy to blipboard button in embed bar of snippets. !18923 (haseebeqx) +- Disables RBAC on nginx-ingress. !18947 +- Correct skewed Kubernetes popover illustration. !18949 +- Resolve Import/Export ci_cd_settings error updating the project. !46049 +- Fix project creation for user endpoint when jobs_enabled parameter supplied. +- 46210 Display logo and user dropdown on mobile for terms page and fix styling. +- Adds illustration for when job log was erased. +- Ensure web hook 'blocked URL' errors are stored in web hook logs and properly surfaced to the user. +- Make toggle markdown preview shortcut only toggle selected field. +- Verifiy if pipeline has commit idetails and render information in MR widget when branch is deleted. +- Fixed inconsistent protected branch pill baseline. +- Fix setting Gitlab metrics content types. +- Display only generic message on merge error to avoid exposing any potentially sensitive or user unfriendly backend messages. +- Fix label links update on project transfer. +- Breaks commit not found message in pipelines table. +- Adjust issue boards list header label text color. +- Prevent pipeline actions in dropdown to redirct to a new page. + +### Changed (35 changes, 15 of them are from the community) + +- Improve tooltips in collapsed right sidebar. !17714 +- Partition job_queue_duration_seconds with jobs_running_for_project. !17730 +- For group dashboard, we no longer show groups which the visitor is not a member of (this applies to admins and auditors). !17884 (Roger Rüttimann) +- Use RFC 3676 mail signature delimiters. !17979 (Enrico Scholz) +- Add sha filter to pipelines list API. !18125 +- New CI Job live-trace architecture. !18169 +- Make project deploy keys table more clearly structured. !18279 +- Remove green background from unlock button in admin area. !18288 +- Renamed Overview to Project in the contextual navigation at a project level. !18295 (Constance Okoghenun) +- Load branches on new merge request page asynchronously. !18315 +- Create settings section for autodevops. !18321 +- Add a comma to the time estimate system notes. !18326 +- Enable specifying variables when executing a manual pipeline. !18440 +- Fix size and position for fork icon. !18449 (George Tsiolis) +- Refactored activity calendar. !18469 (Enrico Scholz) +- Small improvements to repository checks. !18484 +- Add 2FA filter to users API for admins only. !18503 +- Align project avatar on small viewports. !18513 (George Tsiolis) +- Show group and project LFS settings in the interface to Owners and Masters. !18562 +- Update environment item action buttons icons. !18632 (George Tsiolis) +- Update timeline icon for description edit. !18633 (George Tsiolis) +- Revert discussion counter height. !18656 (George Tsiolis) +- Improve quick actions summary preview. !18659 (George Tsiolis) +- Change font for tables inside diff discussions. !18660 (George Tsiolis) +- Add padding to profile description. !18663 (George Tsiolis) +- Break issue title for board card title and issuable header text. !18674 (George Tsiolis) +- Adds push mirrors to GitLab Community Edition. !18715 +- Inform the user when there are no project import options available. !18716 (George Tsiolis) +- Improve commit message body rendering and fix responsive compare panels. !18725 (Constance Okoghenun) +- Reconcile project templates with Auto DevOps. !18737 +- Remove branch name from the status bar of WebIDE. +- Clean up WebIDE status bar and add useful info. +- Improve interaction on WebIDE commit panel. +- Keep current labels visible when editing them in the sidebar. +- Use VueJS for rendering pipeline stages. + +### Performance (26 changes, 11 of them are from the community) + +- Move WorkInProgress vue component. !17536 (George Tsiolis) +- Move ReadyToMerge vue component. !17545 (George Tsiolis) +- Move BoardBlankState vue component. !17666 (George Tsiolis) +- Improve DB performance of calculating total artifacts size. !17839 +- Add i18n and update specs for UnresolvedDiscussions vue component. !17866 (George Tsiolis) +- Introduce new ProjectCiCdSetting model with group_runners_enabled. !18144 +- Move PipelineFailed vue component. !18277 (George Tsiolis) +- Move TimeTrackingEstimateOnlyPane vue component. !18318 (George Tsiolis) +- Move TimeTrackingHelpState vue component. !18319 (George Tsiolis) +- Reduce queries on merge requests list page for merge requests from forks. !18561 +- Destroy build_chunks efficiently with FastDestroyAll module. !18575 +- Improve performance of a service responsible for creating a pipeline. !18582 +- Replace time_ago_in_words with JS-based one. !18607 (Takuya Noguchi) +- Move TimeTrackingNoTrackingPane vue component. !18676 (George Tsiolis) +- Move SidebarTimeTracking vue component. !18677 (George Tsiolis) +- Move TimeTrackingSpentOnlyPane vue component. !18710 (George Tsiolis) +- Detecting tags containing a commit uses Gitaly by default. +- Increase cluster applications installer availability using alpine linux mirrors. +- Compute notification recipients in background jobs. +- Use persisted diff data instead fetching Git on discussions. +- Detecting branchnames containing a commit uses Gitaly by default. +- Detect repository license on Gitaly by default. +- Finish NamespaceService migration to Gitaly. +- Check if a ref exists is done by Gitaly by default. +- Compute Gitlab::Git::Repository#checksum on Gitaly by default. +- Repository#exists? is always executed through Gitaly. + +### Added (22 changes, 10 of them are from the community) + +- Allow group masters to configure runners for groups. !9646 (Alexis Reigel) +- Adds Embedded Snippets Support. !15695 (haseebeqx) +- Add Copy metadata quick action. !16473 (Mateusz Bajorski) +- Show Runner's description on job's page. !17321 +- Add deprecation message to dynamic milestone pages. !17505 +- Show new branch/mr button even when branch exists. !17712 (Jacopo Beschi @jacopo-beschi) +- API: add languages of project GET /projects/:id/languages. !17770 (Roger Rüttimann) +- Display active sessions and allow the user to revoke any of it. !17867 (Alexis Reigel) +- Add cron job to email users on issue due date. !17985 (Stuart Nelson) +- Rubocop rule to avoid returning from a block. !18000 (Jacopo Beschi @jacopo-beschi) +- Add the signature verfication badge to the compare view. !18245 (Marc Shaw) +- Expose Deploy Token data as environment varialbes on CI/CD jobs. !18414 +- Show group id in group settings. !18482 (George Tsiolis) +- Allow admins to enforce accepting Terms of Service on an instance. !18570 +- Add CI_COMMIT_MESSAGE, CI_COMMIT_TITLE and CI_COMMIT_DESCRIPTION predefined variables. !18672 +- Add GCP signup offer to cluster index / create pages. !18684 +- Output some useful information when running the rails console. !18697 +- Display merge commit SHA in merge widget after merge. !18722 +- git SHA is now displayed alongside the GitLab version on the Admin Dashboard. +- Expose the target commit ID through the tag API. +- Added fuzzy file finder to web IDE. +- Add discussion API for merge requests and commits. + +### Other (22 changes, 8 of them are from the community) + +- Replace the `project/issues/milestones.feature` spinach test with an rspec analog. !18300 (@blackst0ne) +- Replace the `project/commits/branches.feature` spinach test with an rspec analog. !18302 (@blackst0ne) +- Replacing gollum libraries for gitlab custom libs. !18343 +- Replace the `project/commits/comments.feature` spinach test with an rspec analog. !18356 (@blackst0ne) +- Replace "Click" with "Select" to be more inclusive of people with accessibility requirements. !18386 (Mark Lapierre) +- Remove ahead/behind graphs on project branches on mobile. !18415 (Takuya Noguchi) +- Replace the `project/source/markdown_render.feature` spinach test with an rspec analog. !18525 (@blackst0ne) +- Add missing changelog type to docs. !18526 (@blackst0ne) +- Added Webhook SSRF prevention to documentation. !18532 +- Upgrade underscore.js to 1.9.0. !18578 +- Add documentation about how to use variables to define deploy policies for staging/production environments. !18675 +- Replace the `project/builds/artifacts.feature` spinach test with an rspec analog. !18729 (@blackst0ne) +- Block access to the API & git for users that did not accept enforced Terms of Service. !18816 +- Transition to atomic internal ids for all models. !44259 +- Removes modal boards store and mixins from global scope. +- Replace GKE acronym with Google Kubernetes Engine. +- Replace vue resource with axios for pipelines details page. +- Enable prometheus monitoring by default. +- Replace vue resource with axios in pipelines table. +- Bump lograge to 0.10.0 and remove monkey patch. +- Improves wording in new pipeline page. +- Gitaly handles repository forks by default. + + +## 10.7.4 (2018-05-21) + +### Fixed (1 change) + +- Fix error when deleting an empty list of refs. + + ## 10.7.3 (2018-05-02) ### Fixed (8 changes) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f5d19ce2ce..383d13656e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,8 +31,8 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._ - [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc) - [Team labels (~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.)](#team-labels-cicd-discussion-quality-platform-etc) - [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release) - - [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-priority-labels-p1-p2-p3-etc) - - [Severity labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-severity-labels-s1-s2-s3-etc) + - [Priority labels (~P1, ~P2, ~P3 , ~P4)](#bug-priority-labels-p1-p2-p3-p4) + - [Severity labels (~S1, ~S2, ~S3 , ~S4)](#bug-severity-labels-s1-s2-s3-s4) - [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests) - [Implement design & UI elements](#implement-design--ui-elements) - [Issue tracker](#issue-tracker) @@ -168,7 +168,7 @@ hits. They are not always necessary, but very convenient. If you are an expert in a particular area, it makes it easier to find issues to work on. You can also subscribe to those labels to receive an email each time an -issue is labelled with a subject label corresponding to your expertise. +issue is labeled with a subject label corresponding to your expertise. Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api, ~issues, ~"merge requests", ~labels, and ~"container registry". @@ -211,9 +211,10 @@ Each issue scheduled for the current milestone should be labeled ~Deliverable or ~"Stretch". Any open issue for a previous milestone should be labeled ~"Next Patch Release", or otherwise rescheduled to a different milestone. -### Bug Priority labels (~P1, ~P2, ~P3 & etc.) +### Bug Priority labels (~P1, ~P2, ~P3, ~P4) -Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be. If there are multiple defects, the priority decides which defect has to be fixed immediately versus later. +Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be. +If there are multiple defects, the priority decides which defect has to be fixed immediately versus later. This label documents the planned timeline & urgency which is used to measure against our actual SLA on delivering ~bug fixes. | Label | Meaning | Estimate time to fix | Guidance | @@ -223,16 +224,7 @@ This label documents the planned timeline & urgency which is used to measure aga | ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | | | ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented | -#### Specific Priority guidance - -| Label | Availability / Performance | -|-------|--------------------------------------------------------------| -| ~P1 | | -| ~P2 | The issue is (almost) guaranteed to occur in the near future | -| ~P3 | The issue is likely to occur in the near future | -| ~P4 | The issue _may_ occur but it's not likely | - -### Bug Severity labels (~S1, ~S2, ~S3 & etc.) +### Bug Severity labels (~S1, ~S2, ~S3, ~S4) Severity labels help us clearly communicate the impact of a ~bug on users. @@ -243,14 +235,14 @@ Severity labels help us clearly communicate the impact of a ~bug on users. | ~S3 | Major Severity | Broken Feature, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue. | | ~S4 | Low Severity | Functionality inconvenience or cosmetic issue | Label colors are incorrect / not being displayed. | -#### Specific Severity guidance +#### Severity impact guidance -| Label | Security Impact | -|-------|-------------------------------------------------------------------| -| ~S1 | >50% customers impacted (possible company extinction level event) | -| ~S2 | Multiple customers impacted (but not apocalyptic) | -| ~S3 | A single customer impacted | -| ~S4 | No customer impact, or expected impact within 30 days | +| Label | Security Impact | Availability / Performance Impact | +|-------|---------------------------------------------------------------------|--------------------------------------------------------------| +| ~S1 | >50% users impacted (possible company extinction level event) | | +| ~S2 | Many users or multiple paid customers impacted (but not apocalyptic)| The issue is (almost) guaranteed to occur in the near future | +| ~S3 | A few users or a single paid customer impacted | The issue is likely to occur in the near future | +| ~S4 | No paid users/customer impacted, or expected impact within 30 days | The issue _may_ occur but it's not likely | ### Label for community contributors (~"Accepting Merge Requests") @@ -304,7 +296,24 @@ any potential community contributor to @-mention per above. ## Implement design & UI elements -Please see the [UX Guide for GitLab]. +For guidance on UX implementation at GitLab, please refer to our [Design System](https://design.gitlab.com/). + +The UX team uses labels to manage their workflow. + +The ~"UX" label on an issue is a signal to the UX team that it will need UX attention. +To better understand the priority by which UX tackles issues, see the [UX section](https://about.gitlab.com/handbook/ux/) of the handbook. + +Once an issue has been worked on and is ready for development, a UXer applies the ~"UX ready" label to that issue. + +The UX team has a special type label called ~"design artifact". This label indicates that the final output +for an issue is a UX solution/design. The solution will be developed by frontend and/or backend in a subsequent milestone. +Any issue labeled ~"design artifact" should not also be labeled ~"frontend" or ~"backend" since no development is +needed until the solution has been decided. + +~"design artifact" issues are like any other issue and should contain a milestone label, ~"Deliverable" or ~"Stretch", when scheduled in the current milestone. + +Once the ~"design artifact" issue has been completed, the UXer removes the ~"design artifact" label and applies the ~"UX ready" label. The Product Manager can use the +existing issue or decide to create a whole new issue for the purpose of development. ## Issue tracker @@ -728,6 +737,3 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [polling-etag]: https://docs.gitlab.com/ce/development/polling.html [testing]: doc/development/testing_guide/index.md [us-english]: https://en.wikipedia.org/wiki/American_English - -[^1]: Please note that specs other than JavaScript specs are considered backend - code. diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 897e21587ed..7bb21aff834 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.100.0 +0.102.0 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 6aba2b245a8..fae6e3d04b2 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -4.2.0 +4.2.1 @@ -50,7 +50,7 @@ gem 'omniauth-saml', '~> 1.10' gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.4' gem 'omniauth_crowd', '~> 2.2.0' -gem 'omniauth-authentiq', '~> 0.3.1' +gem 'omniauth-authentiq', '~> 0.3.3' gem 'rack-oauth2', '~> 1.2.1' gem 'jwt', '~> 1.5.6' @@ -160,7 +160,7 @@ gem 'state_machines-activerecord', '~> 0.5.1' gem 'acts-as-taggable-on', '~> 5.0' # Background jobs -gem 'sidekiq', '~> 5.0' +gem 'sidekiq', '~> 5.1' gem 'sidekiq-cron', '~> 0.6.0' gem 'redis-namespace', '~> 1.5.2' gem 'sidekiq-limit_fetch', '~> 3.4', require: false @@ -174,6 +174,9 @@ gem 'httparty', '~> 0.13.3' # Colored output to console gem 'rainbow', '~> 2.2' +# Progress bar +gem 'ruby-progressbar' + # GitLab settings gem 'settingslogic', '~> 2.0.9' @@ -254,7 +257,6 @@ gem 'sass-rails', '~> 5.0.6' gem 'uglifier', '~> 2.7.2' gem 'addressable', '~> 2.5.2' -gem 'bootstrap-sass', '~> 3.3.0' gem 'font-awesome-rails', '~> 4.7' gem 'gemojione', '~> 3.3' gem 'gon', '~> 6.2' @@ -294,7 +296,7 @@ group :metrics do gem 'influxdb', '~> 0.2', require: false # Prometheus - gem 'prometheus-client-mmap', '~> 0.9.2' + gem 'prometheus-client-mmap', '~> 0.9.3' gem 'raindrops', '~> 0.18' end @@ -325,8 +327,6 @@ group :development, :test do gem 'factory_bot_rails', '~> 4.8.2' gem 'rspec-rails', '~> 3.6.0' gem 'rspec-retry', '~> 0.4.5' - gem 'spinach-rails', '~> 0.2.1' - gem 'spinach-rerun-reporter', '~> 0.0.2' gem 'rspec_profiling', '~> 0.0.5' gem 'rspec-set', '~> 0.1.3' gem 'rspec-parameterized', require: false @@ -343,7 +343,6 @@ group :development, :test do gem 'spring', '~> 2.0.0' gem 'spring-commands-rspec', '~> 1.0.4' - gem 'spring-commands-spinach', '~> 1.1.0' gem 'gitlab-styles', '~> 2.3', require: false # Pin these dependencies, otherwise a new rule could break the CI pipelines diff --git a/Gemfile.lock b/Gemfile.lock index 6028ce32d2f..7332b55c175 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -69,9 +69,6 @@ GEM attr_encrypted (3.1.0) encryptor (~> 3.0.0) attr_required (1.0.0) - autoprefixer-rails (6.2.3) - execjs - json awesome_print (1.2.0) axiom-types (0.1.1) descendants_tracker (~> 0.0.4) @@ -91,9 +88,6 @@ GEM binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) blankslate (2.1.2.4) - bootstrap-sass (3.3.6) - autoprefixer-rails (>= 5.2.1) - sass (>= 3.3.4) bootstrap_form (2.7.0) brakeman (4.2.1) browser (2.2.0) @@ -131,7 +125,6 @@ GEM coderay (1.1.1) coercible (1.0.0) descendants_tracker (~> 0.0.1) - colorize (0.7.7) commonmarker (0.17.8) ruby-enum (~> 0.5) concord (0.1.5) @@ -288,7 +281,6 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gherkin-ruby (0.3.2) gitaly-proto (0.99.0) google-protobuf (~> 3.1) grpc (~> 1.10) @@ -531,8 +523,9 @@ GEM rack (>= 1.6.2, < 3) omniauth-auth0 (2.0.0) omniauth-oauth2 (~> 1.4) - omniauth-authentiq (0.3.1) - omniauth-oauth2 (~> 1.3, >= 1.3.1) + omniauth-authentiq (0.3.3) + jwt (>= 1.5) + omniauth-oauth2 (>= 1.5) omniauth-azure-oauth2 (0.0.9) jwt (~> 1.0) omniauth (~> 1.0) @@ -631,7 +624,7 @@ GEM parser unparser procto (0.0.3) - prometheus-client-mmap (0.9.2) + prometheus-client-mmap (0.9.3) pry (0.10.4) coderay (~> 1.1.0) method_source (~> 0.8.1) @@ -846,11 +839,11 @@ GEM rack shoulda-matchers (3.1.2) activesupport (>= 4.0.0) - sidekiq (5.0.5) + sidekiq (5.1.3) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) rack-protection (>= 1.5.0) - redis (>= 3.3.4, < 5) + redis (>= 3.3.5, < 5) sidekiq-cron (0.6.0) rufus-scheduler (>= 3.3.0) sidekiq (>= 4.2.1) @@ -869,22 +862,10 @@ GEM simplecov-html (0.10.0) slack-notifier (1.5.1) slop (3.6.0) - spinach (0.8.10) - colorize - gherkin-ruby (>= 0.3.2) - json - spinach-rails (0.2.1) - capybara (>= 2.0.0) - railties (>= 3) - spinach (>= 0.4) - spinach-rerun-reporter (0.0.2) - spinach (~> 0.8) spring (2.0.1) activesupport (>= 4.2) spring-commands-rspec (1.0.4) spring (>= 0.9.1) - spring-commands-spinach (1.1.0) - spring (>= 0.9.1) sprockets (3.7.1) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -1005,7 +986,6 @@ DEPENDENCIES benchmark-ips (~> 2.3.0) better_errors (~> 2.1.0) binding_of_caller (~> 0.7.2) - bootstrap-sass (~> 3.3.0) bootstrap_form (~> 2.7.0) brakeman (~> 4.2) browser (~> 2.2) @@ -1106,7 +1086,7 @@ DEPENDENCIES octokit (~> 4.8) omniauth (~> 1.8) omniauth-auth0 (~> 2.0.0) - omniauth-authentiq (~> 0.3.1) + omniauth-authentiq (~> 0.3.3) omniauth-azure-oauth2 (~> 0.0.9) omniauth-cas3 (~> 1.1.4) omniauth-facebook (~> 4.0.0) @@ -1129,7 +1109,7 @@ DEPENDENCIES peek-sidekiq (~> 1.0.3) pg (~> 0.18.2) premailer-rails (~> 1.9.7) - prometheus-client-mmap (~> 0.9.2) + prometheus-client-mmap (~> 0.9.3) pry-byebug (~> 3.4.1) pry-rails (~> 0.3.4) rack-attack (~> 4.4.1) @@ -1164,6 +1144,7 @@ DEPENDENCIES rubocop-rspec (~> 1.22.1) ruby-fogbugz (~> 0.2.1) ruby-prof (~> 0.17.0) + ruby-progressbar ruby_parser (~> 3.8) rufus-scheduler (~> 3.4) rugged (~> 0.27) @@ -1177,17 +1158,14 @@ DEPENDENCIES settingslogic (~> 2.0.9) sham_rack (~> 1.3.6) shoulda-matchers (~> 3.1.2) - sidekiq (~> 5.0) + sidekiq (~> 5.1) sidekiq-cron (~> 0.6.0) sidekiq-limit_fetch (~> 3.4) simple_po_parser (~> 1.1.2) simplecov (~> 0.14.0) slack-notifier (~> 1.5.1) - spinach-rails (~> 0.2.1) - spinach-rerun-reporter (~> 0.0.2) spring (~> 2.0.0) spring-commands-rspec (~> 1.0.4) - spring-commands-spinach (~> 1.1.0) sprockets (~> 3.7.0) sshkey (~> 1.9.0) stackprof (~> 0.2.10) @@ -1213,4 +1191,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.16.1 + 1.16.2 diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index fc6dfd040c2..af7305619eb 100644 --- a/Gemfile.rails5.lock +++ b/Gemfile.rails5.lock @@ -132,7 +132,6 @@ GEM coderay (1.1.2) coercible (1.0.0) descendants_tracker (~> 0.0.1) - colorize (0.8.1) commonmarker (0.17.9) ruby-enum (~> 0.5) concord (0.1.5) @@ -289,7 +288,6 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gherkin-ruby (0.3.2) gitaly-proto (0.99.0) google-protobuf (~> 3.1) grpc (~> 1.10) @@ -871,22 +869,10 @@ GEM simplecov-html (~> 0.10.0) simplecov-html (0.10.2) slack-notifier (1.5.1) - spinach (0.8.10) - colorize - gherkin-ruby (>= 0.3.2) - json - spinach-rails (0.2.1) - capybara (>= 2.0.0) - railties (>= 3) - spinach (>= 0.4) - spinach-rerun-reporter (0.0.2) - spinach (~> 0.8) spring (2.0.2) activesupport (>= 4.2) spring-commands-rspec (1.0.4) spring (>= 0.9.1) - spring-commands-spinach (1.1.0) - spring (>= 0.9.1) sprockets (3.7.1) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -1187,11 +1173,8 @@ DEPENDENCIES simple_po_parser (~> 1.1.2) simplecov (~> 0.14.0) slack-notifier (~> 1.5.1) - spinach-rails (~> 0.2.1) - spinach-rerun-reporter (~> 0.0.2) spring (~> 2.0.0) spring-commands-rspec (~> 1.0.4) - spring-commands-spinach (~> 1.1.0) sprockets (~> 3.7.0) sshkey (~> 1.9.0) stackprof (~> 0.2.10) @@ -4,9 +4,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ---- - -All Documentation content that resides under the doc/ directory of this -repository is licensed under Creative Commons: CC BY-SA 4.0. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file diff --git a/README.md b/README.md index 9c1aad65307..0266fe82c82 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) [![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines) -[![Dependency Status](https://gemnasium.com/gitlabhq/gitlabhq.svg)](https://gemnasium.com/gitlabhq/gitlabhq) [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42) [![Gitter](https://badges.gitter.im/gitlabhq/gitlabhq.svg)](https://gitter.im/gitlabhq/gitlabhq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) @@ -73,6 +72,8 @@ GitLab Community Edition (CE) is available freely under the MIT Expat license. All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component. +All Documentation content that resides under the doc/ directory of this repository is licensed under Creative Commons: CC BY-SA 4.0. + ## Install a development environment To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit). @@ -1 +1 @@ -10.8.0-pre +11.0.0-pre diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 8ad3d18b302..ce1069276ab 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -21,8 +21,11 @@ const Api = { issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', usersPath: '/api/:version/users.json', commitPath: '/api/:version/projects/:id/repository/commits', + commitPipelinesPath: '/:project_id/commit/:sha/pipelines', branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', createBranchPath: '/api/:version/projects/:id/repository/branches', + pipelinesPath: '/api/:version/projects/:id/pipelines', + pipelineJobsPath: '/api/:version/projects/:id/pipelines/:pipeline_id/jobs', group(groupId, callback) { const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); @@ -164,6 +167,19 @@ const Api = { }); }, + commitPipelines(projectId, sha) { + const encodedProjectId = projectId + .split('/') + .map(fragment => encodeURIComponent(fragment)) + .join('/'); + + const url = Api.buildUrl(Api.commitPipelinesPath) + .replace(':project_id', encodedProjectId) + .replace(':sha', encodeURIComponent(sha)); + + return axios.get(url); + }, + branchSingle(id, branch) { const url = Api.buildUrl(Api.branchSinglePath) .replace(':id', encodeURIComponent(id)) @@ -222,6 +238,20 @@ const Api = { }); }, + pipelines(projectPath, params = {}) { + const url = Api.buildUrl(this.pipelinesPath).replace(':id', encodeURIComponent(projectPath)); + + return axios.get(url, { params }); + }, + + pipelineJobs(projectPath, pipelineId, params = {}) { + const url = Api.buildUrl(this.pipelineJobsPath) + .replace(':id', encodeURIComponent(projectPath)) + .replace(':pipeline_id', pipelineId); + + return axios.get(url, { params }); + }, + buildUrl(url) { let urlRoot = ''; if (gon.relative_url_root != null) { diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 976d32abe9b..eb0f06efab4 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -345,7 +345,7 @@ class AwardsHandler { counter.text(counterNumber - 1); this.removeYouFromUserList($emojiButton); } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') { - $emojiButton.tooltip('destroy'); + $emojiButton.tooltip('dispose'); counter.text('0'); this.removeYouFromUserList($emojiButton); if ($emojiButton.parents('.note').length) { @@ -358,7 +358,7 @@ class AwardsHandler { } removeEmoji($emojiButton) { - $emojiButton.tooltip('destroy'); + $emojiButton.tooltip('dispose'); $emojiButton.remove(); const $votesBlock = this.getVotesBlock(); if ($votesBlock.find('.js-emoji-btn').length === 0) { @@ -392,7 +392,7 @@ class AwardsHandler { .removeAttr('data-title') .removeAttr('data-original-title') .attr('title', this.toSentence(authors)) - .tooltip('fixTitle'); + .tooltip('_fixTitle'); } addYouToUserList(votesBlock, emoji) { @@ -405,7 +405,7 @@ class AwardsHandler { users.unshift('You'); return awardBlock .attr('title', this.toSentence(users)) - .tooltip('fixTitle'); + .tooltip('_fixTitle'); } createAwardButtonForVotesBlock(votesBlock, emojiName) { diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue index 6e6cb31e3ac..d0f60e1d4cb 100644 --- a/app/assets/javascripts/badges/components/badge.vue +++ b/app/assets/javascripts/badges/components/badge.vue @@ -89,7 +89,7 @@ export default { v-show="hasError" class="btn-group" > - <div class="btn btn-default btn-xs disabled"> + <div class="btn btn-default btn-sm disabled"> <icon class="prepend-left-8 append-right-8" name="doc_image" @@ -98,7 +98,7 @@ export default { /> </div> <div - class="btn btn-default btn-xs disabled" + class="btn btn-default btn-sm disabled" > <span class="prepend-left-8 append-right-8">{{ s__('Badges|No badge image') }}</span> </div> @@ -106,7 +106,7 @@ export default { <button v-show="hasError" - class="btn btn-transparent btn-xs text-primary" + class="btn btn-transparent btn-sm text-primary" type="button" v-tooltip :title="s__('Badges|Reload badge image')" diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue index ca7197e1e0f..268968b63b3 100644 --- a/app/assets/javascripts/badges/components/badge_list.vue +++ b/app/assets/javascripts/badges/components/badge_list.vue @@ -23,8 +23,8 @@ export default { </script> <template> - <div class="panel panel-default"> - <div class="panel-heading"> + <div class="card"> + <div class="card-header"> {{ s__('Badges|Your badges') }} <span v-show="!isLoading" @@ -33,19 +33,19 @@ export default { </div> <loading-icon v-show="isLoading" - class="panel-body" + class="card-body" size="2" /> <div v-if="hasNoBadges" - class="panel-body" + class="card-body" > <span v-if="isGroupBadge">{{ s__('Badges|This group has no badges') }}</span> <span v-else>{{ s__('Badges|This project has no badges') }}</span> </div> <div v-else - class="panel-body" + class="card-body" > <badge-list-row v-for="badge in badges" diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js index e2a73a1797c..75834ba351d 100644 --- a/app/assets/javascripts/behaviors/copy_to_clipboard.js +++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js @@ -8,10 +8,10 @@ function showTooltip(target, title) { if (!$target.data('hideTooltip')) { $target .attr('title', title) - .tooltip('fixTitle') + .tooltip('_fixTitle') .tooltip('show') .attr('title', originalTitle) - .tooltip('fixTitle'); + .tooltip('_fixTitle'); } } diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index 3ec932bdb73..b6e2781773c 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -69,7 +69,7 @@ $(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-q $this.tooltip({ container: 'body', html: 'true', - placement: 'auto top', + placement: 'top', title, trigger: 'manual', }); diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js index ffff4ddb71a..a8b6dbf0948 100644 --- a/app/assets/javascripts/behaviors/requires_input.js +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -42,9 +42,9 @@ $.fn.requiresInput = function requiresInput() { function hideOrShowHelpBlock(form) { const selected = $('.js-select-namespace option:selected'); if (selected.length && selected.data('optionsParent') === 'groups') { - form.find('.help-block').hide(); + form.find('.form-text.text-muted').hide(); } else if (selected.length) { - form.find('.help-block').show(); + form.find('.form-text.text-muted').show(); } } diff --git a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js index c17877a276d..766039404ce 100644 --- a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js +++ b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js @@ -2,9 +2,9 @@ import sqljs from 'sql.js'; import { template as _template } from 'underscore'; const PREVIEW_TEMPLATE = _template(` - <div class="panel panel-default"> - <div class="panel-heading"><%- name %></div> - <div class="panel-body"> + <div class="card"> + <div class="card-header"><%- name %></div> + <div class="card-body"> <img class="img-thumbnail" src="data:image/png;base64,<%- image %>"/> </div> </div> diff --git a/app/assets/javascripts/blob/sketch/index.js b/app/assets/javascripts/blob/sketch/index.js index 0799991aa40..13318c58006 100644 --- a/app/assets/javascripts/blob/sketch/index.js +++ b/app/assets/javascripts/blob/sketch/index.js @@ -44,7 +44,7 @@ export default class SketchLoader { previewLink.href = previewUrl; previewLink.target = '_blank'; previewImage.src = previewUrl; - previewImage.className = 'img-responsive'; + previewImage.className = 'img-fluid'; previewLink.appendChild(previewImage); this.container.appendChild(previewLink); diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index 137e1f5a099..f61c0be9230 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -116,7 +116,7 @@ export default class BlobViewer { this.copySourceBtn.classList.add('disabled'); } - $(this.copySourceBtn).tooltip('fixTitle'); + $(this.copySourceBtn).tooltip('_fixTitle'); } switchToViewer(name) { diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 84885ca9306..33e3369b971 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -77,7 +77,7 @@ export default { <template> <li - class="card" + class="board-card" :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index 8d84c1735b8..e8dfd95f7ae 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -92,7 +92,7 @@ export default { <template> <div class="board-new-issue-form"> - <div class="card"> + <div class="board-card"> <form @submit="submit($event)"> <div class="flash-container" @@ -122,7 +122,7 @@ export default { /> <div class="clearfix prepend-top-10"> <button - class="btn btn-success pull-left" + class="btn btn-success float-left" type="submit" :disabled="disabled" ref="submit-button" @@ -130,7 +130,7 @@ export default { Submit issue </button> <button - class="btn btn-default pull-right" + class="btn btn-default float-right" type="button" @click="cancel" > diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index 84fe9b1288a..dcc07810d01 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -135,8 +135,8 @@ gl.issueBoards.IssueCardInner = Vue.extend({ }, template: ` <div> - <div class="card-header"> - <h4 class="card-title"> + <div class="board-card-header"> + <h4 class="board-card-title"> <i class="fa fa-eye-slash confidential-icon" v-if="issue.confidential" @@ -147,13 +147,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({ :href="issue.path" :title="issue.title">{{ issue.title }}</a> <span - class="card-number" + class="board-card-number" v-if="issueId" > {{ issue.referencePath }} </span> </h4> - <div class="card-assignee"> + <div class="board-card-assignee"> <user-avatar-link v-for="(assignee, index) in issue.assignees" :key="assignee.id" @@ -175,11 +175,11 @@ gl.issueBoards.IssueCardInner = Vue.extend({ </div> </div> <div - class="card-footer" + class="board-card-footer" v-if="showLabelFooter" > <button - class="label color-label has-tooltip" + class="badge color-label has-tooltip" v-for="label in issue.labels" type="button" v-if="showLabel(label)" diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js index 9e37f95cdd6..eb8a66975ee 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.js +++ b/app/assets/javascripts/boards/components/modal/empty_state.js @@ -41,10 +41,10 @@ gl.issueBoards.ModalEmptyState = Vue.extend({ template: ` <section class="empty-state"> <div class="row"> - <div class="col-xs-12 col-sm-6 col-sm-push-6"> + <div class="col-xs-12 col-sm-6 order-sm-last"> <aside class="svg-content"><img :src="emptyStateSvg"/></aside> </div> - <div class="col-xs-12 col-sm-6 col-sm-pull-6"> + <div class="col-xs-12 col-sm-6 order-sm-first"> <div class="text-content"> <h4>{{ contents.title }}</h4> <p v-html="contents.content"></p> diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js index 9735e0ddacc..11bb3e98334 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js +++ b/app/assets/javascripts/boards/components/modal/footer.js @@ -58,7 +58,7 @@ gl.issueBoards.ModalFooter = Vue.extend({ template: ` <footer class="form-actions add-issues-footer"> - <div class="pull-left"> + <div class="float-left"> <button class="btn btn-success" type="button" @@ -72,7 +72,7 @@ gl.issueBoards.ModalFooter = Vue.extend({ <lists-dropdown></lists-dropdown> </div> <button - class="btn btn-default pull-right" + class="btn btn-default float-right" type="button" @click="toggleModal(false)"> Cancel diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js index 6b04a6c7a6c..6c662432037 100644 --- a/app/assets/javascripts/boards/components/modal/list.js +++ b/app/assets/javascripts/boards/components/modal/list.js @@ -133,9 +133,9 @@ gl.issueBoards.ModalList = Vue.extend({ <div v-for="issue in group" v-if="showIssue(issue)" - class="card-parent"> + class="board-card-parent"> <div - class="card" + class="board-card" :class="{ 'is-active': issue.selected }" @click="toggleIssue($event, issue)"> <issue-card-inner diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js index b6465a88e5e..9d331de8e22 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.js +++ b/app/assets/javascripts/boards/components/modal/tabs.js @@ -24,7 +24,7 @@ gl.issueBoards.ModalTabs = Vue.extend({ role="button" @click.prevent="changeTab('all')"> Open issues - <span class="badge"> + <span class="badge badge-pill"> {{ issuesCount }} </span> </a> @@ -35,7 +35,7 @@ gl.issueBoards.ModalTabs = Vue.extend({ role="button" @click.prevent="changeTab('selected')"> Selected issues - <span class="badge"> + <span class="badge badge-pill"> {{ selectedCount }} </span> </a> diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index a6f8681cfac..29ab13b8e0b 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -214,7 +214,7 @@ export default () => { if (this.disabled) { $tooltip.tooltip(); } else { - $tooltip.tooltip('destroy'); + $tooltip.tooltip('dispose'); } }); }, diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js index e177a3bfdc7..47efb3a8cee 100644 --- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js @@ -141,6 +141,11 @@ export default class VariableList { $rowClone.find(entry.selector).val(entry.default); }); + // Close any dropdowns + $rowClone.find('.dropdown-menu.show').each((index, $dropdown) => { + $dropdown.classList.remove('show'); + }); + this.initRow($rowClone); $row.after($rowClone); diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index c2a35341eb2..fae580c091b 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -179,7 +179,7 @@ role="row" > <div - class="alert alert-danger alert-block append-bottom-0 table-section section-100" + class="alert alert-danger alert-block append-bottom-0" role="gridcell" > <div> diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 9c12b89240c..bb5fcea648d 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -191,11 +191,11 @@ export default { :value="ingressExternalIp" readonly /> - <span class="input-group-btn"> + <span class="input-group-append"> <clipboard-button :text="ingressExternalIp" :title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')" - class="js-clipboard-btn" + class="input-group-text js-clipboard-btn" /> </span> </div> diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js index db96da4ccba..50e2949ab55 100644 --- a/app/assets/javascripts/commons/bootstrap.js +++ b/app/assets/javascripts/commons/bootstrap.js @@ -1,15 +1,7 @@ import $ from 'jquery'; // bootstrap jQuery plugins -import 'bootstrap-sass/assets/javascripts/bootstrap/affix'; -import 'bootstrap-sass/assets/javascripts/bootstrap/alert'; -import 'bootstrap-sass/assets/javascripts/bootstrap/button'; -import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown'; -import 'bootstrap-sass/assets/javascripts/bootstrap/modal'; -import 'bootstrap-sass/assets/javascripts/bootstrap/tab'; -import 'bootstrap-sass/assets/javascripts/bootstrap/transition'; -import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip'; -import 'bootstrap-sass/assets/javascripts/bootstrap/popover'; +import 'bootstrap'; // custom jQuery functions $.fn.extend({ diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js index 9c88466e576..ffe15f02f2e 100644 --- a/app/assets/javascripts/compare_autocomplete.js +++ b/app/assets/javascripts/compare_autocomplete.js @@ -54,7 +54,7 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = ( .attr('href', '#') .addClass(ref === selected ? 'is-active' : '') .text(ref) - .attr('data-ref', escape(ref)); + .attr('data-ref', ref); return $('<li />').append(link); } }, @@ -78,7 +78,7 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = ( $dropdownContainer.on('click', '.dropdown-content a', e => { $dropdown.prop('title', e.target.text.replace(/_+?/g, '-')); if ($dropdown.hasClass('has-tooltip')) { - $dropdown.tooltip('fixTitle'); + $dropdown.tooltip('_fixTitle'); } }); }); diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index a88b6971f90..09d490106df 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -61,8 +61,8 @@ export default class CreateMergeRequestDropdown { } available() { - this.availableButton.classList.remove('hide'); - this.unavailableButton.classList.add('hide'); + this.availableButton.classList.remove('hidden'); + this.unavailableButton.classList.add('hidden'); } bindEvents() { @@ -232,7 +232,7 @@ export default class CreateMergeRequestDropdown { } hide() { - this.wrapperEl.classList.add('hide'); + this.wrapperEl.classList.add('hidden'); } init() { @@ -406,8 +406,8 @@ export default class CreateMergeRequestDropdown { } unavailable() { - this.availableButton.classList.add('hide'); - this.unavailableButton.classList.remove('hide'); + this.availableButton.classList.add('hidden'); + this.unavailableButton.classList.remove('hidden'); } updateBranchName(suggestedBranchName) { diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue index 32ae0cc1476..5be17081b58 100644 --- a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue @@ -16,7 +16,7 @@ <template> <span v-if="count === 50" - class="events-info pull-right" + class="events-info float-right" > <i class="fa fa-warning" diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js index 180a6bd67e7..fe9b0795609 100644 --- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -79,7 +79,7 @@ const DiffNoteAvatars = Vue.extend({ storeState: { handler() { this.$nextTick(() => { - $('.has-tooltip', this.$el).tooltip('fixTitle'); + $('.has-tooltip', this.$el).tooltip('_fixTitle'); // We need to add/remove a class to an element that is outside the Vue instance this.addNoCommentClass(); @@ -138,7 +138,7 @@ const DiffNoteAvatars = Vue.extend({ this.$nextTick(() => { this.setDiscussionVisible(); - $('.has-tooltip', this.$el).tooltip('fixTitle'); + $('.has-tooltip', this.$el).tooltip('_fixTitle'); $('.has-tooltip', this.$el).tooltip('hide'); }); }, diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js index df4c72ba0ed..8d66417abac 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js @@ -61,7 +61,7 @@ const ResolveBtn = Vue.extend({ this.$nextTick(() => { $(this.$refs.button) .tooltip('hide') - .tooltip('fixTitle'); + .tooltip('_fixTitle'); }); }, resolve: function () { diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index ab9e22037d0..0b3fef9fcca 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -74,7 +74,7 @@ </span> </button> - <ul class="dropdown-menu dropdown-menu-align-right"> + <ul class="dropdown-menu dropdown-menu-right"> <li v-for="(action, i) in actions" :key="i"> diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 79326ca3487..23aaab2c441 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -486,14 +486,14 @@ {{ model.folderName }} </span> - <span class="badge"> + <span class="badge badge-pill"> {{ model.size }} </span> </span> </div> <div - class="table-section section-10 deployment-column hidden-xs hidden-sm" + class="table-section section-10 deployment-column d-none d-sm-none d-md-block" role="gridcell" > <span v-if="shouldRenderDeploymentID"> @@ -513,7 +513,7 @@ </div> <div - class="table-section section-15 hidden-xs hidden-sm" + class="table-section section-15 d-none d-sm-none d-md-block" role="gridcell" > <a diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue index deada134b27..8df1b6317e3 100644 --- a/app/assets/javascripts/environments/components/environment_monitoring.vue +++ b/app/assets/javascripts/environments/components/environment_monitoring.vue @@ -28,7 +28,7 @@ <template> <a v-tooltip - class="btn monitoring-url hidden-xs hidden-sm" + class="btn monitoring-url d-none d-sm-none d-md-block" data-container="body" rel="noopener noreferrer nofollow" :href="monitoringUrl" diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue index c822fb1574c..7515d711c50 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.vue +++ b/app/assets/javascripts/environments/components/environment_rollback.vue @@ -40,7 +40,7 @@ <template> <button type="button" - class="btn hidden-xs hidden-sm" + class="btn d-none d-sm-none d-md-block" @click="onClick" :disabled="isLoading" > diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue index dda7429a726..7055f208451 100644 --- a/app/assets/javascripts/environments/components/environment_stop.vue +++ b/app/assets/javascripts/environments/components/environment_stop.vue @@ -43,7 +43,7 @@ if (confirm('Are you sure you want to stop this environment?')) { this.isLoading = true; - $(this.$el).tooltip('destroy'); + $(this.$el).tooltip('dispose'); eventHub.$emit('postAction', this.stopUrl); } @@ -55,7 +55,7 @@ <button v-tooltip type="button" - class="btn stop-env-link hidden-xs hidden-sm" + class="btn stop-env-link d-none d-sm-none d-md-block" data-container="body" @click="onClick" :disabled="isLoading" diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue index e8469d088ef..0dbbbb75e07 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.vue +++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue @@ -30,7 +30,7 @@ <template> <a v-tooltip - class="btn terminal-button hidden-xs hidden-sm" + class="btn terminal-button d-none d-sm-none d-md-block" data-container="body" :title="title" :aria-label="title" diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index c0be72f7401..3da762446c9 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -68,8 +68,7 @@ this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader); this.service.getFolderContent(folder.folder_path) - .then(resp => resp.json()) - .then(response => this.store.setfolderContent(folder, response.environments)) + .then(response => this.store.setfolderContent(folder, response.data.environments)) .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false)) .catch(() => { Flash(s__('Environments|An error occurred while fetching the environments.')); diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js index 34d18d55120..a7a79dbca70 100644 --- a/app/assets/javascripts/environments/mixins/environments_mixin.js +++ b/app/assets/javascripts/environments/mixins/environments_mixin.js @@ -6,7 +6,6 @@ import Visibility from 'visibilityjs'; import Poll from '../../lib/utils/poll'; import { getParameterByName, - parseQueryStringIntoObject, } from '../../lib/utils/common_utils'; import { s__ } from '../../locale'; import Flash from '../../flash'; @@ -46,17 +45,14 @@ export default { methods: { saveData(resp) { - const headers = resp.headers; - return resp.json().then((response) => { - this.isLoading = false; - - if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) { - this.store.storeAvailableCount(response.available_count); - this.store.storeStoppedCount(response.stopped_count); - this.store.storeEnvironments(response.environments); - this.store.setPagination(headers); - } - }); + this.isLoading = false; + + if (_.isEqual(resp.config.params, this.requestData)) { + this.store.storeAvailableCount(resp.data.available_count); + this.store.storeStoppedCount(resp.data.stopped_count); + this.store.storeEnvironments(resp.data.environments); + this.store.setPagination(resp.headers); + } }, /** @@ -70,7 +66,7 @@ export default { updateContent(parameters) { this.updateInternalState(parameters); // fetch new data - return this.service.get(this.requestData) + return this.service.fetchEnvironments(this.requestData) .then(response => this.successCallback(response)) .then(() => { // restart polling @@ -105,7 +101,7 @@ export default { fetchEnvironments() { this.isLoading = true; - return this.service.get(this.requestData) + return this.service.fetchEnvironments(this.requestData) .then(this.successCallback) .catch(this.errorCallback); }, @@ -141,7 +137,7 @@ export default { this.poll = new Poll({ resource: this.service, - method: 'get', + method: 'fetchEnvironments', data: this.requestData, successCallback: this.successCallback, errorCallback: this.errorCallback, diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js index 03ab74b3338..3b121551aca 100644 --- a/app/assets/javascripts/environments/services/environments_service.js +++ b/app/assets/javascripts/environments/services/environments_service.js @@ -1,25 +1,22 @@ -/* eslint-disable class-methods-use-this */ -import Vue from 'vue'; -import VueResource from 'vue-resource'; - -Vue.use(VueResource); +import axios from '~/lib/utils/axios_utils'; export default class EnvironmentsService { constructor(endpoint) { - this.environments = Vue.resource(endpoint); + this.environmentsEndpoint = endpoint; this.folderResults = 3; } - get(options = {}) { + fetchEnvironments(options = {}) { const { scope, page } = options; - return this.environments.get({ scope, page }); + return axios.get(this.environmentsEndpoint, { params: { scope, page } }); } + // eslint-disable-next-line class-methods-use-this postAction(endpoint) { - return Vue.http.post(endpoint, {}, { emulateJSON: true }); + return axios.post(endpoint, {}, { emulateJSON: true }); } getFolderContent(folderUrl) { - return Vue.http.get(`${folderUrl}.json?per_page=${this.folderResults}`); + return axios.get(`${folderUrl}.json?per_page=${this.folderResults}`); } } diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js index 2d5bae9a9c4..2f27c9351bc 100644 --- a/app/assets/javascripts/feature_highlight/feature_highlight.js +++ b/app/assets/javascripts/feature_highlight/feature_highlight.js @@ -24,7 +24,7 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) { template: ` <div class="popover feature-highlight-popover" role="tooltip"> <div class="arrow"></div> - <div class="popover-content"></div> + <div class="popover-body"></div> </div> `, }) diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index fa48d7d1915..746a06b7c4f 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -374,7 +374,7 @@ GitLabDropdown = (function() { $relatedTarget = $(e.relatedTarget); $dropdownMenu = $relatedTarget.closest('.dropdown-menu'); if ($dropdownMenu.length === 0) { - return _this.dropdown.removeClass('open'); + return _this.dropdown.removeClass('show'); } } }; @@ -801,7 +801,7 @@ GitLabDropdown = (function() { if (this.options.filterable) { const initialScrollTop = $(window).scrollTop(); - if (this.dropdown.is('.open')) { + if (this.dropdown.is('.show') && !this.filterInput.is(':focus')) { this.filterInput.focus(); } diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js index 972b2252acb..87c6e37b9fb 100644 --- a/app/assets/javascripts/gl_field_error.js +++ b/app/assets/javascripts/gl_field_error.js @@ -62,7 +62,7 @@ export default class GlFieldError { this.inputDomElement = this.inputElement.get(0); this.form = formErrors; this.errorMessage = this.inputElement.attr('title') || 'This field is required.'; - this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${this.errorMessage}</p>`); + this.fieldErrorElement = $(`<p class='${errorMessageClass} hidden'>${this.errorMessage}</p>`); this.state = { valid: false, @@ -146,8 +146,8 @@ export default class GlFieldError { renderInvalid() { this.inputElement.addClass(inputErrorClass); - this.scopedSiblings.hide(); - return this.fieldErrorElement.show(); + this.scopedSiblings.addClass('hidden'); + return this.fieldErrorElement.removeClass('hidden'); } renderClear() { @@ -157,7 +157,7 @@ export default class GlFieldError { this.accessCurrentValue(trimmedInput); } this.inputElement.removeClass(inputErrorClass); - this.scopedSiblings.hide(); - this.fieldErrorElement.hide(); + this.scopedSiblings.addClass('hidden'); + this.fieldErrorElement.addClass('hidden'); } } diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 764b130fdb8..7f64a9bd741 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -99,7 +99,7 @@ export default { /> </div> <div - class="avatar-container prepend-top-8 prepend-left-5 s24 hidden-xs" + class="avatar-container prepend-top-8 prepend-left-5 s24 d-none d-sm-block" :class="{ 'content-loading': group.isChildrenLoading }" > <a diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue index 4a645c827ad..81961fe3c57 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue @@ -5,7 +5,7 @@ import LoadingButton from '~/vue_shared/components/loading_button.vue'; import CommitMessageField from './message_field.vue'; import Actions from './actions.vue'; import SuccessMessage from './success_message.vue'; -import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT, COMMIT_ITEM_PADDING } from '../../constants'; +import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants'; export default { components: { @@ -70,7 +70,7 @@ export default { ? this.$refs.formEl && this.$refs.formEl.offsetHeight : this.$refs.compactEl && this.$refs.compactEl.offsetHeight; - this.componentHeight = elHeight + COMMIT_ITEM_PADDING; + this.componentHeight = elHeight; }, enterTransition() { this.$nextTick(() => { @@ -78,7 +78,7 @@ export default { ? this.$refs.compactEl && this.$refs.compactEl.offsetHeight : this.$refs.formEl && this.$refs.formEl.offsetHeight; - this.componentHeight = elHeight + COMMIT_ITEM_PADDING; + this.componentHeight = elHeight; }); }, afterEndTransition() { diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue index ea2b13a8b21..cabb3f59b17 100644 --- a/app/assets/javascripts/ide/components/file_finder/index.vue +++ b/app/assets/javascripts/ide/components/file_finder/index.vue @@ -39,12 +39,10 @@ export default { return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS); } - return fuzzaldrinPlus - .filter(this.allBlobs, searchText, { - key: 'path', - maxResults: MAX_FILE_FINDER_RESULTS, - }) - .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt); + return fuzzaldrinPlus.filter(this.allBlobs, searchText, { + key: 'path', + maxResults: MAX_FILE_FINDER_RESULTS, + }); }, filteredBlobsLength() { return this.filteredBlobs.length; diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 6c373a92776..2c184ea726c 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -52,7 +52,10 @@ export default { methods: { ...mapActions(['toggleFileFinder']), mousetrapStopCallback(e, el, combo) { - if (combo === 't' && el.classList.contains('dropdown-input-field')) { + if ( + (combo === 't' && el.classList.contains('dropdown-input-field')) || + el.classList.contains('inputarea') + ) { return true; } else if (combo === 'command+p' || combo === 'ctrl+p') { return false; @@ -99,12 +102,12 @@ export default { class="ide-empty-state" > <div class="row js-empty-state"> - <div class="col-xs-12"> + <div class="col-12"> <div class="svg-content svg-250"> <img :src="emptyStateSvgPath" /> </div> </div> - <div class="col-xs-12"> + <div class="col-12"> <div class="text-content text-center"> <h4> Welcome to the GitLab IDE @@ -120,8 +123,6 @@ export default { </template> </div> </div> - <ide-status-bar - :file="activeFile" - /> + <ide-status-bar :file="activeFile"/> </article> </template> diff --git a/app/assets/javascripts/ide/components/ide_file_buttons.vue b/app/assets/javascripts/ide/components/ide_file_buttons.vue index a6c6f46a144..30b00abf6ed 100644 --- a/app/assets/javascripts/ide/components/ide_file_buttons.vue +++ b/app/assets/javascripts/ide/components/ide_file_buttons.vue @@ -32,14 +32,14 @@ export default { <template> <div v-if="showButtons" - class="pull-right ide-btn-group" + class="float-right ide-btn-group" > <a v-tooltip v-if="!file.binary" :href="file.blamePath" :title="__('Blame')" - class="btn btn-xs btn-transparent blame" + class="btn btn-sm btn-transparent blame" > <icon name="blame" @@ -50,7 +50,7 @@ export default { v-tooltip :href="file.commitsPath" :title="__('History')" - class="btn btn-xs btn-transparent history" + class="btn btn-sm btn-transparent history" > <icon name="history" @@ -61,7 +61,7 @@ export default { v-tooltip :href="file.permalink" :title="__('Permalink')" - class="btn btn-xs btn-transparent permalink" + class="btn btn-sm btn-transparent permalink" > <icon name="link" @@ -72,7 +72,7 @@ export default { v-tooltip :href="file.rawPath" target="_blank" - class="btn btn-xs btn-transparent prepend-left-10 raw" + class="btn btn-sm btn-transparent prepend-left-10 raw" rel="noopener noreferrer" :title="rawDownloadButtonLabel"> <icon diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index 70c6d53c3ab..6f60cfbf184 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -1,14 +1,16 @@ <script> -import { mapGetters } from 'vuex'; +import { mapActions, mapState, mapGetters } from 'vuex'; import icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; import timeAgoMixin from '~/vue_shared/mixins/timeago'; +import CiIcon from '../../vue_shared/components/ci_icon.vue'; import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; export default { components: { icon, userAvatarImage, + CiIcon, }, directives: { tooltip, @@ -27,8 +29,16 @@ export default { }; }, computed: { + ...mapState(['currentBranchId', 'currentProjectId']), ...mapGetters(['currentProject', 'lastCommit']), }, + watch: { + lastCommit() { + if (!this.isPollingInitialized) { + this.initPipelinePolling(); + } + }, + }, mounted() { this.startTimer(); }, @@ -36,13 +46,21 @@ export default { if (this.intervalId) { clearInterval(this.intervalId); } + if (this.isPollingInitialized) { + this.stopPipelinePolling(); + } }, methods: { + ...mapActions(['pipelinePoll', 'stopPipelinePolling']), startTimer() { this.intervalId = setInterval(() => { this.commitAgeUpdate(); }, 1000); }, + initPipelinePolling() { + this.pipelinePoll(); + this.isPollingInitialized = true; + }, commitAgeUpdate() { if (this.lastCommit) { this.lastCommitFormatedAge = this.timeFormated(this.lastCommit.committed_date); @@ -61,6 +79,23 @@ export default { class="ide-status-branch" v-if="lastCommit && lastCommitFormatedAge" > + <span + class="ide-status-pipeline" + v-if="lastCommit.pipeline && lastCommit.pipeline.details" + > + <ci-icon + :status="lastCommit.pipeline.details.status" + v-tooltip + :title="lastCommit.pipeline.details.status.text" + /> + Pipeline + <a + class="monospace" + :href="lastCommit.pipeline.details.status.details_path">#{{ lastCommit.pipeline.id }}</a> + {{ lastCommit.pipeline.details.status.text }} + for + </span> + <icon name="commit" /> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index a0ce1c9dac7..f0b29702497 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -57,7 +57,7 @@ export default { <div class="dropdown" :class="{ - open: dropdownOpen, + show: dropdownOpen, }" > <button @@ -69,12 +69,12 @@ export default { <icon name="plus" :size="12" - css-classes="pull-left" + css-classes="float-left" /> <icon name="arrow-down" :size="12" - css-classes="pull-left" + css-classes="float-left" /> </button> <ul diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index a95a0225950..d83a90f71e1 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -70,12 +70,12 @@ export default { @submit="createEntryInStore" > <form - class="form-horizontal" slot="body" @submit.prevent="createEntryInStore" + class="form-group row append-bottom-0" > <fieldset class="form-group append-bottom-0"> - <label class="label-light col-sm-3 ide-new-modal-label"> + <label class="label-light col-form-label col-sm-3 ide-new-modal-label"> {{ __('Name') }} </label> <div class="col-sm-9"> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index f8678b602ac..a281ecb95c8 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -43,9 +43,13 @@ export default { }, }, watch: { - file(oldVal, newVal) { + file(newVal, oldVal) { + if (oldVal.pending) { + this.removePendingTab(oldVal); + } + // Compare key to allow for files opened in review mode to be cached differently - if (newVal.key !== this.file.key) { + if (oldVal.key !== this.file.key) { this.initMonaco(); if (this.currentActivityView !== activityBarViews.edit) { @@ -99,6 +103,7 @@ export default { 'setFileViewMode', 'setFileEOL', 'updateViewer', + 'removePendingTab', ]), initMonaco() { if (this.shouldHideEditor) return; @@ -192,7 +197,7 @@ export default { > <div class="ide-mode-tabs clearfix" > <ul - class="nav-links pull-left" + class="nav-links float-left" v-if="!shouldHideEditor && isEditModeActive" > <li :class="editTabCSS"> diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index 14946f8c9fa..442697e1c80 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -122,11 +122,11 @@ export default { <div class="file" :class="fileClass" + @click="clickFile" + role="button" > <div class="file-name" - @click="clickFile" - role="button" > <span class="ide-file-name str-truncated" @@ -144,7 +144,7 @@ export default { :file="file" /> </span> - <span class="pull-right ide-file-icon-holder"> + <span class="float-right ide-file-icon-holder"> <mr-file-icon v-if="file.mrChange" /> @@ -177,7 +177,7 @@ export default { :project-id="file.projectId" :branch="file.branchId" :path="file.path" - class="pull-right prepend-left-8" + class="float-right prepend-left-8" /> </div> </div> diff --git a/app/assets/javascripts/ide/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue index 79af8c0b0c7..3e47da88050 100644 --- a/app/assets/javascripts/ide/components/repo_loading_file.vue +++ b/app/assets/javascripts/ide/components/repo_loading_file.vue @@ -25,13 +25,13 @@ /> </td> <template v-if="!leftPanelCollapsed"> - <td class="hidden-sm hidden-xs"> + <td class="d-none d-sm-none d-md-block"> <skeleton-loading-container :small="true" /> </td> - <td class="hidden-xs"> + <td class="d-none d-sm-block"> <skeleton-loading-container class="animation-container-right" :small="true" diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index 48d4cc43198..83fe22f40a4 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -5,8 +5,6 @@ export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33; export const MAX_WINDOW_HEIGHT_COMPACT = 750; -export const COMMIT_ITEM_PADDING = 32; - // Commit message textarea export const MAX_TITLE_LENGTH = 50; export const MAX_BODY_LENGTH = 72; diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index adca85dc65b..a21cec4e8d8 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -41,7 +41,7 @@ const router = new VueRouter({ component: EmptyRouterComponent, children: [ { - path: ':targetmode(edit|tree|blob)/:branch/*', + path: ':targetmode(edit|tree|blob)/*', component: EmptyRouterComponent, }, { @@ -63,23 +63,27 @@ router.beforeEach((to, from, next) => { .then(() => { const fullProjectId = `${to.params.namespace}/${to.params.project}`; - if (to.params.branch) { - store.dispatch('setCurrentBranchId', to.params.branch); + const baseSplit = to.params[0].split('/-/'); + const branchId = baseSplit[0].slice(-1) === '/' ? baseSplit[0].slice(0, -1) : baseSplit[0]; + + if (branchId) { + const basePath = baseSplit.length > 1 ? baseSplit[1] : ''; + + store.dispatch('setCurrentBranchId', branchId); store.dispatch('getBranchData', { projectId: fullProjectId, - branchId: to.params.branch, + branchId, }); store .dispatch('getFiles', { projectId: fullProjectId, - branchId: to.params.branch, + branchId, }) .then(() => { - if (to.params[0]) { - const path = - to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0]; + if (basePath) { + const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; const treeEntryKey = Object.keys(store.state.entries).find( key => key === path && !store.state.entries[key].pending, ); diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index a12e637616a..e8b51f2b516 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -75,4 +75,8 @@ export default { }, }); }, + lastCommitPipelines({ getters }) { + const commitSha = getters.lastCommit.id; + return Api.commitPipelines(getters.currentProject.path_with_namespace, commitSha); + }, }; diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index b6baa693104..13aea91d8ba 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -63,7 +63,9 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive const file = state.entries[path]; commit(types.TOGGLE_LOADING, { entry: file }); return service - .getFileData(`${gon.relative_url_root ? gon.relative_url_root : ''}${file.url}`) + .getFileData( + `${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`, + ) .then(res => { const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']); setPageTitle(pageTitle); diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index eff9bc03651..cece9154c82 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -1,6 +1,11 @@ +import Visibility from 'visibilityjs'; import flash from '~/flash'; +import { __ } from '~/locale'; import service from '../../services'; import * as types from '../mutation_types'; +import Poll from '../../../lib/utils/poll'; + +let eTagPoll; export const getProjectData = ( { commit, state, dispatch }, @@ -21,7 +26,7 @@ export const getProjectData = ( }) .catch(() => { flash( - 'Error loading project data. Please try again.', + __('Error loading project data. Please try again.'), 'alert', document, null, @@ -59,7 +64,7 @@ export const getBranchData = ( }) .catch(() => { flash( - 'Error loading branch data. Please try again.', + __('Error loading branch data. Please try again.'), 'alert', document, null, @@ -73,25 +78,74 @@ export const getBranchData = ( } }); -export const refreshLastCommitData = ( - { commit, state, dispatch }, - { projectId, branchId } = {}, -) => service - .getBranchData(projectId, branchId) - .then(({ data }) => { - commit(types.SET_BRANCH_COMMIT, { - projectId, - branchId, - commit: data.commit, +export const refreshLastCommitData = ({ commit, state, dispatch }, { projectId, branchId } = {}) => + service + .getBranchData(projectId, branchId) + .then(({ data }) => { + commit(types.SET_BRANCH_COMMIT, { + projectId, + branchId, + commit: data.commit, + }); + }) + .catch(() => { + flash(__('Error loading last commit.'), 'alert', document, null, false, true); }); - }) - .catch(() => { - flash( - 'Error loading last commit.', - 'alert', - document, - null, - false, - true, + +export const pollSuccessCallBack = ({ commit, state, dispatch }, { data }) => { + if (data.pipelines && data.pipelines.length) { + const lastCommitHash = + state.projects[state.currentProjectId].branches[state.currentBranchId].commit.id; + const lastCommitPipeline = data.pipelines.find( + pipeline => pipeline.commit.id === lastCommitHash, ); + commit(types.SET_LAST_COMMIT_PIPELINE, { + projectId: state.currentProjectId, + branchId: state.currentBranchId, + pipeline: lastCommitPipeline || {}, + }); + } + + return data; +}; + +export const pipelinePoll = ({ getters, dispatch }) => { + eTagPoll = new Poll({ + resource: service, + method: 'lastCommitPipelines', + data: { + getters, + }, + successCallback: ({ data }) => dispatch('pollSuccessCallBack', { data }), + errorCallback: () => { + flash( + __('Something went wrong while fetching the latest pipeline status.'), + 'alert', + document, + null, + false, + true, + ); + }, }); + + if (!Visibility.hidden()) { + eTagPoll.makeRequest(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + eTagPoll.restart(); + } else { + eTagPoll.stop(); + } + }); +}; + +export const stopPipelinePolling = () => { + eTagPoll.stop(); +}; + +export const restartPipelinePolling = () => { + eTagPoll.restart(); +}; diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js index 7c82ce7976b..699710055e3 100644 --- a/app/assets/javascripts/ide/stores/index.js +++ b/app/assets/javascripts/ide/stores/index.js @@ -5,6 +5,7 @@ import * as actions from './actions'; import * as getters from './getters'; import mutations from './mutations'; import commitModule from './modules/commit'; +import pipelines from './modules/pipelines'; Vue.use(Vuex); @@ -15,5 +16,6 @@ export default new Vuex.Store({ getters, modules: { commit: commitModule, + pipelines, }, }); diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index b85246b2502..cd25c3060f2 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -204,17 +204,23 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo dispatch('updateViewer', 'editor', { root: true }); router.push( - `/project/${rootState.currentProjectId}/blob/${getters.branchName}/${ + `/project/${rootState.currentProjectId}/blob/${getters.branchName}/-/${ rootGetters.activeFile.path }`, ); } }) .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH)) - .then(() => dispatch('refreshLastCommitData', { - projectId: rootState.currentProjectId, - branchId: rootState.currentBranchId, - }, { root: true })); + .then(() => + dispatch( + 'refreshLastCommitData', + { + projectId: rootState.currentProjectId, + branchId: rootState.currentBranchId, + }, + { root: true }, + ), + ); }) .catch(err => { let errMsg = __('Error committing changes. Please try again.'); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js new file mode 100644 index 00000000000..07f7b201f2e --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js @@ -0,0 +1,49 @@ +import { __ } from '../../../../locale'; +import Api from '../../../../api'; +import flash from '../../../../flash'; +import * as types from './mutation_types'; + +export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE); +export const receiveLatestPipelineError = ({ commit }) => { + flash(__('There was an error loading latest pipeline')); + commit(types.RECEIVE_LASTEST_PIPELINE_ERROR); +}; +export const receiveLatestPipelineSuccess = ({ commit }, pipeline) => + commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, pipeline); + +export const fetchLatestPipeline = ({ dispatch, rootState }, sha) => { + dispatch('requestLatestPipeline'); + + return Api.pipelines(rootState.currentProjectId, { sha, per_page: '1' }) + .then(({ data }) => { + dispatch('receiveLatestPipelineSuccess', data.pop()); + }) + .catch(() => dispatch('receiveLatestPipelineError')); +}; + +export const requestJobs = ({ commit }) => commit(types.REQUEST_JOBS); +export const receiveJobsError = ({ commit }) => { + flash(__('There was an error loading jobs')); + commit(types.RECEIVE_JOBS_ERROR); +}; +export const receiveJobsSuccess = ({ commit }, data) => commit(types.RECEIVE_JOBS_SUCCESS, data); + +export const fetchJobs = ({ dispatch, state, rootState }, page = '1') => { + dispatch('requestJobs'); + + Api.pipelineJobs(rootState.currentProjectId, state.latestPipeline.id, { + page, + }) + .then(({ data, headers }) => { + const nextPage = headers && headers['x-next-page']; + + dispatch('receiveJobsSuccess', data); + + if (nextPage) { + dispatch('fetchJobs', nextPage); + } + }) + .catch(() => dispatch('receiveJobsError')); +}; + +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js new file mode 100644 index 00000000000..d6c91f5b64d --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js @@ -0,0 +1,7 @@ +export const hasLatestPipeline = state => !state.isLoadingPipeline && !!state.latestPipeline; + +export const failedJobs = state => + state.stages.reduce( + (acc, stage) => acc.concat(stage.jobs.filter(job => job.status === 'failed')), + [], + ); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/index.js b/app/assets/javascripts/ide/stores/modules/pipelines/index.js new file mode 100644 index 00000000000..b44c3141b81 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pipelines/index.js @@ -0,0 +1,12 @@ +import state from './state'; +import * as actions from './actions'; +import mutations from './mutations'; +import * as getters from './getters'; + +export default { + namespaced: true, + state: state(), + actions, + mutations, + getters, +}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js new file mode 100644 index 00000000000..6b5701670a6 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js @@ -0,0 +1,7 @@ +export const REQUEST_LATEST_PIPELINE = 'REQUEST_LATEST_PIPELINE'; +export const RECEIVE_LASTEST_PIPELINE_ERROR = 'RECEIVE_LASTEST_PIPELINE_ERROR'; +export const RECEIVE_LASTEST_PIPELINE_SUCCESS = 'RECEIVE_LASTEST_PIPELINE_SUCCESS'; + +export const REQUEST_JOBS = 'REQUEST_JOBS'; +export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR'; +export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS'; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js new file mode 100644 index 00000000000..2b16e57b386 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js @@ -0,0 +1,53 @@ +/* eslint-disable no-param-reassign */ +import * as types from './mutation_types'; + +export default { + [types.REQUEST_LATEST_PIPELINE](state) { + state.isLoadingPipeline = true; + }, + [types.RECEIVE_LASTEST_PIPELINE_ERROR](state) { + state.isLoadingPipeline = false; + }, + [types.RECEIVE_LASTEST_PIPELINE_SUCCESS](state, pipeline) { + state.isLoadingPipeline = false; + + if (pipeline) { + state.latestPipeline = { + id: pipeline.id, + status: pipeline.status, + }; + } + }, + [types.REQUEST_JOBS](state) { + state.isLoadingJobs = true; + }, + [types.RECEIVE_JOBS_ERROR](state) { + state.isLoadingJobs = false; + }, + [types.RECEIVE_JOBS_SUCCESS](state, jobs) { + state.isLoadingJobs = false; + + state.stages = jobs.reduce((acc, job) => { + let stage = acc.find(s => s.title === job.stage); + + if (!stage) { + stage = { + title: job.stage, + jobs: [], + }; + + acc.push(stage); + } + + stage.jobs = stage.jobs.concat({ + id: job.id, + name: job.name, + status: job.status, + stage: job.stage, + duration: job.duration, + }); + + return acc; + }, state.stages); + }, +}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/state.js b/app/assets/javascripts/ide/stores/modules/pipelines/state.js new file mode 100644 index 00000000000..6f22542aaea --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pipelines/state.js @@ -0,0 +1,6 @@ +export default () => ({ + isLoadingPipeline: false, + isLoadingJobs: false, + latestPipeline: null, + stages: [], +}); diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index a3fb3232f1d..0a3f8d031c4 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -23,6 +23,7 @@ export const SET_BRANCH = 'SET_BRANCH'; export const SET_BRANCH_COMMIT = 'SET_BRANCH_COMMIT'; export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE'; export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN'; +export const SET_LAST_COMMIT_PIPELINE = 'SET_LAST_COMMIT_PIPELINE'; // Tree mutation types export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA'; diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js index e09f88878f4..f17ec4da308 100644 --- a/app/assets/javascripts/ide/stores/mutations/branch.js +++ b/app/assets/javascripts/ide/stores/mutations/branch.js @@ -14,6 +14,10 @@ export default { treeId: `${projectPath}/${branchName}`, active: true, workingReference: '', + commit: { + ...branch.commit, + pipeline: {}, + }, }, }, }); @@ -28,4 +32,9 @@ export default { commit, }); }, + [types.SET_LAST_COMMIT_PIPELINE](state, { projectId, branchId, pipeline }) { + Object.assign(state.projects[projectId].branches[branchId].commit, { + pipeline, + }); + }, }; diff --git a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js index d249b05f47c..0a1c253c637 100644 --- a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js +++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js @@ -26,7 +26,7 @@ self.addEventListener('message', e => { id: folderPath, name: folderName, path: folderPath, - url: `/${projectId}/tree/${branchId}/${folderPath}/`, + url: `/${projectId}/tree/${branchId}/-/${folderPath}/`, type: 'tree', parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`, tempFile, @@ -64,7 +64,7 @@ self.addEventListener('message', e => { id: path, name: blobName, path, - url: `/${projectId}/blob/${branchId}/${path}`, + url: `/${projectId}/blob/${branchId}/-/${path}`, type: 'blob', parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`, tempFile, diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue index a539506bce2..7ef5e679881 100644 --- a/app/assets/javascripts/issue_show/components/edit_actions.vue +++ b/app/assets/javascripts/issue_show/components/edit_actions.vue @@ -51,7 +51,7 @@ <template> <div class="prepend-top-default append-bottom-default clearfix"> <button - class="btn btn-save pull-left" + class="btn btn-save float-left" :class="{ disabled: formState.updateLoading || !isSubmitEnabled }" type="submit" :disabled="formState.updateLoading || !isSubmitEnabled" @@ -64,14 +64,14 @@ </i> </button> <button - class="btn btn-default pull-right" + class="btn btn-default float-right" type="button" @click="closeForm"> Cancel </button> <button v-if="shouldShowDeleteButton" - class="btn btn-danger pull-right append-right-default" + class="btn btn-danger float-right append-right-default" :class="{ disabled: deleteLoading }" type="button" :disabled="deleteLoading" diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue index 779705e19ac..ab8bd34762f 100644 --- a/app/assets/javascripts/issue_show/components/form.vue +++ b/app/assets/javascripts/issue_show/components/form.vue @@ -84,7 +84,7 @@ <div :class="{ 'col-sm-8 col-lg-9': hasIssuableTemplates, - 'col-xs-12': !hasIssuableTemplates, + 'col-12': !hasIssuableTemplates, }" > <title-field diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js index ace45e9dd29..c67bd7fb0c6 100644 --- a/app/assets/javascripts/job.js +++ b/app/assets/javascripts/job.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import _ from 'underscore'; +import StickyFill from 'stickyfilljs'; import axios from './lib/utils/axios_utils'; import { visitUrl } from './lib/utils/url_utility'; import bp from './breakpoints'; @@ -82,17 +83,11 @@ export default class Job { /** If the browser does not support position sticky, it returns the position as static. If the browser does support sticky, then we allow the browser to handle it, if not - then we default back to Bootstraps affix + then we use a polyfill **/ if (this.$topBar.css('position') !== 'static') return; - const offsetTop = this.$buildTrace.offset().top; - - this.$topBar.affix({ - offset: { - top: offsetTop, - }, - }); + StickyFill.add(this.$topBar); } // eslint-disable-next-line class-methods-use-this diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue index 21b545d6cab..c1044f4cd42 100644 --- a/app/assets/javascripts/jobs/components/header.vue +++ b/app/assets/javascripts/jobs/components/header.vue @@ -56,7 +56,7 @@ export default { actions.push({ label: 'New issue', path: this.job.new_issue_path, - cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block', + cssClass: 'js-new-issue btn btn-new btn-inverted d-none d-md-block d-lg-block d-xl-block', type: 'link', }); } diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue index dfe87d89a39..83560a8ff0e 100644 --- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue +++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue @@ -39,7 +39,7 @@ <span v-if="hasHelpURL" - class="help-button pull-right" + class="help-button float-right" > <a :href="helpUrl" diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue index db19dc9b238..82fec7e936b 100644 --- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue +++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue @@ -48,7 +48,7 @@ export default { return `${this.job.runner.description} (#${this.job.runner.id})`; }, retryButtonClass() { - let className = 'js-retry-button pull-right btn btn-retry visible-md-block visible-lg-block'; + let className = 'js-retry-button pull-right btn btn-retry d-none d-md-block d-lg-block d-xl-block'; className += this.job.status && this.job.recoverable ? ' btn-primary' @@ -105,7 +105,7 @@ export default { type="button" :aria-label="__('Toggle Sidebar')" class="btn btn-blank gutter-toggle pull-right - visible-xs-block visible-sm-block js-sidebar-build-toggle" + d-block d-sm-block d-md-none js-sidebar-build-toggle" > <i aria-hidden="true" diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js index e230dbbd4ac..6af22ecc990 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -35,7 +35,7 @@ export default class LabelManager { const $label = $(`#${$btn.data('domId')}`); const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`); - $tooltip.tooltip('destroy'); + $tooltip.tooltip('dispose'); _this.toggleLabelPriority($label, action); _this.toggleEmptyState($label, $btn, action); } diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 9b62cfb8206..eafdaf4a672 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -120,7 +120,7 @@ export default class LabelsSelect { $sidebarLabelTooltip .attr('title', labelTooltipTitle) - .tooltip('fixTitle'); + .tooltip('_fixTitle'); $('.has-tooltip', $value).tooltip({ container: 'body' diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 9ff2042475b..8b5445d012b 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -51,7 +51,7 @@ export const rstrip = (val) => { return val; }; -export const updateTooltipTitle = ($tooltipEl, newTitle) => $tooltipEl.attr('title', newTitle).tooltip('fixTitle'); +export const updateTooltipTitle = ($tooltipEl, newTitle) => $tooltipEl.attr('title', newTitle).tooltip('_fixTitle'); export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventName = 'input') => { const field = $(fieldSelector); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index c3d94d63c13..0ff23bbb061 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -2,10 +2,7 @@ import $ from 'jquery'; import timeago from 'timeago.js'; import dateFormat from 'vendor/date.format'; import { pluralize } from './text_utility'; -import { - languageCode, - s__, -} from '../../locale'; +import { languageCode, s__ } from '../../locale'; window.timeago = timeago; window.dateFormat = dateFormat; @@ -17,11 +14,37 @@ window.dateFormat = dateFormat; * * @param {Boolean} abbreviated */ -const getMonthNames = (abbreviated) => { +const getMonthNames = abbreviated => { if (abbreviated) { - return [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')]; + return [ + s__('Jan'), + s__('Feb'), + s__('Mar'), + s__('Apr'), + s__('May'), + s__('Jun'), + s__('Jul'), + s__('Aug'), + s__('Sep'), + s__('Oct'), + s__('Nov'), + s__('Dec'), + ]; } - return [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')]; + return [ + s__('January'), + s__('February'), + s__('March'), + s__('April'), + s__('May'), + s__('June'), + s__('July'), + s__('August'), + s__('September'), + s__('October'), + s__('November'), + s__('December'), + ]; }; /** @@ -29,7 +52,8 @@ const getMonthNames = (abbreviated) => { * @param {date} date * @returns {String} */ -export const getDayName = date => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()]; +export const getDayName = date => + ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()]; /** * @example @@ -55,7 +79,7 @@ export function getTimeago() { if (!timeagoInstance) { const localeRemaining = function getLocaleRemaining(number, index) { return [ - [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')], + [s__('Timeago|less than a minute ago'), s__('Timeago|right now')], [s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')], [s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')], [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')], @@ -73,7 +97,7 @@ export function getTimeago() { }; const locale = function getLocale(number, index) { return [ - [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')], + [s__('Timeago|less than a minute ago'), s__('Timeago|right now')], [s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')], [s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')], [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')], @@ -102,7 +126,7 @@ export function getTimeago() { * For the given element, renders a timeago instance. * @param {jQuery} $els */ -export const renderTimeago = ($els) => { +export const renderTimeago = $els => { const timeagoEls = $els || document.querySelectorAll('.js-timeago-render'); // timeago.js sets timeouts internally for each timeago value to be updated in real time @@ -119,7 +143,7 @@ export const localTimeAgo = ($timeagoEls, setTimeago = true) => { if (setTimeago) { // Recreate with custom template $(el).tooltip({ - template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>', + template: '<div class="tooltip local-timeago" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>', }); } @@ -141,7 +165,9 @@ export const timeFor = (time, expiredLabel) => { if (new Date(time) < new Date()) { return expiredLabel || s__('Timeago|Past due'); } - return getTimeago().format(time, `${timeagoLanguageCode}-remaining`).trim(); + return getTimeago() + .format(time, `${timeagoLanguageCode}-remaining`) + .trim(); }; export const getDayDifference = (a, b) => { @@ -161,7 +187,7 @@ export const getDayDifference = (a, b) => { export function timeIntervalInWords(intervalInSeconds) { const secondsInteger = parseInt(intervalInSeconds, 10); const minutes = Math.floor(secondsInteger / 60); - const seconds = secondsInteger - (minutes * 60); + const seconds = secondsInteger - minutes * 60; let text = ''; if (minutes >= 1) { @@ -178,8 +204,34 @@ export function dateInWords(date, abbreviated = false, hideYear = false) { const month = date.getMonth(); const year = date.getFullYear(); - const monthNames = [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')]; - const monthNamesAbbr = [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')]; + const monthNames = [ + s__('January'), + s__('February'), + s__('March'), + s__('April'), + s__('May'), + s__('June'), + s__('July'), + s__('August'), + s__('September'), + s__('October'), + s__('November'), + s__('December'), + ]; + const monthNamesAbbr = [ + s__('Jan'), + s__('Feb'), + s__('Mar'), + s__('Apr'), + s__('May'), + s__('Jun'), + s__('Jul'), + s__('Aug'), + s__('Sep'), + s__('Oct'), + s__('Nov'), + s__('Dec'), + ]; const monthName = abbreviated ? monthNamesAbbr[month] : monthNames[month]; @@ -210,7 +262,7 @@ export const monthInWords = (date, abbreviated = false) => { * * @param {Date} date */ -export const totalDaysInMonth = (date) => { +export const totalDaysInMonth = date => { if (!date) { return 0; } @@ -223,12 +275,20 @@ export const totalDaysInMonth = (date) => { * * @param {Date} date */ -export const getSundays = (date) => { +export const getSundays = date => { if (!date) { return []; } - const daysToSunday = ['Saturday', 'Friday', 'Thursday', 'Wednesday', 'Tuesday', 'Monday', 'Sunday']; + const daysToSunday = [ + 'Saturday', + 'Friday', + 'Thursday', + 'Wednesday', + 'Tuesday', + 'Monday', + 'Sunday', + ]; const month = date.getMonth(); const year = date.getFullYear(); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 247aeb481c6..9803bebfd10 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -46,9 +46,9 @@ document.addEventListener('beforeunload', () => { // Unbind scroll events $(document).off('scroll'); // Close any open tooltips - $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy'); + $('.has-tooltip, [data-toggle="tooltip"]').tooltip('dispose'); // Close any open popover - $('[data-toggle="popover"]').popover('destroy'); + $('[data-toggle="popover"]').popover('dispose'); }); window.addEventListener('hashchange', handleLocationHash); @@ -111,7 +111,7 @@ document.addEventListener('DOMContentLoaded', () => { $('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() { $(this) - .tooltip('destroy') + .tooltip('dispose') .closest('li') .fadeOut(); }); @@ -141,9 +141,9 @@ document.addEventListener('DOMContentLoaded', () => { }); // Initialize tooltips - $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover'; $body.tooltip({ selector: '.has-tooltip, [data-toggle="tooltip"]', + trigger: 'hover', placement(tip, el) { return $(el).data('placement') || 'bottom'; }, @@ -196,7 +196,7 @@ document.addEventListener('DOMContentLoaded', () => { $container.remove(); }); - $('.navbar-toggle').on('click', () => { + $('.navbar-toggler').on('click', () => { $('.header-content').toggleClass('menu-expanded'); gl.lazyLoader.loadCheck(); }); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js index db1d09eb2f2..70f185e3656 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js @@ -351,7 +351,7 @@ import Cookies from 'js-cookie'; }, getCommitButtonText() { - const initial = 'Commit conflict resolution'; + const initial = 'Commit to source branch'; const inProgress = 'Committing...'; return this.state ? this.state.isSubmitting ? inProgress : initial : initial; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 3f84f4b9499..3548c07aea8 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -362,7 +362,7 @@ export default class MergeRequestTabs { // // status - Boolean, true to show, false to hide toggleLoading(status) { - $('.mr-loading-status .loading').toggle(status); + $('.mr-loading-status .loading').toggleClass('hidden', status); } diffViewType() { diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue index a6dbe42a8f0..241627f9790 100644 --- a/app/assets/javascripts/monitoring/components/graph_group.vue +++ b/app/assets/javascripts/monitoring/components/graph_group.vue @@ -17,12 +17,12 @@ export default { <template> <div v-if="showPanels" - class="panel panel-default prometheus-panel" + class="card prometheus-panel" > - <div class="panel-heading"> + <div class="card-header"> <h4>{{ name }}</h4> </div> - <div class="panel-body prometheus-graph-group"> + <div class="card-body prometheus-graph-group"> <slot></slot> </div> </div> diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 96f2b3eac98..b2c1a26bbae 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1231,8 +1231,8 @@ export default class Notes { const isForced = forceShow === true || forceShow === false; const showNow = forceShow === true || (!isCurrentlyShown && !isForced); - targetRow.toggle(showNow); - notesContent.toggle(showNow); + targetRow.toggleClass('hide', !showNow); + notesContent.toggleClass('hide', !showNow); } if (addForm) { @@ -1675,7 +1675,7 @@ export default class Notes { <div class="note-header"> <div class="note-header-info"> <a href="/${_.escape(currentUsername)}"> - <span class="hidden-xs">${_.escape( + <span class="d-none d-sm-block">${_.escape( currentUsername, )}</span> <span class="note-headline-light">${_.escape( @@ -1694,7 +1694,7 @@ export default class Notes { </li>`, ); - $tempNote.find('.hidden-xs').text(_.escape(currentUserFullname)); + $tempNote.find('.d-none.d-sm-block').text(_.escape(currentUserFullname)); $tempNote .find('.note-headline-light') .text(`@${_.escape(currentUsername)}`); diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 48642c4a086..72d7e22fba0 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -321,7 +321,7 @@ Please check your network connection and try again.`; <li class="timeline-entry"> <div class="timeline-entry-inner"> <div class="flash-container error-alert timeline-content"></div> - <div class="timeline-icon hidden-xs hidden-sm"> + <div class="timeline-icon d-none d-sm-none d-md-block"> <user-avatar-link v-if="author" :link-href="author.path" @@ -369,7 +369,7 @@ js-gfm-input js-autosize markdown-area js-vue-textarea" </markdown-field> <div class="note-form-actions"> <div - class="pull-left btn-group + class="float-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> <button @click.prevent="handleSave()" @@ -383,6 +383,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" name="button" type="button" class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle" + data-display="static" data-toggle="dropdown" aria-label="Open comment type dropdown"> <i diff --git a/app/assets/javascripts/notes/components/note_edited_text.vue b/app/assets/javascripts/notes/components/note_edited_text.vue index 4ddca918495..2dc39d1a186 100644 --- a/app/assets/javascripts/notes/components/note_edited_text.vue +++ b/app/assets/javascripts/notes/components/note_edited_text.vue @@ -32,17 +32,17 @@ export default { <template> <div :class="className"> {{ actionText }} - <time-ago-tooltip - :time="editedAt" - tooltip-placement="bottom" - /> <template v-if="editedBy"> - by + {{ s__('ByAuthor|by') }} <a :href="editedBy.path" class="js-vue-author author_link"> {{ editedBy.name }} </a> </template> + <time-ago-tooltip + :time="editedAt" + tooltip-placement="bottom" + /> </div> </template> diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index c3d1ef1fcc6..a4081957207 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -62,6 +62,21 @@ export default { <template> <div class="note-header-info"> + <div + v-if="includeToggle" + class="discussion-actions"> + <button + @click="handleToggle" + class="note-action-button discussion-toggle-button js-vue-toggle-button" + type="button"> + <i + :class="toggleChevronClass" + class="fa" + aria-hidden="true"> + </i> + {{ __('Toggle discussion') }} + </button> + </div> <a :href="author.path"> <span class="note-header-author-name">{{ author.name }}</span> <span class="note-headline-light"> @@ -78,10 +93,13 @@ export default { v-html="actionTextHtml" class="system-note-message"> </span> + <span class="system-note-separator"> + · + </span> <a :href="noteTimestampLink" @click="updateTargetNoteHash" - class="note-timestamp"> + class="note-timestamp system-note-separator"> <time-ago-tooltip :time="createdAt" tooltip-placement="bottom" @@ -95,20 +113,5 @@ export default { </i> </span> </span> - <div - v-if="includeToggle" - class="discussion-actions"> - <button - @click="handleToggle" - class="note-action-button discussion-toggle-button js-vue-toggle-button" - type="button"> - <i - :class="toggleChevronClass" - class="fa" - aria-hidden="true"> - </i> - Toggle discussion - </button> - </div> </div> </template> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index e0f883a8e08..7f5aacaa3a2 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -100,7 +100,7 @@ export default { : 'div'; }, wrapperClass() { - return this.isDiffDiscussion ? '' : 'panel panel-default'; + return this.isDiffDiscussion ? '' : 'card'; }, }, mounted() { @@ -263,7 +263,7 @@ Please check your network connection and try again.`; class="discussion-reply-holder"> <template v-if="!isReplying && canReply"> <div - class="btn-group-justified discussion-with-resolve-btn" + class="btn-group d-flex discussion-with-resolve-btn" role="group"> <div class="btn-group" diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js index 9e6cf67dff0..00e27ca0e70 100644 --- a/app/assets/javascripts/notifications_form.js +++ b/app/assets/javascripts/notifications_form.js @@ -15,7 +15,7 @@ export default class NotificationsForm { toggleCheckbox(e) { const $checkbox = $(e.currentTarget); - const $parent = $checkbox.closest('.checkbox'); + const $parent = $checkbox.closest('.form-check'); this.saveEvent($checkbox, $parent); } diff --git a/app/assets/javascripts/pages/admin/admin.js b/app/assets/javascripts/pages/admin/admin.js index 91f154b7ecd..ff4d6ab15f9 100644 --- a/app/assets/javascripts/pages/admin/admin.js +++ b/app/assets/javascripts/pages/admin/admin.js @@ -25,7 +25,7 @@ export default function adminInit() { $('body').on('click', '.js-toggle-colors-link', (e) => { e.preventDefault(); - $('.js-toggle-colors-container').toggle(); + $('.js-toggle-colors-container').toggleClass('hide'); }); $('.log-tabs a').on('click', function logTabsClick(e) { diff --git a/app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js b/app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js new file mode 100644 index 00000000000..edd7e38471b --- /dev/null +++ b/app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js @@ -0,0 +1,3 @@ +import initU2F from '../../../shared/sessions/u2f'; + +document.addEventListener('DOMContentLoaded', initU2F); diff --git a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js index fbdef329ab2..8e8f47c21d8 100644 --- a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js +++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js @@ -5,7 +5,7 @@ document.addEventListener('DOMContentLoaded', () => { const twoFactorNode = document.querySelector('.js-two-factor-auth'); const skippable = twoFactorNode.dataset.twoFactorSkippable === 'true'; if (skippable) { - const button = `<a class="btn btn-xs btn-warning pull-right" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`; + const button = `<a class="btn btn-sm btn-warning float-right" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`; const flashAlert = document.querySelector('.flash-alert .container-fluid'); if (flashAlert) flashAlert.insertAdjacentHTML('beforeend', button); } diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js index a99ce0f1c36..5316d3e9f3c 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */ +/* eslint-disable func-names, space-before-function-paren, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */ import $ from 'jquery'; import _ from 'underscore'; @@ -13,17 +13,17 @@ import { dateTickFormat } from '~/lib/utils/tick_formats'; const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse }; -const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; const hasProp = {}.hasOwnProperty; +const extend = function(child, parent) { for (const key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; export const ContributorsGraph = (function() { function ContributorsGraph() {} ContributorsGraph.prototype.MARGIN = { top: 20, - right: 20, + right: 10, bottom: 30, - left: 50 + left: 40 }; ContributorsGraph.prototype.x_domain = null; @@ -32,6 +32,12 @@ export const ContributorsGraph = (function() { ContributorsGraph.prototype.dates = []; + ContributorsGraph.prototype.determine_width = function(baseWidth, $parentElement) { + const parentPaddingWidth = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right')); + const marginWidth = this.MARGIN.left + this.MARGIN.right; + return baseWidth - parentPaddingWidth - marginWidth; + }; + ContributorsGraph.set_x_domain = function(data) { return ContributorsGraph.prototype.x_domain = data; }; @@ -105,11 +111,10 @@ export const ContributorsMasterGraph = (function(superClass) { function ContributorsMasterGraph(data1) { const $parentElement = $('#contributors-master'); - const parentPadding = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right')); this.data = data1; this.update_content = this.update_content.bind(this); - this.width = $('.content').width() - parentPadding - (this.MARGIN.left + this.MARGIN.right); + this.width = this.determine_width($('.js-graphs-show').width(), $parentElement); this.height = 200; this.x = null; this.y = null; @@ -122,8 +127,7 @@ export const ContributorsMasterGraph = (function(superClass) { } ContributorsMasterGraph.prototype.process_dates = function(data) { - var dates; - dates = this.get_dates(data); + const dates = this.get_dates(data); this.parse_dates(data); return ContributorsGraph.set_dates(dates); }; @@ -133,8 +137,7 @@ export const ContributorsMasterGraph = (function(superClass) { }; ContributorsMasterGraph.prototype.parse_dates = function(data) { - var parseDate; - parseDate = d3.timeParse("%Y-%m-%d"); + const parseDate = d3.timeParse("%Y-%m-%d"); return data.forEach(function(d) { return d.date = parseDate(d.date); }); @@ -152,7 +155,14 @@ export const ContributorsMasterGraph = (function(superClass) { }; ContributorsMasterGraph.prototype.create_svg = function() { - return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + this.svg = d3.select("#contributors-master") + .append("svg") + .attr("width", this.width + this.MARGIN.left + this.MARGIN.right) + .attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom) + .attr("class", "tint-box") + .append("g") + .attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + return this.svg; }; ContributorsMasterGraph.prototype.create_area = function(x, y) { @@ -218,12 +228,14 @@ export const ContributorsAuthorGraph = (function(superClass) { extend(ContributorsAuthorGraph, superClass); function ContributorsAuthorGraph(data1) { + const $parentElements = $('.person'); + this.data = data1; // Don't split graph size in half for mobile devices. - if ($(window).width() < 768) { - this.width = $('.content').width() - 80; + if ($(window).width() < 790) { + this.width = this.determine_width($('.js-graphs-show').width(), $parentElements); } else { - this.width = ($('.content').width() / 2) - 100; + this.width = this.determine_width($('.js-graphs-show').width() / 2, $parentElements); } this.height = 200; this.x = null; @@ -249,8 +261,7 @@ export const ContributorsAuthorGraph = (function(superClass) { ContributorsAuthorGraph.prototype.create_area = function(x, y) { return this.area = d3.area().x(function(d) { - var parseDate; - parseDate = d3.timeParse("%Y-%m-%d"); + const parseDate = d3.timeParse("%Y-%m-%d"); return x(parseDate(d)); }).y0(this.height).y1((function(_this) { return function(d) { @@ -264,9 +275,16 @@ export const ContributorsAuthorGraph = (function(superClass) { }; ContributorsAuthorGraph.prototype.create_svg = function() { - var persons = document.querySelectorAll('.person'); + const persons = document.querySelectorAll('.person'); this.list_item = persons[persons.length - 1]; - return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + this.svg = d3.select(this.list_item) + .append("svg") + .attr("width", this.width + this.MARGIN.left + this.MARGIN.right) + .attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom) + .attr("class", "spark") + .append("g") + .attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); + return this.svg; }; ContributorsAuthorGraph.prototype.draw_path = function(data) { diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue index 25a88f846eb..17b91479ea5 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue @@ -42,7 +42,7 @@ </label> <span v-if="helpText" - class="help-block" + class="form-text text-muted" > {{ helpText }} </span> diff --git a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue new file mode 100644 index 00000000000..df21e2f8771 --- /dev/null +++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue @@ -0,0 +1,77 @@ +<script> +import _ from 'underscore'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; +import { s__, sprintf } from '~/locale'; + +export default { + components: { + GlModal, + }, + props: { + deleteWikiUrl: { + type: String, + required: true, + default: '', + }, + pageTitle: { + type: String, + required: true, + default: '', + }, + csrfToken: { + type: String, + required: true, + default: '', + }, + }, + computed: { + message() { + return s__('WikiPageConfirmDelete|Are you sure you want to delete this page?'); + }, + title() { + return sprintf( + s__('WikiPageConfirmDelete|Delete page %{pageTitle}?'), + { + pageTitle: _.escape(this.pageTitle), + }, + false, + ); + }, + }, + methods: { + onSubmit() { + this.$refs.form.submit(); + }, + }, +}; +</script> + +<template> + <gl-modal + id="delete-wiki-modal" + :header-title-text="title" + footer-primary-button-variant="danger" + :footer-primary-button-text="s__('WikiPageConfirmDelete|Delete page')" + @submit="onSubmit" + > + {{ message }} + <form + ref="form" + :action="deleteWikiUrl" + method="post" + class="form-horizontal js-requires-input" + > + <input + ref="method" + type="hidden" + name="_method" + value="delete" + /> + <input + type="hidden" + name="authenticity_token" + :value="csrfToken" + /> + </form> + </gl-modal> +</template> diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js index ec01c66ffda..0295653cb29 100644 --- a/app/assets/javascripts/pages/projects/wikis/index.js +++ b/app/assets/javascripts/pages/projects/wikis/index.js @@ -1,12 +1,40 @@ import $ from 'jquery'; +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; +import csrf from '~/lib/utils/csrf'; import Wikis from './wikis'; import ShortcutsWiki from '../../../shortcuts_wiki'; import ZenMode from '../../../zen_mode'; import GLForm from '../../../gl_form'; +import deleteWikiModal from './components/delete_wiki_modal.vue'; document.addEventListener('DOMContentLoaded', () => { new Wikis(); // eslint-disable-line no-new new ShortcutsWiki(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new new GLForm($('.wiki-form'), true); // eslint-disable-line no-new + + const deleteWikiButton = document.getElementById('delete-wiki-button'); + + if (deleteWikiButton) { + Vue.use(Translate); + + const { deleteWikiUrl, pageTitle } = deleteWikiButton.dataset; + const deleteWikiModalEl = document.getElementById('delete-wiki-modal'); + const deleteModal = new Vue({ // eslint-disable-line + el: deleteWikiModalEl, + data: { + deleteWikiUrl: '', + }, + render(createElement) { + return createElement(deleteWikiModal, { + props: { + pageTitle, + deleteWikiUrl, + csrfToken: csrf.token, + }, + }); + }, + }); + } }); diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js index 124bc2ba710..ca375007ec5 100644 --- a/app/assets/javascripts/pages/users/user_tabs.js +++ b/app/assets/javascripts/pages/users/user_tabs.js @@ -181,7 +181,7 @@ export default class UserTabs { toggleLoading(status) { return this.$parentEl.find('.loading-status .loading') - .toggle(status); + .toggleClass('hidden', status); } setCurrentAction(source) { @@ -195,6 +195,6 @@ export default class UserTabs { } getCurrentAction() { - return this.$parentEl.find('.nav-links .active a').data('action'); + return this.$parentEl.find('.nav-links a.active').data('action'); } } diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue index 3ed07a4a47d..dd9578a6c7f 100644 --- a/app/assets/javascripts/performance_bar/components/request_selector.vue +++ b/app/assets/javascripts/performance_bar/components/request_selector.vue @@ -37,7 +37,7 @@ export default { <template> <div id="peek-request-selector" - class="pull-right" + class="float-right" > <select v-model="currentRequestId"> <option diff --git a/app/assets/javascripts/pipelines/components/blank_state.vue b/app/assets/javascripts/pipelines/components/blank_state.vue index 8d3d6223d7b..f3219b8291c 100644 --- a/app/assets/javascripts/pipelines/components/blank_state.vue +++ b/app/assets/javascripts/pipelines/components/blank_state.vue @@ -17,13 +17,13 @@ <template> <div class="row empty-state"> - <div class="col-xs-12"> + <div class="col-12"> <div class="svg-content"> <img :src="svgPath" /> </div> </div> - <div class="col-xs-12 text-center"> + <div class="col-12 text-center"> <div class="text-content"> <h4>{{ message }}</h4> </div> diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue index 10ac8c08bed..50c27bed9fd 100644 --- a/app/assets/javascripts/pipelines/components/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/empty_state.vue @@ -19,13 +19,13 @@ </script> <template> <div class="row empty-state js-empty-state"> - <div class="col-xs-12"> + <div class="col-12"> <div class="svg-content svg-250"> <img :src="emptyStateSvgPath" /> </div> </div> - <div class="col-xs-12"> + <div class="col-12"> <div class="text-content"> <template v-if="canSetCi"> @@ -34,7 +34,7 @@ </h4> <p> - {{ s__(`Pipelines|Continous Integration can help + {{ s__(`Pipelines|Continuous Integration can help catch bugs by running your tests automatically, while Continuous Deployment can help you deliver code to your product environment.`) }} diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index fd3491c7fe0..82b4ce083fb 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -1,11 +1,21 @@ <script> import $ from 'jquery'; -import tooltip from '../../../vue_shared/directives/tooltip'; -import Icon from '../../../vue_shared/components/icon.vue'; -import { dasherize } from '../../../lib/utils/text_utility'; -import eventHub from '../../event_hub'; +import axios from '~/lib/utils/axios_utils'; +import { dasherize } from '~/lib/utils/text_utility'; +import { __ } from '~/locale'; +import createFlash from '~/flash'; +import tooltip from '~/vue_shared/directives/tooltip'; +import Icon from '~/vue_shared/components/icon.vue'; + /** - * Renders either a cancel, retry or play icon pointing to the given path. + * Renders either a cancel, retry or play icon button and handles the post request + * + * Used in: + * - mr widget mini pipeline graph: `mr_widget_pipeline.vue` + * - pipelines table + * - pipelines table in merge request page + * - pipelines table in commit page + * - pipelines detail page in big graph */ export default { components: { @@ -32,16 +42,10 @@ export default { required: true, }, - requestFinishedFor: { - type: String, - required: false, - default: '', - }, }, data() { return { isDisabled: false, - linkRequested: '', }; }, @@ -51,19 +55,28 @@ export default { return `${actionIconDash} js-icon-${actionIconDash}`; }, }, - watch: { - requestFinishedFor() { - if (this.requestFinishedFor === this.linkRequested) { - this.isDisabled = false; - } - }, - }, methods: { + /** + * The request should not be handled here. + * However due to this component being used in several + * different apps it avoids repetition & complexity. + * + */ onClickAction() { $(this.$el).tooltip('hide'); - eventHub.$emit('postAction', this.link); - this.linkRequested = this.link; + this.isDisabled = true; + + axios.post(`${this.link}.json`) + .then(() => { + this.isDisabled = false; + this.$emit('pipelineActionRequestComplete'); + }) + .catch(() => { + this.isDisabled = false; + + createFlash(__('An error occurred while making the request.')); + }); }, }, }; @@ -80,6 +93,6 @@ btn-transparent ci-action-icon-container ci-action-icon-wrapper" data-container="body" :disabled="isDisabled" > - <icon :name="actionIcon" /> + <icon :name="actionIcon"/> </button> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue index 4027d26098f..7bfe11ab8cd 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -42,11 +42,6 @@ export default { type: Object, required: true, }, - requestFinishedFor: { - type: String, - required: false, - default: '', - }, }, computed: { @@ -76,11 +71,15 @@ export default { e.stopPropagation(); }); }, + + pipelineActionRequestComplete() { + this.$emit('pipelineActionRequestComplete'); + }, }, }; </script> <template> - <div class="ci-job-dropdown-container"> + <div class="ci-job-dropdown-container dropdown"> <button v-tooltip type="button" @@ -110,7 +109,7 @@ export default { <job-component :job="item" css-class-job-name="mini-pipeline-graph-dropdown-item" - :request-finished-for="requestFinishedFor" + @pipelineActionRequestComplete="pipelineActionRequestComplete" /> </li> </ul> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 7b8a5edcbff..4ec67f6c01b 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -16,11 +16,6 @@ export default { type: Object, required: true, }, - requestFinishedFor: { - type: String, - required: false, - default: '', - }, }, computed: { @@ -51,6 +46,10 @@ export default { return className; }, + + refreshPipelineGraph() { + this.$emit('refreshPipelineGraph'); + }, }, }; </script> @@ -74,7 +73,7 @@ export default { :key="stage.name" :stage-connector-class="stageConnectorClass(index, stage)" :is-first-column="isFirstColumn(index)" - :request-finished-for="requestFinishedFor" + @refreshPipelineGraph="refreshPipelineGraph" /> </ul> </div> diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index c1f0f051b63..dc16d395bcb 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -46,11 +46,6 @@ export default { required: false, default: '', }, - requestFinishedFor: { - type: String, - required: false, - default: '', - }, }, computed: { status() { @@ -84,6 +79,11 @@ export default { return this.job.status && this.job.status.action && this.job.status.action.path; }, }, + methods: { + pipelineActionRequestComplete() { + this.$emit('pipelineActionRequestComplete'); + }, + }, }; </script> <template> @@ -96,6 +96,7 @@ export default { :class="cssClassJobName" data-container="body" data-html="true" + data-boundary="viewport" class="js-pipeline-graph-job-link" > @@ -126,7 +127,7 @@ export default { :tooltip-text="status.action.title" :link="status.action.path" :action-icon="status.action.icon" - :request-finished-for="requestFinishedFor" + @pipelineActionRequestComplete="pipelineActionRequestComplete" /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index 5461fdbbadd..f32368947e8 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -29,12 +29,6 @@ export default { required: false, default: '', }, - - requestFinishedFor: { - type: String, - required: false, - default: '', - }, }, methods: { @@ -49,6 +43,10 @@ export default { buildConnnectorClass(index) { return index === 0 && !this.isFirstColumn ? 'left-connector' : ''; }, + + pipelineActionRequestComplete() { + this.$emit('refreshPipelineGraph'); + }, }, }; </script> @@ -75,12 +73,13 @@ export default { v-if="job.size === 1" :job="job" css-class-job-name="build-content" + @pipelineActionRequestComplete="pipelineActionRequestComplete" /> <dropdown-job-component v-if="job.size > 1" :job="job" - :request-finished-for="requestFinishedFor" + @pipelineActionRequestComplete="pipelineActionRequestComplete" /> </li> diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index ceb4d9ca604..4d965733f95 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -46,7 +46,7 @@ }; </script> <template> - <div class="table-section section-15 hidden-xs hidden-sm pipeline-tags"> + <div class="table-section section-15 d-none d-sm-none d-md-block pipeline-tags"> <a :href="pipeline.path" class="js-pipeline-url-link"> @@ -69,35 +69,35 @@ <span v-if="pipeline.flags.latest" v-tooltip - class="js-pipeline-url-latest label label-success" + class="js-pipeline-url-latest badge badge-success" title="Latest pipeline for this branch"> latest </span> <span v-if="pipeline.flags.yaml_errors" v-tooltip - class="js-pipeline-url-yaml label label-danger" + class="js-pipeline-url-yaml badge badge-danger" :title="pipeline.yaml_errors"> yaml invalid </span> <span v-if="pipeline.flags.failure_reason" v-tooltip - class="js-pipeline-url-failure label label-danger" + class="js-pipeline-url-failure badge badge-danger" :title="pipeline.failure_reason"> error </span> <a v-if="pipeline.flags.auto_devops" tabindex="0" - class="js-pipeline-url-autodevops label label-info autodevops-badge" + class="js-pipeline-url-autodevops badge badge-info autodevops-badge" v-popover="popoverOptions" role="button"> Auto DevOps </a> <span v-if="pipeline.flags.stuck" - class="js-pipeline-url-stuck label label-warning"> + class="js-pipeline-url-stuck badge badge-warning"> stuck </span> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index 3297af7bde4..e9bc3cf14ca 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -63,7 +63,7 @@ <loading-icon v-if="isLoading" /> </button> - <ul class="dropdown-menu dropdown-menu-align-right"> + <ul class="dropdown-menu dropdown-menu-right"> <li v-for="(action, i) in actions" :key="i" diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue index 1b9e0f917a4..31fcc9dd412 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue @@ -37,7 +37,7 @@ > </i> </button> - <ul class="dropdown-menu dropdown-menu-align-right"> + <ul class="dropdown-menu dropdown-menu-right"> <li v-for="(artifact, i) in artifacts" :key="i"> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 41986b827cd..4318abe97e0 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -37,6 +37,7 @@ return { pipelineId: '', endpoint: '', + cancelingPipeline: null, }; }, computed: { @@ -64,6 +65,7 @@ }, onSubmit() { eventHub.$emit('postAction', this.endpoint); + this.cancelingPipeline = this.pipelineId; }, }, }; @@ -106,6 +108,7 @@ :update-graph-dropdown="updateGraphDropdown" :auto-devops-help-path="autoDevopsHelpPath" :view-type="viewType" + :canceling-pipeline="cancelingPipeline" /> <modal diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 498a97851fa..a3c17479e6f 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -9,6 +9,7 @@ import CommitComponent from '../../vue_shared/components/commit.vue'; import LoadingButton from '../../vue_shared/components/loading_button.vue'; import Icon from '../../vue_shared/components/icon.vue'; + import { PIPELINES_TABLE } from '../constants'; /** * Pipeline table row. @@ -45,11 +46,16 @@ type: String, required: true, }, + cancelingPipeline: { + type: String, + required: false, + default: null, + }, }, + pipelinesTable: PIPELINES_TABLE, data() { return { isRetrying: false, - isCancelling: false, }; }, computed: { @@ -225,12 +231,14 @@ isChildView() { return this.viewType === 'child'; }, + + isCancelling() { + return this.cancelingPipeline === this.pipeline.id; + }, }, methods: { handleCancelClick() { - this.isCancelling = true; - eventHub.$emit('openConfirmationModal', { pipelineId: this.pipeline.id, endpoint: this.pipeline.cancel_path, @@ -297,6 +305,7 @@ v-for="(stage, index) in pipeline.details.stages" :key="index"> <pipeline-stage + :type="$options.pipelinesTable" :stage="stage" :update-dropdown="updateGraphDropdown" /> @@ -322,7 +331,7 @@ <pipelines-artifacts-component v-if="pipeline.details.artifacts.length" - class="hidden-xs hidden-sm" + class="d-none d-sm-none d-md-block" :artifacts="pipeline.details.artifacts" /> diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index a65485c05eb..f9769815796 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -21,6 +21,7 @@ import Icon from '../../vue_shared/components/icon.vue'; import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; import JobComponent from './graph/job_component.vue'; import tooltip from '../../vue_shared/directives/tooltip'; +import { PIPELINES_TABLE } from '../constants'; export default { components: { @@ -44,6 +45,12 @@ export default { required: false, default: false, }, + + type: { + type: String, + required: false, + default: '', + }, }, data() { @@ -133,6 +140,16 @@ export default { isDropdownOpen() { return this.$el.classList.contains('open'); }, + + pipelineActionRequestComplete() { + if (this.type === PIPELINES_TABLE) { + // warn the table to update + eventHub.$emit('refreshPipelinesTable'); + } else { + // close the dropdown in mr widget + $(this.$refs.dropdown).dropdown('toggle'); + } + }, }, }; </script> @@ -151,6 +168,7 @@ export default { id="stageDropdown" aria-haspopup="true" aria-expanded="false" + ref="dropdown" > <span @@ -188,6 +206,7 @@ export default { <job-component :job="job" css-class-job-name="mini-pipeline-graph-dropdown-item" + @pipelineActionRequestComplete="pipelineActionRequestComplete" /> </li> </ul> diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue index cd54d26c9d3..79dbdca4010 100644 --- a/app/assets/javascripts/pipelines/components/time_ago.vue +++ b/app/assets/javascripts/pipelines/components/time_ago.vue @@ -75,7 +75,7 @@ </p> <p - class="finished-at hidden-xs hidden-sm" + class="finished-at d-none d-sm-none d-md-block" v-if="hasFinishedTime" > diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js index b384c7500e7..eaa11a84cb9 100644 --- a/app/assets/javascripts/pipelines/constants.js +++ b/app/assets/javascripts/pipelines/constants.js @@ -1,2 +1,2 @@ -// eslint-disable-next-line import/prefer-default-export export const CANCEL_REQUEST = 'CANCEL_REQUEST'; +export const PIPELINES_TABLE = 'PIPELINES_TABLE'; diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index de0faf181e5..30b1eee186d 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -55,11 +55,13 @@ export default { eventHub.$on('postAction', this.postAction); eventHub.$on('retryPipeline', this.postAction); eventHub.$on('clickedDropdown', this.updateTable); + eventHub.$on('refreshPipelinesTable', this.fetchPipelines); }, beforeDestroy() { eventHub.$off('postAction', this.postAction); eventHub.$off('retryPipeline', this.postAction); eventHub.$off('clickedDropdown', this.updateTable); + eventHub.$off('refreshPipelinesTable', this.fetchPipelines); }, destroyed() { this.poll.stop(); diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index 04fe7958fe6..b49a16a87e6 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -25,30 +25,14 @@ export default () => { data() { return { mediator, - requestFinishedFor: null, }; }, - created() { - eventHub.$on('postAction', this.postAction); - }, - beforeDestroy() { - eventHub.$off('postAction', this.postAction); - }, methods: { - postAction(action) { - // Click was made, reset this variable - this.requestFinishedFor = null; - - this.mediator.service - .postAction(action) - .then(() => { - this.mediator.refreshPipeline(); - this.requestFinishedFor = action; - }) - .catch(() => { - this.requestFinishedFor = action; - Flash(__('An error occurred while making the request.')); - }); + requestRefreshPipelineGraph() { + // When an action is clicked + // (wether in the dropdown or in the main nodes, we refresh the big graph) + this.mediator.refreshPipeline() + .catch(() => Flash(__('An error occurred while making the request.'))); }, }, render(createElement) { @@ -56,7 +40,9 @@ export default () => { props: { isLoading: this.mediator.state.isLoading, pipeline: this.mediator.store.state.pipeline, - requestFinishedFor: this.requestFinishedFor, + }, + on: { + refreshPipelineGraph: this.requestRefreshPipelineGraph, }, }); }, diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue index e5de3f69b01..a7a2a7235fd 100644 --- a/app/assets/javascripts/profile/account/components/update_username.vue +++ b/app/assets/javascripts/profile/account/components/update_username.vue @@ -86,7 +86,11 @@ Please update your Git repository remotes as soon as possible.`), <div class="form-group"> <label :for="$options.inputId">{{ s__('Profiles|Path') }}</label> <div class="input-group"> - <div class="input-group-addon">{{ rootUrl }}</div> + <div class="input-group-prepend"> + <div class="input-group-text"> + {{ rootUrl }} + </div> + </div> <input :id="$options.inputId" class="form-control" diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js index 6fedd94a6a9..f5cd1c3cc3e 100644 --- a/app/assets/javascripts/project_fork.js +++ b/app/assets/javascripts/project_fork.js @@ -4,6 +4,6 @@ export default () => { $('.js-fork-thumbnail').on('click', function forkThumbnailClicked() { if ($(this).hasClass('disabled')) return false; - return $('.js-fork-content').toggle(); + return $('.js-fork-content').toggleClass('hidden'); }); }; diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js index f31beb4dc78..6f06944ebb6 100644 --- a/app/assets/javascripts/project_label_subscription.js +++ b/app/assets/javascripts/project_label_subscription.js @@ -42,7 +42,7 @@ export default class ProjectLabelSubscription { const $button = $(button); if ($button.attr('data-original-title')) { - $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle'); + $button.tooltip('hide').attr('data-original-title', newAction).tooltip('_fixTitle'); } return button; diff --git a/app/assets/javascripts/project_visibility.js b/app/assets/javascripts/project_visibility.js index 7c95c71e239..a52ac768e57 100644 --- a/app/assets/javascripts/project_visibility.js +++ b/app/assets/javascripts/project_visibility.js @@ -7,7 +7,7 @@ function setVisibilityOptions(namespaceSelector) { const selectedNamespace = namespaceSelector.options[namespaceSelector.selectedIndex]; const { name, visibility, visibilityLevel, showPath, editPath } = selectedNamespace.dataset; - document.querySelectorAll('.visibility-level-setting .radio').forEach((option) => { + document.querySelectorAll('.visibility-level-setting .form-check').forEach((option) => { const optionInput = option.querySelector('input[type=radio]'); const optionValue = optionInput ? optionInput.value : 0; const optionTitle = option.querySelector('.option-title'); diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 93603dfc14d..888b1d6ce33 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -66,8 +66,8 @@ const bindEvents = () => { .on('click', (e) => { e.preventDefault(); }) .popover({ title: $pushNewProjectTipTrigger.data('title'), - placement: 'auto bottom', - html: 'true', + placement: 'bottom', + html: true, content: $('.push-new-project-tip-template').html(), }) .on('shown.bs.popover', () => { diff --git a/app/assets/javascripts/projects_dropdown/components/search.vue b/app/assets/javascripts/projects_dropdown/components/search.vue index 0c46ed184be..7fcd62dcdb8 100644 --- a/app/assets/javascripts/projects_dropdown/components/search.vue +++ b/app/assets/javascripts/projects_dropdown/components/search.vue @@ -46,7 +46,7 @@ <template> <div - class="search-input-container hidden-xs" + class="search-input-container d-none d-sm-block" > <input type="search" diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js index 0a60f4845b2..1a75fdd75db 100644 --- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js +++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js @@ -31,7 +31,7 @@ export default class PrometheusMetrics { /* eslint-disable class-methods-use-this */ handlePanelToggle(e) { const $toggleBtn = $(e.currentTarget); - const $currentPanelBody = $toggleBtn.closest('.panel').find('.panel-body'); + const $currentPanelBody = $toggleBtn.closest('.card').find('.card-body'); $currentPanelBody.toggleClass('hidden'); if ($toggleBtn.hasClass('fa-caret-down')) { $toggleBtn.removeClass('fa-caret-down').addClass('fa-caret-right'); diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index a03180e80e6..2fc3778820b 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -28,11 +28,6 @@ isOpen: false, }; }, - computed: { - clipboardText() { - return `docker pull ${this.repo.location}`; - }, - }, methods: { ...mapActions([ 'fetchRepos', @@ -84,12 +79,12 @@ <clipboard-button v-if="repo.location" - :text="clipboardText" + :text="repo.location" :title="repo.location" css-class="btn-default btn-transparent btn-clipboard" /> - <div class="controls hidden-xs pull-right"> + <div class="controls d-none d-sm-block float-right"> <button v-if="repo.canDelete" type="button" diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index a2227b2f554..e4a4b3bb129 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -56,10 +56,6 @@ .catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY)); }, - clipboardText(text) { - return `docker pull ${text}`; - }, - showError(message) { Flash(errorMessages[message]); }, @@ -89,7 +85,7 @@ <clipboard-button v-if="item.location" :title="item.location" - :text="clipboardText(item.location)" + :text="item.location" css-class="btn-default btn-transparent btn-clipboard" /> </td> @@ -124,7 +120,7 @@ <button v-if="item.canDelete" type="button" - class="js-delete-registry btn btn-danger hidden-xs pull-right" + class="js-delete-registry btn btn-danger d-none d-sm-block float-right" :title="s__('ContainerRegistry|Remove tag')" :aria-label="s__('ContainerRegistry|Remove tag')" data-container="body" diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 6eb0b62fa1c..2afcf4626b8 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -108,7 +108,7 @@ Sidebar.prototype.todoUpdateDone = function(data) { .attr('title', $el.data(`${attrPrefix}Text`)); if ($el.hasClass('has-tooltip')) { - $el.tooltip('fixTitle'); + $el.tooltip('_fixTitle'); } if ($el.data(`${attrPrefix}Icon`)) { diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index a4d10850471..78f7353eb0d 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -7,7 +7,7 @@ export default class ShortcutsNavigation extends Shortcuts { super(); Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project')); - Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity')); + Mousetrap.bind('g v', () => findAndFollowLink('.shortcuts-project-activity')); Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree')); Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits')); Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds')); @@ -16,9 +16,10 @@ export default class ShortcutsNavigation extends Shortcuts { Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues')); Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards')); Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests')); - Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos')); Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki')); Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets')); + Mousetrap.bind('g k', () => findAndFollowLink('.shortcuts-kubernetes')); + Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments')); Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue')); this.enabledHelp.push('.hidden-shortcut.project'); diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue index 5eeb2a41bae..284a258d3c9 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue @@ -41,7 +41,7 @@ export default { </i> <a v-if="editable" - class="js-sidebar-dropdown-toggle edit-link pull-right" + class="js-sidebar-dropdown-toggle edit-link float-right" href="#" > {{ __('Edit') }} @@ -49,7 +49,7 @@ export default { <a v-if="showToggle" aria-label="Toggle sidebar" - class="gutter-toggle pull-right js-sidebar-toggle" + class="gutter-toggle float-right js-sidebar-toggle" href="#" role="button" > diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue index 2d00e8ac7e0..5a374d84796 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue @@ -130,6 +130,7 @@ export default { v-tooltip data-container="body" data-placement="left" + data-boundary="viewport" :title="collapsedTooltipTitle" > <i diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index 7f0de722f61..3f6e2f05396 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -75,6 +75,7 @@ export default { v-tooltip data-container="body" data-placement="left" + data-boundary="viewport" :title="tooltipLabel" > <icon @@ -86,7 +87,7 @@ export default { {{ __('Confidentiality') }} <a v-if="isEditable" - class="pull-right confidential-edit" + class="float-right confidential-edit" href="#" @click.prevent="toggleForm" > diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue index 3783f71a848..4165aa19acf 100644 --- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue +++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue @@ -32,7 +32,7 @@ export default { </script> <template> - <div class="dropdown open"> + <div class="dropdown show"> <div class="dropdown-menu sidebar-item-warning-message"> <div> <p diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue index e1e4715826a..d392977e5e2 100644 --- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue +++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue @@ -41,7 +41,7 @@ export default { </script> <template> - <div class="dropdown open"> + <div class="dropdown show"> <div class="dropdown-menu sidebar-item-warning-message"> <p class="text" diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue index 1a5e7b67eca..fb69c741dcd 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -99,6 +99,7 @@ export default { v-tooltip data-container="body" data-placement="left" + data-boundary="viewport" :title="tooltipLabel" > <icon @@ -112,7 +113,7 @@ export default { {{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }} <button v-if="isEditable" - class="pull-right lock-edit" + class="float-right lock-edit" type="button" @click.prevent="toggleForm" > diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue index 8f9e6761d20..0a945fc7fd5 100644 --- a/app/assets/javascripts/sidebar/components/participants/participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/participants.vue @@ -84,6 +84,7 @@ v-tooltip data-container="body" data-placement="left" + data-boundary="viewport" :title="participantLabel" @click="onClickCollapsedIcon" > diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue index f0df759ef7a..6745c1aafff 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue @@ -82,6 +82,7 @@ :title="notificationTooltip" data-container="body" data-placement="left" + data-boundary="viewport" > <icon :name="notificationIcon" @@ -91,12 +92,12 @@ /> </span> </div> - <span class="issuable-header-text hide-collapsed pull-left"> + <span class="issuable-header-text hide-collapsed float-left"> {{ __('Notifications') }} </span> <toggle-button ref="toggleButton" - class="pull-right hide-collapsed js-issuable-subscribe-button" + class="float-right hide-collapsed js-issuable-subscribe-button" :is-loading="showLoadingState" :value="subscribed" @change="toggleSubscription" diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue index 9d9ee9dea4d..209af1ce152 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue @@ -114,6 +114,7 @@ v-tooltip data-container="body" data-placement="left" + data-boundary="viewport" :title="tooltipText" > <icon name="timer" /> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue index 82c4562f9a9..6f79310b1cc 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue @@ -71,7 +71,7 @@ export default { </div> </div> <div class="compare-display-container"> - <div class="compare-display pull-left"> + <div class="compare-display float-left"> <span class="compare-label"> {{ s__('TimeTracking|Spent') }} </span> @@ -79,7 +79,7 @@ export default { {{ timeSpentHumanReadable }} </span> </div> - <div class="compare-display estimated pull-right"> + <div class="compare-display estimated float-right"> <span class="compare-label"> {{ s__('TimeTrackingEstimated|Est') }} </span> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue index 8f5d0bee107..7d56d2fa5ee 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -116,7 +116,7 @@ export default { <div class="title hide-collapsed"> {{ __('Time tracking') }} <div - class="help-button pull-right" + class="help-button float-right" v-if="!showHelpState" @click="toggleHelpState(true)" > @@ -127,7 +127,7 @@ export default { </i> </div> <div - class="close-help-button pull-right" + class="close-help-button float-right" v-if="showHelpState" @click="toggleHelpState(false)" > diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 8486019897d..cd954f75613 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -202,7 +202,7 @@ function UsersSelect(currentUser, els, options = {}) { tooltipTitle = __('Assignee'); } $value.html(assigneeTemplate(user)); - $collapsedSidebar.attr('title', tooltipTitle).tooltip('fixTitle'); + $collapsedSidebar.attr('title', tooltipTitle).tooltip('_fixTitle'); return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); }); }; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index 1fea231c816..1608858da22 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -127,7 +127,7 @@ export default { </span> <loading-button v-if="deployment.stop_url" - container-class="btn btn-default btn-xs prepend-left-default" + container-class="btn btn-default btn-sm prepend-left-default" label="Stop environment" :loading="isStopping" @click="stopEnvironment" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue index cb6e9858736..8338fde61c7 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue @@ -2,7 +2,7 @@ import tooltip from '../../vue_shared/directives/tooltip'; export default { - name: 'MRWidgetAuthor', + name: 'MrWidgetAuthor', directives: { tooltip, }, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue index 8f1fd809a81..644e4b7d81a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue @@ -1,10 +1,10 @@ <script> - import mrWidgetAuthor from './mr_widget_author.vue'; + import MrWidgetAuthor from './mr_widget_author.vue'; export default { name: 'MRWidgetAuthorTime', components: { - mrWidgetAuthor, + MrWidgetAuthor, }, props: { actionText: { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue index 18ee4c62bf1..51a0fda6555 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue @@ -131,7 +131,7 @@ export default { aria-hidden="true"> </i> </button> - <ul class="dropdown-menu dropdown-menu-align-right"> + <ul class="dropdown-menu dropdown-menu-right"> <li> <a class="js-download-email-patches" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue deleted file mode 100644 index f0298f732ea..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue +++ /dev/null @@ -1,20 +0,0 @@ -<script> - export default { - name: 'MRWidgetMaintainerEdit', - props: { - maintainerEditAllowed: { - type: Boolean, - default: false, - required: false, - }, - }, - }; -</script> - -<template> - <section class="mr-info-list mr-links"> - <p v-if="maintainerEditAllowed"> - {{ s__("mrWidget|Allows edits from maintainers") }} - </p> - </section> -</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue index ebaf2b972eb..b404a592234 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue @@ -42,7 +42,7 @@ @click="refreshWidget" :disabled="isRefreshing" type="button" - class="btn btn-xs btn-default" + class="btn btn-sm btn-default" > <loading-icon v-if="isRefreshing" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue index dad4b0fe49d..5c1500ab801 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue @@ -43,13 +43,13 @@ To merge this request, first rebase locally.`) }} <a v-if="mr.canMerge && mr.conflictResolutionPath" :href="mr.conflictResolutionPath" - class="js-resolve-conflicts-button btn btn-default btn-xs" + class="js-resolve-conflicts-button btn btn-default btn-sm" > {{ s__("mrWidget|Resolve conflicts") }} </a> <button v-if="mr.canMerge" - class="js-merge-locally-button btn btn-default btn-xs" + class="js-merge-locally-button btn btn-default btn-sm" data-toggle="modal" data-target="#modal_merge_info" > diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue index 7d366c495f0..05fecd4de35 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue @@ -98,7 +98,7 @@ export default { </span> <button @click="refresh" - class="btn btn-default btn-xs js-refresh-button" + class="btn btn-default btn-sm js-refresh-button" type="button" > {{ s__("mrWidget|Refresh now") }} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue index 7ff7fc7988a..e8352c362d6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue @@ -1,13 +1,13 @@ <script> import Flash from '../../../flash'; import statusIcon from '../mr_widget_status_icon.vue'; - import mrWidgetAuthor from '../../components/mr_widget_author.vue'; + import MrWidgetAuthor from '../../components/mr_widget_author.vue'; import eventHub from '../../event_hub'; export default { name: 'MRWidgetMergeWhenPipelineSucceeds', components: { - mrWidgetAuthor, + MrWidgetAuthor, statusIcon, }, props: { @@ -94,7 +94,7 @@ :disabled="isCancellingAutoMerge" role="button" href="#" - class="btn btn-xs btn-default js-cancel-auto-merge"> + class="btn btn-sm btn-default js-cancel-auto-merge"> <i v-if="isCancellingAutoMerge" class="fa fa-spinner fa-spin" @@ -129,7 +129,7 @@ :disabled="isRemovingSourceBranch" @click.prevent="removeSourceBranch" role="button" - class="btn btn-xs btn-default js-remove-source-branch" + class="btn btn-sm btn-default js-remove-source-branch" href="#" > <i diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index 3e36a3a10f9..8c7e0664559 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -118,7 +118,7 @@ <a v-if="mr.canRevertInCurrentMR" v-tooltip - class="btn btn-close btn-xs" + class="btn btn-close btn-sm" href="#modal-revert-commit" data-toggle="modal" data-container="body" @@ -129,7 +129,7 @@ <a v-else-if="mr.revertInForkPath" v-tooltip - class="btn btn-close btn-xs" + class="btn btn-close btn-sm" data-method="post" :href="mr.revertInForkPath" :title="revertTitle" @@ -139,7 +139,7 @@ <a v-if="mr.canCherryPickInCurrentMR" v-tooltip - class="btn btn-default btn-xs" + class="btn btn-default btn-sm" href="#modal-cherry-pick-commit" data-toggle="modal" data-container="body" @@ -150,7 +150,7 @@ <a v-else-if="mr.cherryPickInForkPath" v-tooltip - class="btn btn-default btn-xs" + class="btn btn-default btn-sm" data-method="post" :href="mr.cherryPickInForkPath" :title="cherryPickTitle" @@ -189,7 +189,7 @@ @click="removeSourceBranch" :disabled="isMakingRequest" type="button" - class="btn btn-xs btn-default js-remove-branch-button" + class="btn btn-sm btn-default js-remove-branch-button" > {{ s__("mrWidget|Remove Source Branch") }} </button> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue index bf8628d18a6..926a3172412 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue @@ -10,6 +10,6 @@ In EE, the configuration extends this object to add a functioning squash-before- button. */ -export default { - template: '', -}; +<script> + export default {}; +</script> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue index 3d9161f6926..086dbabe77e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue @@ -18,10 +18,10 @@ export default { <template> <div class="mr-widget-body mr-widget-empty-state"> <div class="row"> - <div class="artwork col-sm-5 col-sm-push-7 col-xs-12 text-center"> + <div class="artwork col-md-5 order-md-last col-12 text-center"> <span v-html="emptyStateSVG"></span> </div> - <div class="text col-sm-7 col-sm-pull-5 col-xs-12"> + <div class="text col-md-7 order-md-first col-12"> <span> Merge requests are a place to propose changes you have made to a project and discuss those changes with others. diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index 0264625a526..1d1c8ebc179 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -312,7 +312,7 @@ export default { v-else @click="toggleCommitMessageEditor" :disabled="isMergeButtonDisabled" - class="js-modify-commit-message-button btn btn-default btn-xs" + class="js-modify-commit-message-button btn btn-default btn-sm" type="button"> Modify commit message </button> @@ -329,7 +329,7 @@ export default { class="prepend-top-default commit-message-editor"> <div class="form-group clearfix"> <label - class="control-label" + class="col-form-label" for="commit-message"> Commit message </label> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue index a1f7e696795..8ea3f22ecc2 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue @@ -28,7 +28,7 @@ export default { <a v-if="mr.createIssueToResolveDiscussionsPath" :href="mr.createIssueToResolveDiscussionsPath" - class="btn btn-default btn-xs js-create-issue" + class="btn btn-default btn-sm js-create-issue" > {{ s__("mrWidget|Create an issue to resolve them later") }} </a> diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js index 7f5f28091da..15097fa2a3f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js +++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js @@ -15,7 +15,6 @@ export { default as WidgetHeader } from './components/mr_widget_header.vue'; export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue'; export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue'; export { default as Deployment } from './components/deployment.vue'; -export { default as WidgetMaintainerEdit } from './components/mr_widget_maintainer_edit.vue'; export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue'; export { default as MergedState } from './components/states/mr_widget_merged.vue'; export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue'; @@ -41,8 +40,8 @@ export { default as MRWidgetService } from './services/mr_widget_service'; export { default as eventHub } from './event_hub'; export { default as getStateKey } from './stores/get_state_key'; export { default as stateMaps } from './stores/state_maps'; -export { default as SquashBeforeMerge } from './components/states/mr_widget_squash_before_merge'; +export { default as SquashBeforeMerge } from './components/states/mr_widget_squash_before_merge.vue'; export { default as notify } from '../lib/utils/notify'; export { default as SourceBranchRemovalStatus } from './components/source_branch_removal_status.vue'; -export { default as mrWidgetOptions } from './mr_widget_options'; +export { default as mrWidgetOptions } from './mr_widget_options.vue'; diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 345f9ac1b4b..f69fe03fcb3 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -1,12 +1,13 @@ +<script> + import Project from '~/pages/projects/project'; import SmartInterval from '~/smart_interval'; -import Flash from '../flash'; +import createFlash from '../flash'; import { WidgetHeader, WidgetMergeHelp, WidgetPipeline, Deployment, - WidgetMaintainerEdit, WidgetRelatedLinks, MergedState, ClosedState, @@ -40,10 +41,39 @@ import { setFavicon } from '../lib/utils/common_utils'; export default { el: '#js-vue-mr-widget', name: 'MRWidget', + components: { + 'mr-widget-header': WidgetHeader, + 'mr-widget-merge-help': WidgetMergeHelp, + 'mr-widget-pipeline': WidgetPipeline, + Deployment, + 'mr-widget-related-links': WidgetRelatedLinks, + 'mr-widget-merged': MergedState, + 'mr-widget-closed': ClosedState, + 'mr-widget-merging': MergingState, + 'mr-widget-failed-to-merge': FailedToMerge, + 'mr-widget-wip': WorkInProgressState, + 'mr-widget-archived': ArchivedState, + 'mr-widget-conflicts': ConflictsState, + 'mr-widget-nothing-to-merge': NothingToMergeState, + 'mr-widget-not-allowed': NotAllowedState, + 'mr-widget-missing-branch': MissingBranchState, + 'mr-widget-ready-to-merge': ReadyToMergeState, + 'sha-mismatch': ShaMismatchState, + 'mr-widget-squash-before-merge': SquashBeforeMerge, + 'mr-widget-checking': CheckingState, + 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState, + 'mr-widget-pipeline-blocked': PipelineBlockedState, + 'mr-widget-pipeline-failed': PipelineFailedState, + 'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState, + 'mr-widget-auto-merge-failed': AutoMergeFailed, + 'mr-widget-rebase': RebaseState, + SourceBranchRemovalStatus, + }, props: { mrData: { type: Object, required: false, + default: null, }, }, data() { @@ -72,6 +102,13 @@ export default { (!this.mr.isNothingToMergeState && !this.mr.isMergedState); }, }, + created() { + this.initPolling(); + this.bindEventHubListeners(); + }, + mounted() { + this.handleMounted(); + }, methods: { createService(store) { const endpoints = { @@ -99,7 +136,7 @@ export default { cb.call(null, data); } }) - .catch(() => new Flash('Something went wrong. Please try again.')); + .catch(() => createFlash('Something went wrong. Please try again.')); }, initPolling() { this.pollingInterval = new SmartInterval({ @@ -134,7 +171,7 @@ export default { } }) .catch(() => { - new Flash('Something went wrong while fetching the environments for this merge request. Please try again.'); // eslint-disable-line + createFlash('Something went wrong while fetching the environments for this merge request. Please try again.'); // eslint-disable-line }); }, fetchActionsContent() { @@ -147,7 +184,7 @@ export default { Project.initRefSwitcher(); } }) - .catch(() => new Flash('Something went wrong. Please try again.')); + .catch(() => createFlash('Something went wrong. Please try again.')); }, handleNotification(data) { if (data.ci_status === this.mr.ciStatus) return; @@ -202,76 +239,53 @@ export default { this.initDeploymentsPolling(); }, }, - created() { - this.initPolling(); - this.bindEventHubListeners(); - }, - mounted() { - this.handleMounted(); - }, - components: { - 'mr-widget-header': WidgetHeader, - 'mr-widget-merge-help': WidgetMergeHelp, - 'mr-widget-pipeline': WidgetPipeline, - Deployment, - 'mr-widget-maintainer-edit': WidgetMaintainerEdit, - 'mr-widget-related-links': WidgetRelatedLinks, - 'mr-widget-merged': MergedState, - 'mr-widget-closed': ClosedState, - 'mr-widget-merging': MergingState, - 'mr-widget-failed-to-merge': FailedToMerge, - 'mr-widget-wip': WorkInProgressState, - 'mr-widget-archived': ArchivedState, - 'mr-widget-conflicts': ConflictsState, - 'mr-widget-nothing-to-merge': NothingToMergeState, - 'mr-widget-not-allowed': NotAllowedState, - 'mr-widget-missing-branch': MissingBranchState, - 'mr-widget-ready-to-merge': ReadyToMergeState, - 'mr-widget-sha-mismatch': ShaMismatchState, - 'mr-widget-squash-before-merge': SquashBeforeMerge, - 'mr-widget-checking': CheckingState, - 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState, - 'mr-widget-pipeline-blocked': PipelineBlockedState, - 'mr-widget-pipeline-failed': PipelineFailedState, - 'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState, - 'mr-widget-auto-merge-failed': AutoMergeFailed, - 'mr-widget-rebase': RebaseState, - SourceBranchRemovalStatus, - }, - template: ` - <div class="mr-state-widget prepend-top-default"> - <mr-widget-header :mr="mr" /> - <mr-widget-pipeline - v-if="shouldRenderPipelines" - :pipeline="mr.pipeline" - :ci-status="mr.ciStatus" - :has-ci="mr.hasCI" - /> - <deployment - v-for="deployment in mr.deployments" - :key="deployment.id" - :deployment="deployment" +}; +</script> +<template> + <div class="mr-state-widget prepend-top-default"> + <mr-widget-header + :mr="mr" + /> + <mr-widget-pipeline + v-if="shouldRenderPipelines" + :pipeline="mr.pipeline" + :ci-status="mr.ciStatus" + :has-ci="mr.hasCI" + /> + <deployment + v-for="deployment in mr.deployments" + :key="deployment.id" + :deployment="deployment" + /> + <div class="mr-widget-section"> + <component + :is="componentName" + :mr="mr" + :service="service" + /> + + <section + v-if="mr.maintainerEditAllowed" + class="mr-info-list mr-links" + > + {{ s__("mrWidget|Allows edits from maintainers") }} + </section> + + <mr-widget-related-links + v-if="shouldRenderRelatedLinks" + :state="mr.state" + :related-links="mr.relatedLinks" + /> + + <source-branch-removal-status + v-if="shouldRenderSourceBranchRemovalStatus" /> - <div class="mr-widget-section"> - <component - :is="componentName" - :mr="mr" - :service="service" /> - <mr-widget-maintainer-edit - :maintainerEditAllowed="mr.maintainerEditAllowed" /> - <mr-widget-related-links - v-if="shouldRenderRelatedLinks" - :state="mr.state" - :related-links="mr.relatedLinks" /> - <source-branch-removal-status - v-if="shouldRenderSourceBranchRemovalStatus" - /> - </div> - <div - class="mr-widget-footer" - v-if="shouldRenderMergeHelp"> - <mr-widget-merge-help /> - </div> </div> - `, -}; + <div + class="mr-widget-footer" + v-if="shouldRenderMergeHelp" + > + <mr-widget-merge-help /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue index 395a71acccf..7b5367ac19b 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue @@ -42,7 +42,7 @@ export default { target="_blank"> <icon name="download" - css-classes="pull-left append-right-8" + css-classes="float-left append-right-8" :size="16" /> {{ __('Download') }} diff --git a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue index dcf1489b37c..424af5a0293 100644 --- a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue +++ b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue @@ -87,7 +87,7 @@ <div :id="id" class="modal" - :class="id ? '' : 'show'" + :class="id ? '' : 'd-block'" role="dialog" tabindex="-1" > @@ -99,12 +99,12 @@ <div class="modal-content"> <div class="modal-header"> <slot name="header"> - <h4 class="modal-title pull-left"> + <h4 class="modal-title float-left"> {{ title }} </h4> <button type="button" - class="close pull-right" + class="close float-right" @click="emitCancel($event)" data-dismiss="modal" aria-label="Close" @@ -166,7 +166,7 @@ </div> <div v-if="!id" - class="modal-backdrop fade in" + class="modal-backdrop fade show" > </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue index f28e5e2715d..d5d5a7d3798 100644 --- a/app/assets/javascripts/vue_shared/components/gl_modal.vue +++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue @@ -53,6 +53,11 @@ export default { <div class="modal-content"> <div class="modal-header"> <slot name="header"> + <h4 class="modal-title"> + <slot name="title"> + {{ headerTitleText }} + </slot> + </h4> <button type="button" class="close js-modal-close-action" @@ -62,11 +67,6 @@ export default { > <span aria-hidden="true">×</span> </button> - <h4 class="modal-title"> - <slot name="title"> - {{ headerTitleText }} - </slot> - </h4> </slot> </div> diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index 088187ed348..ca17fa06a00 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -163,8 +163,8 @@ export default { <button v-if="hasSidebarButton" type="button" - class="btn btn-default visible-xs-block -visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header" + class="btn btn-default d-block d-sm-none d-md-none +sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header" aria-label="Toggle Sidebar" id="toggleSidebar" > diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index c0ee88bbf72..d63318f3da6 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -111,7 +111,7 @@ Attach a file </button> <button - class="btn btn-default btn-xs hide button-cancel-uploading-files" + class="btn btn-default btn-sm hide button-cancel-uploading-files" type="button" > Cancel diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue index 92d187e24bf..08d4936f480 100644 --- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue +++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue @@ -67,7 +67,7 @@ export default { <span v-if="shouldRenderBadge(tab.count)" - class="badge" + class="badge badge-pill" > {{ tab.count }} </span> diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue index 50b1508691b..eccba61a8c0 100644 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue @@ -54,7 +54,7 @@ <div class="note-header"> <div class="note-header-info"> <a :href="getUserData.path"> - <span class="hidden-xs">{{ getUserData.name }}</span> + <span class="d-none d-sm-block">{{ getUserData.name }}</span> <span class="note-headline-light">@{{ getUserData.username }}</span> </a> </div> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue index 3fcacd156c5..71ec34f2c7a 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue @@ -116,7 +116,7 @@ v-if="isLoading" :inline="true" /> - <div class="pull-right"> + <div class="float-right"> <button v-if="editable && !editing" type="button" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue index 34a07f33a23..3c400afdc1d 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue @@ -77,13 +77,13 @@ export default { <div class="clearfix"> <button type="button" - class="btn btn-primary pull-left js-new-label-btn disabled" + class="btn btn-primary float-left js-new-label-btn disabled" > {{ __('Create') }} </button> <button type="button" - class="btn btn-default pull-right js-cancel-label-btn" + class="btn btn-default float-right js-cancel-label-btn" > {{ __('Cancel') }} </button> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue index 7da82e90e29..9ac32ff13c6 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue @@ -21,7 +21,7 @@ export default { </i> <button type="button" - class="edit-link btn btn-blank pull-right js-sidebar-dropdown-toggle" + class="edit-link btn btn-blank float-right js-sidebar-dropdown-toggle" > {{ __('Edit') }} </button> diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue index bec4e7c99b6..368eeb6c453 100644 --- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue +++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue @@ -40,7 +40,7 @@ export default { :class="cssClass" :title="tooltipTitle(time)" :data-placement="tooltipPlacement" - data-container="body"> - {{ timeFormated(time) }} + data-container="body" + v-text="timeFormated(time)"> </time> </template> diff --git a/app/assets/javascripts/vue_shared/directives/popover.js b/app/assets/javascripts/vue_shared/directives/popover.js index eb35294906b..c913bc34c68 100644 --- a/app/assets/javascripts/vue_shared/directives/popover.js +++ b/app/assets/javascripts/vue_shared/directives/popover.js @@ -17,6 +17,6 @@ export default { }, unbind(el) { - $(el).popover('destroy'); + $(el).popover('dispose'); }, }; diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js index b7f7e9fec15..155e6b6698a 100644 --- a/app/assets/javascripts/vue_shared/directives/tooltip.js +++ b/app/assets/javascripts/vue_shared/directives/tooltip.js @@ -6,10 +6,10 @@ export default { }, componentUpdated(el) { - $(el).tooltip('fixTitle'); + $(el).tooltip('_fixTitle'); }, unbind(el) { - $(el).tooltip('destroy'); + $(el).tooltip('dispose'); }, }; diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss new file mode 100644 index 00000000000..3b7ee5c73e6 --- /dev/null +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -0,0 +1,149 @@ +/* + * Scss to help with bootstrap 3 to 4 migration + */ + +$text-color: $gl-text-color; + +$brand-primary: $gl-primary; +$brand-success: $gl-success; +$brand-info: $gl-info; +$brand-warning: $gl-warning; +$brand-danger: $gl-danger; + +$border-radius-base: 3px !default; + +$modal-body-bg: $white-light; +$input-border: $border-color; +$input-border-focus: $focus-border-color; + +$padding-base-vertical: $gl-vert-padding; +$padding-base-horizontal: $gl-padding; + +html { + // Override default font size used in bs4 + font-size: 14px; +} + +button, +html [type="button"], +[type="reset"], +[type="submit"] { + // Override bootstrap reboot + -webkit-appearance: inherit; +} + +[role="button"] { + cursor: pointer; +} + +a { + color: $gl-link-color; +} + +.form-group.row .col-form-label { + // Bootstrap 4 aligns labels to the left + // for horizontal forms + @include media-breakpoint-up(md) { + text-align: right; + } +} + +table { + // Remove any table border lines + border-spacing: 0; +} + +.tooltip { + // Fix bootstrap4 bug whereby tooltips flicker when they are hovered over their borders + pointer-events: none; +} + +.popover { + font-size: 14px; +} + +@each $breakpoint in map-keys($grid-breakpoints) { + @include media-breakpoint-up($breakpoint) { + $infix: breakpoint-infix($breakpoint, $grid-breakpoints); + + .d#{$infix}-table-header-group { display: table-header-group !important; } + } +} + +.text-secondary { + // Override Bootstrap's light secondary color + // We have to use !important because bootstrap has that set as well + color: $gl-text-color-secondary !important; +} + +// Polyfill deprecated selectors + +.hidden { + display: none !important; + visibility: hidden !important; +} + +.hide { + display: none; +} + +.dropdown-toggle::after { + // Remove bootstrap's dropdown caret + display: none; +} + +.badge { + padding: 4px 5px; + font-size: 12px; + font-style: normal; + font-weight: $gl-font-weight-normal; + display: inline-block; + + &.badge-gray { + background-color: $label-gray-bg; + color: $gl-text-color; + text-shadow: none; + } + + &.badge-inverse { + background-color: $label-inverse-bg; + } +} + +.divider { + @extend .dropdown-divider; +} + +.info-well { + background: $theme-gray-50; + color: $gl-text-color; + border: 1px solid $border-color; + border-radius: 4px; + margin-bottom: 16px; + + .well-segment { + padding: 16px; + + &:not(:last-of-type) { + border-bottom: 1px solid $well-inner-border; + } + } +} + +.card { + &.card-without-border { + @extend .border-0; + } + + &.card-without-margin { + margin: 0; + } + + &.bg-light { + @extend .border-0; + } +} + +.nav-tabs .nav-link { + border: 0; +} diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 9bd35183d8a..7c28024001f 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -1,7 +1,7 @@ @import 'framework/variables'; @import 'framework/mixins'; -@import 'framework/tw_bootstrap_variables'; -@import 'framework/tw_bootstrap'; +@import '../../../node_modules/bootstrap/scss/bootstrap'; +@import 'bootstrap_migration'; @import 'framework/layout'; @import 'framework/animations'; diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 077d0424093..c1ec11e434a 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -31,7 +31,7 @@ .avatar { @extend .avatar-circle; - @include transition-property(none); + transition-property: none; width: 40px; height: 40px; diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss index 6bbe32df772..57df9b969c3 100644 --- a/app/assets/stylesheets/framework/badges.scss +++ b/app/assets/stylesheets/framework/badges.scss @@ -1,4 +1,4 @@ -.badge { +.badge.badge-pill { font-weight: $gl-font-weight-normal; background-color: $badge-bg; color: $badge-color; diff --git a/app/assets/stylesheets/framework/banner.scss b/app/assets/stylesheets/framework/banner.scss index 02f3896d591..71bbab2065d 100644 --- a/app/assets/stylesheets/framework/banner.scss +++ b/app/assets/stylesheets/framework/banner.scss @@ -23,7 +23,7 @@ border-bottom: 1px solid $border-color; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { justify-content: center; flex-direction: column; align-items: center; diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss index 9982a5779af..91dbb2a6365 100644 --- a/app/assets/stylesheets/framework/blank.scss +++ b/app/assets/stylesheets/framework/blank.scss @@ -37,7 +37,7 @@ flex: 0 0 100%; margin-bottom: 15px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { flex: 0 0 49%; &:nth-child(odd) { @@ -67,7 +67,7 @@ border: 1px solid $border-color; border-radius: $border-radius-default; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: flex; align-items: center; padding: 50px 30px; @@ -89,12 +89,12 @@ } .blank-state-body { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { text-align: center; margin-top: 20px; } - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { padding-left: 20px; } } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index c60f65e7a85..c5be27f2d29 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -151,7 +151,7 @@ display: inline-block; margin-left: 5px; font-size: 18px; - color: $gray; + color: color("gray"); } p { @@ -195,7 +195,7 @@ } } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { text-align: center; .avatar { @@ -315,7 +315,7 @@ } } - @media (max-width: $screen-sm-min) { + @include media-breakpoint-down(sm) { flex-direction: column; .inner-content { @@ -342,7 +342,7 @@ .btn { margin: $btn-side-margin 5px; - @media(max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { width: 100%; } } diff --git a/app/assets/stylesheets/framework/broadcast_messages.scss b/app/assets/stylesheets/framework/broadcast_messages.scss index 9b54fb94cdc..d3e7d751e63 100644 --- a/app/assets/stylesheets/framework/broadcast_messages.scss +++ b/app/assets/stylesheets/framework/broadcast_messages.scss @@ -19,3 +19,9 @@ @extend .broadcast-message; margin-bottom: 20px; } + +.toggle-colors { + input { + min-height: 34px; + } +} diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index cd9d60b96d3..0115f542c88 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -304,7 +304,8 @@ padding: 0 5px; } -.input-group-btn { +.input-group-prepend, +.input-group-append { .btn { @include btn-middle; @@ -393,7 +394,7 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .btn-wide-on-xs { width: 100%; } diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index c165ec0b94b..0b9dff64b0b 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -4,7 +4,7 @@ border-top: 0; direction: rtl; - @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { + @media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) { overflow-x: auto; } } diff --git a/app/assets/stylesheets/framework/ci_variable_list.scss b/app/assets/stylesheets/framework/ci_variable_list.scss index 5fe835dd8f9..7207e5119ce 100644 --- a/app/assets/stylesheets/framework/ci_variable_list.scss +++ b/app/assets/stylesheets/framework/ci_variable_list.scss @@ -10,14 +10,14 @@ display: flex; align-items: flex-start; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { align-items: flex-end; } &:not(:last-child) { margin-bottom: $gl-btn-padding; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { margin-bottom: 3 * $gl-btn-padding; } } @@ -26,7 +26,7 @@ .ci-variable-body-item:last-child { margin-right: $ci-variable-remove-button-width; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { margin-right: 0; } } @@ -35,7 +35,7 @@ display: none; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .ci-variable-row-body { margin-right: $ci-variable-remove-button-width; } @@ -48,7 +48,7 @@ align-items: flex-start; width: 100%; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { display: block; } } @@ -59,7 +59,7 @@ &:not(:last-child) { margin-right: $gl-btn-padding; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { margin-right: 0; margin-bottom: $gl-btn-padding; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 2faea55a5f5..1e7b9534275 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -70,7 +70,7 @@ pre { padding: 0; } - &.well-pre { + &.card.card-body-pre { border: 1px solid $well-pre-bg; background: $gray-light; border-radius: 0; @@ -218,7 +218,7 @@ li.note { } .git_error_tips { - @extend .col-md-6; + @extend .col-lg-6; text-align: left; margin-top: 40px; @@ -308,7 +308,7 @@ img.emoji { .btn-sign-in { text-shadow: none; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { margin-top: 8px; } } @@ -327,7 +327,7 @@ img.emoji { } } -.well { +.card.card-body { margin-bottom: $gl-padding; hr { @@ -336,7 +336,7 @@ img.emoji { } .search_box { - @extend .well; + @extend .card.card-body; text-align: center; } diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss index e2d97d0298f..1a415e1b852 100644 --- a/app/assets/stylesheets/framework/contextual_sidebar.scss +++ b/app/assets/stylesheets/framework/contextual_sidebar.scss @@ -1,11 +1,11 @@ .page-with-contextual-sidebar { transition: padding-left $sidebar-transition-duration; - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { padding-left: $contextual-sidebar-collapsed-width; } - @media (min-width: $screen-lg-min) { + @include media-breakpoint-up(lg) { padding-left: $contextual-sidebar-width; } @@ -16,7 +16,7 @@ } .page-with-icon-sidebar { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { padding-left: $contextual-sidebar-collapsed-width; } } @@ -75,7 +75,7 @@ transform: translate3d(0, 0, 0); &:not(.sidebar-collapsed-desktop) { - @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { + @media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) { box-shadow: inset -2px 0 0 $border-color, 2px 1px 3px $dropdown-shadow-color; } @@ -88,7 +88,7 @@ overflow-x: hidden; } - .badge:not(.fly-out-badge), + .badge.badge-pill:not(.fly-out-badge), .sidebar-context-title, .nav-item-name { display: none; @@ -142,7 +142,7 @@ } } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { left: (-$contextual-sidebar-width); } @@ -166,7 +166,7 @@ width: 100%; overflow: auto; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { overflow: hidden; } } @@ -207,7 +207,7 @@ > li { > a { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { margin-right: 2px; } @@ -222,7 +222,7 @@ } .sidebar-sub-level-items { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { position: fixed; top: 0; left: 0; @@ -277,7 +277,7 @@ } } - .badge { + .badge.badge-pill { background-color: $inactive-badge-background; color: $gl-text-color-secondary; } @@ -291,7 +291,7 @@ padding-left: 11px; } - .badge { + .badge.badge-pill { font-weight: $gl-font-weight-bold; } @@ -339,7 +339,7 @@ } .toggle-sidebar-button { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { display: none; } } @@ -420,7 +420,7 @@ color: $gl-text-color-secondary; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { display: flex; align-items: center; @@ -429,7 +429,7 @@ } } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { + .breadcrumbs-links { padding-left: $gl-padding; border-left: 1px solid $gl-text-color-quaternary; @@ -437,7 +437,7 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .close-nav-button { display: flex; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 6576f7ca3b3..b91d579cae6 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -24,7 +24,7 @@ display: none; } -.open { +.show.dropdown { .dropdown-menu, .dropdown-menu-nav { @include set-visible; @@ -32,7 +32,7 @@ max-height: $dropdown-max-height; overflow-y: auto; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { width: 100%; } } @@ -188,7 +188,7 @@ text-decoration: none; - .badge { + .badge.badge-pill { background-color: darken($dropdown-link-hover-bg, 5%); } } @@ -214,11 +214,10 @@ .dropdown-menu, .dropdown-menu-nav { - @include set-invisible; + display: none; position: absolute; width: auto; top: 100%; - left: 0; z-index: 300; min-width: 240px; max-width: 500px; @@ -332,7 +331,7 @@ color: $gl-text-color-secondary; } - .badge + span:not(.badge) { + .badge.badge-pill + span:not(.badge.badge-pill) { // Expects up to 3 digits on the badge margin-right: 40px; } @@ -396,7 +395,7 @@ transform: translateY(0); } -.comment-type-dropdown.open .dropdown-menu { +.comment-type-dropdown.show .dropdown-menu { display: block; } @@ -477,16 +476,11 @@ .dropdown-select { width: $dropdown-width; - @media (max-width: $screen-sm-min) { + @include media-breakpoint-down(sm) { width: 100%; } } -.dropdown-menu-align-right { - left: auto; - right: 0; -} - .dropdown-menu-selectable { li { a, @@ -787,7 +781,7 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .navbar-gitlab { li.header-projects, li.header-more, @@ -842,7 +836,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu { width: 70%; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { flex-direction: column; width: 100%; height: auto; @@ -898,7 +892,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu { height: 284px; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .projects-list-frequent-container { width: auto; height: auto; @@ -939,7 +933,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu { color: $gl-text-color-secondary; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .project-item-metadata-container { float: none; } diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss index 4f26cd015e4..cad915bc86f 100644 --- a/app/assets/stylesheets/framework/feature_highlight.scss +++ b/app/assets/stylesheets/framework/feature_highlight.scss @@ -79,7 +79,7 @@ border-right-color: $dropdown-border-color; } - .popover-content { + .popover-body { padding: 0; } } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index d835d49d8b2..f77ec4b6a2c 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -75,7 +75,7 @@ padding: 8px $gl-padding; border-bottom: 1px solid $border-color; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { text-align: left; } @@ -125,7 +125,7 @@ &.wiki { padding: $gl-padding; - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { padding: $gl-padding * 2; } } @@ -364,7 +364,7 @@ span.idiff { } } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { display: block; .file-actions { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 621a4adc0cb..0ee5748952a 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -9,19 +9,19 @@ float: right; margin-right: 0; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { float: none; } } } .filters-section { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { display: inline-block; } } -@media (min-width: $screen-sm-min) { +@include media-breakpoint-up(sm) { .filter-item:not(:last-child) { margin-right: 6px; } @@ -37,7 +37,7 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .filter-item { display: block; margin: 0 0 10px; @@ -53,7 +53,7 @@ display: -webkit-flex; display: flex; - @media (max-width: $screen-xs-min) { + @include media-breakpoint-down(xs) { -webkit-flex-direction: column; flex-direction: column; } @@ -193,7 +193,7 @@ border: 1px solid $border-color; background-color: $white-light; - @media (max-width: $screen-xs-min) { + @include media-breakpoint-down(xs) { -webkit-flex: 1 1 auto; flex: 1 1 auto; margin-bottom: 10px; @@ -264,7 +264,7 @@ max-width: 280px; overflow: auto; - @media (max-width: $screen-xs-min) { + @include media-breakpoint-down(xs) { width: auto; left: 0; right: 0; @@ -315,7 +315,7 @@ .filtered-search-history-dropdown { width: 40%; - @media (max-width: $screen-xs-min) { + @include media-breakpoint-down(xs) { left: 0; right: 0; max-width: none; @@ -353,7 +353,7 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .issues-details-filters { padding: 0 0 10px; background-color: $white-light; @@ -361,7 +361,7 @@ } } -@media (max-width: $screen-xs) { +@include media-breakpoint-down(sm) { .filter-dropdown-container { .dropdown-toggle, .dropdown, diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index cb2f71b0033..52c3f18a682 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -8,19 +8,19 @@ .flash-notice { @extend .alert; - @extend .alert-info; + background-color: $blue-500; margin: 0; } .flash-warning { @extend .alert; - @extend .alert-warning; + background-color: $orange-500; margin: 0; } .flash-alert { @extend .alert; - @extend .alert-danger; + background-color: $red-500; margin: 0; .flash-text, @@ -42,14 +42,16 @@ .flash-success { @extend .alert; - @extend .alert-success; + background-color: $green-500; margin: 0; } .flash-notice, .flash-alert, - .flash-success { + .flash-success, + .flash-warning { border-radius: $border-radius-default; + color: $white-light; .container-fluid, .container-fluid.container-limited { @@ -72,7 +74,7 @@ } } -@media (max-width: $screen-sm-max) { +@include media-breakpoint-down(sm) { ul.notes { .flash-container.timeline-content { margin-left: 0; diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 2c30311b1c1..c76ea532912 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -36,8 +36,8 @@ label { } } -.control-label { - @extend .col-sm-2; +.form-control-label { + @extend .col-md-2; } .inline-input-group { @@ -48,21 +48,21 @@ label { width: 150px; } -@media (min-width: $screen-sm-min) { +@include media-breakpoint-up(sm) { .custom-form-control { width: 150px; } } /* Medium devices (desktops, 992px and up) */ -@media (min-width: $screen-md-min) { +@include media-breakpoint-up(md) { .custom-form-control { width: 170px; } } /* Large devices (large desktops, 1200px and up) */ -@media (min-width: $screen-lg-min) { +@include media-breakpoint-up(lg) { .custom-form-control { width: 200px; } @@ -72,7 +72,7 @@ label { margin-left: 0; margin-right: 0; - .control-label { + .form-control-label { font-weight: $gl-font-weight-bold; padding-top: 4px; } @@ -83,7 +83,8 @@ label { font-family: $monospace_font; } - .input-group-btn .btn { + .input-group-prepend .btn, + .input-group-append .btn { padding: 3px $gl-btn-padding; background-color: $gray-light; border: 1px solid $border-color; @@ -102,10 +103,10 @@ label { } } - @media(max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { padding: 0 $gl-padding; - .control-label, + .form-control-label, .text-block { padding-left: 0; } @@ -120,6 +121,14 @@ label { @include box-shadow(none); border-radius: 2px; padding: $gl-vert-padding $gl-input-padding; + + &.input-short { + width: $input-short-width; + + @include media-breakpoint-up(md) { + width: $input-short-md-width; + } + } } .select-wrapper { @@ -155,8 +164,8 @@ label { margin-top: 35px; } -.form-group .control-label, -.form-group .control-label-full-width { +.form-group .form-control-label, +.form-group .form-control-label-full-width { font-weight: $gl-font-weight-normal; } @@ -170,17 +179,19 @@ label { max-width: 180px; } - .input-group-addon { + .input-group-prepend, + .input-group-append { background-color: $input-group-addon-bg; } - .input-group-addon:not(:first-child):not(:last-child) { + .input-group-prepend:not(:first-child):not(:last-child), + .input-group-append:not(:first-child):not(:last-child) { border-left: 0; border-right: 0; } } -.help-block { +.form-text.text-muted { margin-bottom: 0; margin-top: #{$grid-size / 2}; } @@ -221,7 +232,7 @@ label { } } -@media(max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .remember-me { .remember-me-checkbox { margin-top: 0; diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss index 0bbd6eb27c1..d6ae8cbb416 100644 --- a/app/assets/stylesheets/framework/gitlab_theme.scss +++ b/app/assets/stylesheets/framework/gitlab_theme.scss @@ -21,7 +21,7 @@ } .container-fluid { - .navbar-toggle { + .navbar-toggler { border-left: 1px solid lighten($color-700, 10%); } } @@ -63,7 +63,7 @@ &:hover, &:focus { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { background-color: rgba($color-200, 0.2); } @@ -165,14 +165,14 @@ } } - .sidebar-top-level-items > li.active .badge { + .sidebar-top-level-items > li.active .badge.badge-pill { color: $color-800; } - .nav-links li.active a { - border-bottom-color: $color-500; + .nav-links li a.active { + border-bottom: 2px solid $color-500; - .badge { + .badge.badge-pill { font-weight: $gl-font-weight-bold; } } @@ -277,8 +277,8 @@ body { } .container-fluid { - .navbar-toggle, - .navbar-toggle:hover { + .navbar-toggler, + .navbar-toggler:hover { color: $theme-gray-700; border-left: 1px solid $theme-gray-200; } @@ -328,7 +328,7 @@ body { } } - .sidebar-top-level-items > li.active .badge { + .sidebar-top-level-items > li.active .badge.badge-pill { color: $theme-gray-900; } } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 0136af76a13..2085e5646ef 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -37,6 +37,7 @@ } .header-content { + width: 100%; display: -webkit-flex; display: flex; justify-content: space-between; @@ -91,7 +92,7 @@ border-radius: $border-radius-default; .tanuki-logo { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { margin-right: 8px; } } @@ -110,7 +111,7 @@ } &.menu-expanded { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .title-container { display: none; } @@ -133,13 +134,13 @@ border-top: 0; padding: 0; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { flex: 1 1 auto; } .nav { - > li:not(.hidden-xs) a { - @media (max-width: $screen-xs-max) { + > li:not(.d-none) a { + @include media-breakpoint-down(xs) { margin-left: 0; min-width: 100%; } @@ -156,7 +157,7 @@ } } - .navbar-toggle { + .navbar-toggler { right: -10px; border-radius: 0; min-width: 45px; @@ -181,14 +182,14 @@ } .navbar-nav { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { display: -webkit-flex; display: flex; padding-right: 10px; } li { - .badge { + .badge.badge-pill { box-shadow: none; font-weight: $gl-font-weight-bold; } @@ -197,7 +198,7 @@ .nav > li { &.header-user { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { padding-left: 10px; } } @@ -208,7 +209,7 @@ padding: 6px 8px; height: 32px; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { padding: 0; } @@ -324,8 +325,8 @@ fill: currentColor; } -.header-user .dropdown-menu-nav, -.header-new .dropdown-menu-nav { +.header-user .dropdown-menu, +.header-new .dropdown-menu { margin-top: $dropdown-vertical-offset; } @@ -378,7 +379,7 @@ margin-bottom: 0; line-height: 16px; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { flex-wrap: wrap; } @@ -410,7 +411,7 @@ .breadcrumb-item-text { text-decoration: inherit; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { @include str-truncated(128px); } } @@ -452,7 +453,7 @@ .navbar-nav { li { - .badge { + .badge.badge-pill { position: inherit; font-weight: $gl-font-weight-normal; margin-left: -6px; @@ -478,7 +479,7 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .navbar-gitlab .container-fluid { font-size: 18px; @@ -493,7 +494,7 @@ margin-left: -8px; margin-right: -10px; - .nav > li:not(.hidden-xs) { + .nav > li:not(.d-none) { display: table-cell !important; width: 25%; @@ -514,7 +515,7 @@ } .header-user { - .dropdown-menu-nav { + .dropdown-menu { width: auto; min-width: 160px; margin-top: 4px; diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index d8c57a0e2d9..1d247671761 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -11,7 +11,7 @@ padding: 5px 11px; margin-top: 4px; /* Small devices (tablets, 768px and up) */ - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { padding: 0 $gl-btn-padding; margin-top: 5px; } diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index d107422e517..0536c39cee7 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -15,7 +15,7 @@ body { background-color: $white-light !important; } - &.card-content { + &.board-card-content { background-color: $gray-darker; .content-wrapper { @@ -74,7 +74,7 @@ body { } /* Center alert text and alert action links on smaller screens */ - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { .alert { text-align: center; } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index f1a8a46dda4..17f4958d535 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -2,7 +2,7 @@ * Well styled list * */ -.well-list { +.card-body-list { position: relative; margin: 0; padding: 0; @@ -72,7 +72,7 @@ } } - .well-title { + .card.card-body-title { font-size: $list-font-size; line-height: 18px; } @@ -159,7 +159,7 @@ ul.content-list { &:last-child { margin-right: 0; - @media(max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { margin: 0 auto; } } @@ -173,7 +173,7 @@ ul.content-list { .member-controls { float: none; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { float: right; } } @@ -228,7 +228,7 @@ ul.content-list { } } - .label-default { + .badge-secondary { color: $gl-text-color-secondary; } @@ -237,7 +237,7 @@ ul.content-list { } } -.panel > .content-list > li { +.card > .content-list > li { padding: $gl-padding-top $gl-padding; } @@ -279,251 +279,14 @@ ul.indent-list { padding: 10px 0 0 30px; } - // Specific styles for tree list @keyframes spin-avatar { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } -.groups-list-tree-container { - .has-no-search-results { - text-align: center; - padding: $gl-padding; - font-style: italic; - color: $well-light-text-color; - } - - > .group-list-tree > .group-row.has-children:first-child { - border-top: 0; - } -} - -.group-list-tree { - .avatar-container.content-loading { - position: relative; - - > a, - > a .avatar { - height: 100%; - border-radius: 50%; - } - - > a { - padding: 2px; - - .avatar { - border: 2px solid $white-normal; - - &.identicon { - line-height: 15px; - } - } - } - - &::after { - content: ""; - position: absolute; - height: 100%; - width: 100%; - background-color: transparent; - border: 2px outset $kdb-border; - border-radius: 50%; - animation: spin-avatar 3s infinite linear; - } - } - - .folder-toggle-wrap { - float: left; - line-height: $list-text-height; - font-size: 0; - - span { - font-size: $gl-font-size; - } - } - - .folder-caret, - .item-type-icon { - display: inline-block; - } - - .folder-caret { - width: 15px; - - svg { - margin-bottom: 2px; - } - } - - .item-type-icon { - margin-top: 2px; - width: 20px; - } - - > .group-row:not(.has-children) { - .folder-caret { - opacity: 0; - } - } - - .content-list li:last-child { - padding-bottom: 0; - } - - .group-list-tree { - margin-bottom: 0; - margin-left: 30px; - position: relative; - - &::before { - content: ''; - display: block; - width: 0; - position: absolute; - top: 5px; - bottom: 0; - left: -16px; - border-left: 2px solid $border-white-normal; - } - - .group-row { - position: relative; - - &::before { - content: ""; - display: block; - width: 10px; - height: 0; - border-top: 2px solid $border-white-normal; - position: absolute; - top: 30px; - left: -16px; - } - - &:last-child::before { - background: $white-light; - height: auto; - top: 30px; - bottom: 0; - } - - &.being-removed { - opacity: 0.5; - } - } - } - - .group-row { - padding: 0; - - &.has-children { - border-top: 0; - } - - &:first-child { - border-top: 1px solid $white-normal; - } - - &:last-of-type { - .group-row-contents:not(:hover) { - border-bottom: 1px solid transparent; - } - } - } - - .group-row-contents { - padding: 10px 10px 8px; - border-top: solid 1px transparent; - border-bottom: solid 1px $white-normal; - - &:hover { - border-color: $row-hover-border; - background-color: $row-hover; - cursor: pointer; - } - - .avatar-container > a { - width: 100%; - text-decoration: none; - } - - &.has-more-items { - display: block; - padding: 20px 10px; - } - - .stats { - position: relative; - line-height: 46px; - - > span { - display: inline-flex; - align-items: center; - height: 16px; - min-width: 30px; - } - - > span:last-child { - margin-right: 0; - } - - .stat-value { - margin: 2px 0 0 5px; - } - } - - .controls { - margin-left: 5px; - - > .btn { - margin-right: $btn-xs-side-margin; - } - } - } - - .project-row-contents .stats { - line-height: inherit; - - > span:first-child { - margin-left: 25px; - } - - .item-visibility { - margin-right: 0; - } - - .last-updated { - position: absolute; - right: 12px; - min-width: 250px; - text-align: right; - color: $gl-text-color-secondary; - } - } -} - .namespace-title { .tooltip-inner { max-width: 350px; } } - -ul.group-list-tree { - li.group-row { - > .group-row-contents .title { - line-height: $list-text-height; - } - - &.has-description > .group-row-contents .title { - line-height: inherit; - } - } -} - -.js-groups-list-holder { - .groups-list-loading { - font-size: 34px; - text-align: center; - } -} diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 7b5d1c2cf8b..b893151e4fe 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -74,7 +74,7 @@ } .md-header-tab { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { flex: 1; width: 100%; border-bottom: 1px solid $border-color; @@ -90,7 +90,7 @@ &.active { display: block; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { flex: none; display: flex; justify-content: center; @@ -192,7 +192,7 @@ margin-left: $gl-padding; margin-right: -5px; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { margin-left: 0; margin-right: 0; } @@ -268,7 +268,7 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .atwho-view-ul { width: 350px; } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 9ff24ebc127..0ea0b65b95f 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -210,3 +210,15 @@ margin-left: -$size; } } + +/* + * Mixin that fixes wrapping issues with long strings (e.g. URLs) + * + * Note: the width needs to be set for it to work in Firefox + */ +@mixin overflow-break-word { + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; + max-width: 100%; +} diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 9e03bb98b8e..6244fb86fea 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -1,5 +1,5 @@ /** Common mobile (screen XS, SM) styles **/ -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .container .content { margin-top: 20px; } @@ -14,7 +14,7 @@ font-size: 12px; margin-right: 3px; - .badge { + .badge.badge-pill { display: none; } } @@ -86,7 +86,7 @@ } } -@media (max-width: $screen-sm-max) { +@include media-breakpoint-down(sm) { .issues-filters { .milestone-filter { display: none; diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index eb789cc64b0..667661d8b5c 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -46,7 +46,7 @@ margin-left: $grid-size; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { flex-direction: column; .btn + .btn { @@ -55,7 +55,7 @@ } } - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { .btn:first-of-type { margin-left: auto; } @@ -74,7 +74,7 @@ body.modal-open { } } -@media (min-width: $screen-lg-min) { +@include media-breakpoint-up(lg) { .modal-full { width: 98%; } @@ -84,7 +84,7 @@ body.modal-open { background-color: $black-transparent; z-index: 2100; - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { .modal-dialog { margin: 30px auto; } diff --git a/app/assets/stylesheets/framework/page_header.scss b/app/assets/stylesheets/framework/page_header.scss index 0c879f40930..660e3dcac8d 100644 --- a/app/assets/stylesheets/framework/page_header.scss +++ b/app/assets/stylesheets/framework/page_header.scss @@ -3,7 +3,7 @@ padding: 10px 0; margin-bottom: 0; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: flex; align-items: center; @@ -19,7 +19,7 @@ margin-right: 3px; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .btn { width: 100%; margin-top: 10px; @@ -35,7 +35,7 @@ @extend .avatar-inline; margin-left: 0; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { margin-left: 4px; } } diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss index c3ec9db0f07..d3e013590b6 100644 --- a/app/assets/stylesheets/framework/pagination.scss +++ b/app/assets/stylesheets/framework/pagination.scss @@ -6,6 +6,7 @@ .pagination { padding: 0; + margin: 20px 0; a { cursor: pointer; @@ -30,7 +31,7 @@ } } -.panel > .gl-pagination { +.card > .gl-pagination { margin: 0; } @@ -44,7 +45,7 @@ display: none; } - .page { + .page-item { display: none; &.active { @@ -57,13 +58,13 @@ /** * Small screen pagination */ -@media (max-width: $screen-xs-min) { +@include media-breakpoint-down(xs) { .gl-pagination { .pagination li a { padding: 6px 10px; } - .page { + .page-item { display: none; &.active { @@ -76,9 +77,9 @@ /** * Medium screen pagination */ -@media (min-width: $screen-xs-min) and (max-width: $screen-md-max) { +@media (min-width: map-get($grid-breakpoints, xs)) and (max-width: map-get($grid-breakpoints, sm)) { .gl-pagination { - .page { + .page-item { display: none; &.active, diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index e8d69e62194..a8e28104a94 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -1,14 +1,14 @@ -.panel { +.card { margin-bottom: $gl-padding; } -.panel-slim { - @extend .panel; +.card-slim { + @extend .card; margin-bottom: $gl-vert-padding; } -.panel-heading { +.card-header { padding: $gl-vert-padding $gl-padding; line-height: 36px; @@ -21,7 +21,7 @@ line-height: 20px; } - .badge { + .badge.badge-pill { margin-top: -2px; margin-left: 5px; } @@ -41,12 +41,13 @@ } } -.panel-empty-heading { +.card-empty-heading { border-bottom: 0; } -.panel-body { +.card-body { padding: $gl-padding; + background-color: $white-light; .form-actions { margin: -$gl-padding; @@ -54,7 +55,7 @@ } } -.panel-title { +.card-title { font-size: inherit; line-height: inherit; } diff --git a/app/assets/stylesheets/framework/responsive_tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss index 34fccf6f0a4..764bebd82c6 100644 --- a/app/assets/stylesheets/framework/responsive_tables.scss +++ b/app/assets/stylesheets/framework/responsive_tables.scss @@ -6,7 +6,7 @@ .gl-responsive-table-row-layout { width: 100%; - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { display: flex; align-items: center; @@ -21,7 +21,7 @@ margin-top: 10px; border: 1px solid $border-color; - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { margin: 0; padding: $gl-padding 0; border: 0; @@ -44,13 +44,13 @@ &.section-#{$width} { flex: 0 0 #{$width + '%'}; - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { max-width: #{$width + '%'}; } } } - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { display: flex; align-self: stretch; padding: 10px; @@ -65,7 +65,7 @@ &.section-wrap { white-space: normal; - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { flex-wrap: wrap; } } @@ -76,11 +76,11 @@ } .table-button-footer { - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { text-align: right; } - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { display: block; align-self: stretch; min-height: 0; @@ -122,7 +122,7 @@ .table-row-header { font-size: 13px; - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { display: none; } } @@ -132,13 +132,13 @@ color: $gl-text-color-secondary; text-align: left; - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { display: none; } } .table-mobile-content { - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { @include flex-max-width(60); text-align: right; } @@ -154,7 +154,7 @@ overflow: hidden; text-overflow: ellipsis; - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { flex: 0 0 90%; } diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss index 66dbe403385..c3c64adf3da 100644 --- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss +++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss @@ -27,18 +27,18 @@ color: $black; border-bottom: 2px solid $gray-darkest; - .badge { + .badge.badge-pill { color: $black; } } - } - - &.active a { - color: $black; - font-weight: $gl-font-weight-bold; - .badge { + &.active { color: $black; + font-weight: $gl-font-weight-bold; + + .badge.badge-pill { + color: $black; + } } } } @@ -56,7 +56,7 @@ white-space: normal; /* Small devices (phones, tablets, 768px and lower) */ - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { width: 100%; } } @@ -80,7 +80,7 @@ } /* Small devices (phones, tablets, 768px and lower) */ - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { width: 100%; &.mobile-separator { @@ -124,21 +124,13 @@ position: relative; /* Medium devices (desktops, 992px and up) */ - @media (min-width: $screen-md-min) { width: 200px; } + @include media-breakpoint-up(md) { width: 200px; } /* Large devices (large desktops, 1200px and up) */ - @media (min-width: $screen-lg-min) { width: 250px; } - - &.input-short { - /* Medium devices (desktops, 992px and up) */ - @media (min-width: $screen-md-min) { width: 170px; } - - /* Large devices (large desktops, 1200px and up) */ - @media (min-width: $screen-lg-min) { width: 210px; } - } + @include media-breakpoint-up(lg) { width: 250px; } } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { padding-bottom: 0; width: 100%; @@ -164,10 +156,6 @@ } } - .input-short { - width: 100%; - } - .icon-label { display: inline-block; } @@ -184,7 +172,7 @@ .nav-controls { width: auto; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { width: 100%; } } @@ -204,7 +192,7 @@ width: 100%; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { flex-flow: row wrap; .nav-controls { @@ -364,7 +352,7 @@ max-width: 300px; width: auto; - @media(max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { max-width: 250px; } } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 64fff7463d2..8c716400913 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -31,7 +31,7 @@ .right-sidebar-collapsed { padding-right: 0; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { &:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper { padding-right: $gutter_collapsed_width; } @@ -65,13 +65,13 @@ display: inline-flex; } - @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + @include media-breakpoint-only(sm) { &:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper { padding-right: $gutter_collapsed_width; } } - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { .content-wrapper { padding-right: $gutter_width; } diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss index 606d4675f19..f80e9b1014b 100644 --- a/app/assets/stylesheets/framework/snippets.scss +++ b/app/assets/stylesheets/framework/snippets.scss @@ -40,7 +40,7 @@ } .snippet-actions { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { float: right; } } @@ -67,7 +67,8 @@ padding: 8px 40px; } - .embed-toggle { + .embed-toggle, + .snippet-clipboard-btn { height: 35px; } } diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 5bde96caf42..a10bd1544c5 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -48,7 +48,7 @@ table { } .responsive-table { - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { th { width: 100%; } diff --git a/app/assets/stylesheets/framework/tabs.scss b/app/assets/stylesheets/framework/tabs.scss index c8ba14b7066..6b60149fbbb 100644 --- a/app/assets/stylesheets/framework/tabs.scss +++ b/app/assets/stylesheets/framework/tabs.scss @@ -1,6 +1,7 @@ .gitlab-tabs { background: $gray-light; border: 1px solid $border-color; + flex-wrap: nowrap; li { width: 50%; diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 373f35e71d8..75c11590547 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -4,7 +4,7 @@ padding: 0; &::before { - @include notes-media('max', $screen-xs-min) { + @include notes-media('max', map-get($grid-breakpoints, xs)) { background: none; } } @@ -34,7 +34,7 @@ .timeline-entry-inner { position: relative; - @include notes-media('max', $screen-xs-min) { + @include notes-media('max', map-get($grid-breakpoints, xs)) { .timeline-icon { display: none; } diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss index 0cd83df218f..d5cc78a6680 100644 --- a/app/assets/stylesheets/framework/toggle.scss +++ b/app/assets/stylesheets/framework/toggle.scss @@ -121,7 +121,7 @@ cursor: not-allowed; } - @media (max-width: $screen-xs-min) { + @include media-breakpoint-down(xs) { width: 50px; &::before, diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss deleted file mode 100644 index 1c6e2bf3074..00000000000 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Twitter bootstrap with GitLab customizations/additions - * - */ - -// Core variables and mixins -@import "bootstrap/variables"; -@import "bootstrap/mixins"; - -// Reset -@import "bootstrap/normalize"; -@import "bootstrap/print"; - -// Core CSS -@import "bootstrap/scaffolding"; -@import "bootstrap/type"; -@import "bootstrap/code"; -@import "bootstrap/grid"; -@import "bootstrap/tables"; -@import "bootstrap/forms"; -@import "bootstrap/buttons"; - -// Components -@import "bootstrap/component-animations"; -// @import "bootstrap/dropdowns"; -@import "bootstrap/button-groups"; -@import "bootstrap/input-groups"; -@import "bootstrap/navs"; -@import "bootstrap/navbar"; -@import "bootstrap/breadcrumbs"; -@import "bootstrap/pagination"; -@import "bootstrap/pager"; -@import "bootstrap/labels"; -@import "bootstrap/badges"; -@import "bootstrap/alerts"; -@import "bootstrap/progress-bars"; -@import "bootstrap/list-group"; -@import "bootstrap/wells"; -@import "bootstrap/close"; -@import "bootstrap/panels"; - -// Components w/ JavaScript -@import "bootstrap/modals"; -@import "bootstrap/tooltip"; -@import "bootstrap/popovers"; - -// Utility classes -.clearfix { - @include clearfix(); -} - -.center-block { - @include center-block(); -} - -.pull-right { - float: right !important; -} - -.pull-left { - float: left !important; -} - -.hide { - display: none; -} - -.show { - display: block !important; -} - -.invisible { - visibility: hidden; -} - -.text-hide { - @include text-hide(); -} - -.hidden { - display: none !important; - visibility: hidden !important; -} - -.affix { - position: fixed; -} - -/* - * Fix <summary> elements on firefox - * See https://github.com/necolas/normalize.css/issues/640 - * and https://github.com/twbs/bootstrap/issues/21060 - * - */ -summary { - display: list-item; -} - -@import "bootstrap/responsive-utilities"; - -// Labels -.label { - padding: 4px 5px; - font-size: 12px; - font-style: normal; - font-weight: $gl-font-weight-normal; - display: inline-block; - - &.label-gray { - background-color: $label-gray-bg; - color: $gl-text-color; - text-shadow: none; - } - - &.label-inverse { - background-color: $label-inverse-bg; - } -} - - -/** - * fix to keep tooltips position in top navigation bar - * - */ -.navbar .nav > li { - position: relative; - white-space: nowrap; -} - -/** - * Add some extra stuff to panels - * - */ - -.panel { - box-shadow: none; - - .panel-body { - form, - pre { - margin: 0; - } - - .form-actions { - margin: -15px; - margin-top: 18px; - } - } - - .panel-footer { - .pagination { - margin: 0; - } - - .btn { - min-width: 124px; - } - - .btn-clipboard { - min-width: 0; - } - } - - &.panel-small { - .panel-heading { - padding: 6px 15px; - font-size: 13px; - font-weight: $gl-font-weight-normal; - - a { - color: $panel-heading-link-color; - } - } - } - - &.panel-without-border { - border: 0; - } - - &.panel-without-margin { - margin: 0; - } -} - -.panel-succes .panel-heading, -.panel-info .panel-heading, -.panel-danger .panel-heading, -.panel-warning .panel-heading, -.panel-primary .panel-heading, -.alert { - a:not(.btn) { - @extend .alert-link; - color: $white-light; - text-decoration: underline; - } -} - -// Prevent datetimes on tooltips to break into two lines -.local-timeago { - white-space: nowrap; -} diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss deleted file mode 100644 index d04e555769b..00000000000 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ /dev/null @@ -1,199 +0,0 @@ -// Override Bootstrap variables here (defaults from bootstrap-sass v3.3.3): -// For all variables see https://github.com/twbs/bootstrap-sass/blob/master/templates/project/_bootstrap-variables.sass -// -// Variables -// -------------------------------------------------- - - -//== Colors -// -//## Gray and brand colors for use across Bootstrap. - -// $gray-base: #000 -// $gray-darker: lighten($gray-base, 13.5%) // #222 -// $gray-dark: lighten($gray-base, 20%) // #333 -// $gray: lighten($gray-base, 33.5%) // #555 -// $gray-light: lighten($gray-base, 46.7%) // #777 -// $gray-lighter: lighten($gray-base, 93.5%) // #eee - -$brand-primary: $gl-primary; -$brand-success: $gl-success; -$brand-info: $gl-info; -$brand-warning: $gl-warning; -$brand-danger: $gl-danger; - -$border-radius-base: 3px !default; -$border-radius-large: 3px !default; -$border-radius-small: 3px !default; - - -//== Scaffolding -// -$text-color: $gl-text-color; -$link-color: $gl-link-color; -$link-hover-color: $gl-link-hover-color; - - -//== Typography -// -//## Font, line-height, and color for body text, headings, and more. - -$font-family-sans-serif: $regular_font; -$font-family-monospace: $monospace_font; -$font-size-base: $gl-font-size; - - -//== Components -// -//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start). - -$padding-base-vertical: $gl-vert-padding; -$padding-base-horizontal: $gl-padding; -$component-active-color: $white-light; -$component-active-bg: $brand-info; - -//== Forms -// -//## - -$input-color: $text-color; -$input-border: $border-color; -$input-border-focus: $focus-border-color; -$legend-color: $text-color; - - -//== Pagination -// -//## - -$pagination-color: $gl-text-color; -$pagination-bg: $white-light; -$pagination-border: $border-color; - -$pagination-hover-color: $gl-text-color; -$pagination-hover-bg: $row-hover; -$pagination-hover-border: $border-color; - -$pagination-active-color: $white-light; -$pagination-active-bg: $gl-link-color; -$pagination-active-border: $gl-link-color; - -$pagination-disabled-color: #cdcdcd; -$pagination-disabled-bg: $gray-light; -$pagination-disabled-border: $border-color; - - -//== Form states and alerts -// -//## Define colors for form feedback states and, by default, alerts. - -$state-success-text: $white-light; -$state-success-bg: $brand-success; -$state-success-border: $brand-success; - -$state-info-text: $white-light; -$state-info-bg: $brand-info; -$state-info-border: $brand-info; - -$state-warning-text: $white-light; -$state-warning-bg: $brand-warning; -$state-warning-border: $brand-warning; - -$state-danger-text: $white-light; -$state-danger-bg: $brand-danger; -$state-danger-border: $brand-danger; - - -//== Alerts -// -//## Define alert colors, border radius, and padding. - -$alert-border-radius: 0; - - -//== Panels -// -//## - -$panel-border-radius: 2px; -$panel-default-text: $text-color; -$panel-default-border: $border-color; -$panel-default-heading-bg: $gray-light; -$panel-footer-bg: $gray-light; -$panel-inner-border: $border-color; - -$badge-bg: $badge-bg; -$badge-color: $badge-color; - -//== Wells -// -//## - -$well-bg: $gray-light; -$well-border: #eee; - -//== Code -// -//## - -$code-color: $red-600; -$code-bg: lighten($red-100, 2%); - -$kbd-color: $white-light; -$kbd-bg: #333; - -//== Buttons -// -//## -$btn-default-color: $gl-text-color; -$btn-default-bg: $white-light; -$btn-default-border: #e7e9ed; - -//== Nav -// -//## -$nav-link-padding: 13px $gl-padding; - -//== Code -// -//## -$pre-bg: $gray-light !default; -$pre-color: $gl-text-color !default; -$pre-border-color: $border-color; - -$table-bg-accent: $gray-light; - -$zindex-popover: 900; - -//== Modals -// -//## - -//** Padding applied to the modal body -$modal-inner-padding: $gl-padding; - -//** Padding applied to the modal title -$modal-title-padding: $gl-padding; -//** Modal title line-height -// $modal-title-line-height: $line-height-base - -//** Background color of modal content area -$modal-content-bg: $gray-light; -$modal-body-bg: $white-light; -//** Modal content border color -// $modal-content-border-color: rgba(0,0,0,.2) -//** Modal content border color **for IE8** -// $modal-content-fallback-border-color: #999 - -//** Modal backdrop background color -// $modal-backdrop-bg: #000 -//** Modal backdrop opacity -// $modal-backdrop-opacity: .5 -//** Modal header border color -// $modal-header-border-color: #e5e5e5 -//** Modal footer border color -// $modal-footer-border-color: $modal-header-border-color - -$modal-lg: 860px; -$modal-md: 540px; -// $modal-sm: 300px diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 9e1371648ed..ed0bfbbe08b 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -114,7 +114,7 @@ font-size: 0.95em; } - blockquote { + .blockquote { color: $gl-grayish-blue; font-size: inherit; padding: 8px 24px; @@ -122,12 +122,12 @@ border-left: 3px solid $white-dark; } - blockquote:dir(rtl) { + .blockquote:dir(rtl) { border-left: 0; border-right: 3px solid $white-dark; } - blockquote p { + .blockquote p { color: $gl-grayish-blue !important; font-size: inherit; line-height: 1.5; @@ -321,6 +321,16 @@ h6 { /** CODE **/ pre { font-family: $monospace_font; + display: block; + padding: $gl-padding-8; + margin: 0 0 $gl-padding-8; + font-size: 13px; + word-break: break-all; + word-wrap: break-word; + color: $gl-text-color; + background-color: $gray-light; + border: 1px solid $border-color; + border-radius: $border-radius-small; } code { @@ -391,7 +401,7 @@ h4 { } .text-right-lg { - @media (min-width: $screen-lg-min) { + @include media-breakpoint-up(lg) { text-align: right; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index b5505538541..89b61530ddb 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -142,6 +142,11 @@ $border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor); $border-gray-dark: darken($white-normal, $darken-border-factor); /* + * Override Bootstrap 4 variables + */ +$secondary: $gray-light; + +/* * UI elements */ $border-color: #e5e5e5; @@ -230,7 +235,7 @@ $row-hover: $blue-50; $row-hover-border: $blue-200; $progress-color: #c0392b; $header-height: 40px; -$ide-statusbar-height: 27px; +$ide-statusbar-height: 25px; $fixed-layout-width: 1280px; $limited-layout-width: 990px; $limited-layout-width-sm: 790px; @@ -306,6 +311,11 @@ $gl-warning: $orange-500; $gl-danger: $red-500; $gl-btn-active-background: rgba(0, 0, 0, 0.16); $gl-btn-active-gradient: inset 0 2px 3px $gl-btn-active-background; +// Bootstrap override states +$success: $gl-success; +$info: $gl-info; +$warning: $gl-warning; +$danger: $gl-danger; /* * Commit Diff Colors @@ -554,6 +564,8 @@ $input-danger-border: $red-400; $input-group-addon-bg: #f7f8fa; $gl-field-focus-shadow: rgba(0, 0, 0, 0.075); $gl-field-focus-shadow-error: rgba($red-500, 0.6); +$input-short-width: 200px; +$input-short-md-width: 280px; /* * Help diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss index 3fa7a260017..514fac82b1e 100644 --- a/app/assets/stylesheets/framework/wells.scss +++ b/app/assets/stylesheets/framework/wells.scss @@ -5,7 +5,7 @@ border-radius: $border-radius-default; margin-bottom: $gl-padding; - .well-segment { + .card.card-body-segment { padding: $gl-padding; &:not(:last-of-type) { @@ -59,7 +59,7 @@ } } - .label.label-gray { + .label-gray { background-color: $well-expand-item; } @@ -108,7 +108,7 @@ } } -.well-centered { +.card.card-body-centered { h1 { font-weight: $gl-font-weight-normal; text-align: center; diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss index 658ac26fca9..b5eda79e5ed 100644 --- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss +++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss @@ -138,7 +138,7 @@ pre { margin: 0; } -blockquote { +.blockquote { color: $gl-grayish-blue; padding: 0 0 0 15px; margin: 0; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 681242f8d85..1c3d312f7ac 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -1,5 +1,3 @@ -@import './issues/issue_count_badge'; - [v-cloak] { display: none; } @@ -60,7 +58,7 @@ .boards-app { position: relative; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { transition: width $sidebar-transition-duration; width: 100%; @@ -83,11 +81,11 @@ white-space: nowrap; min-height: 200px; - @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + @include media-breakpoint-only(sm) { height: calc(100vh - #{$issue-board-list-difference-sm}); } - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { height: calc(100vh - #{$issue-board-list-difference-md}); } @@ -96,13 +94,13 @@ 100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height} ); - @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + @include media-breakpoint-only(sm) { height: calc( 100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height} ); } - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { height: calc( 100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height} ); @@ -119,7 +117,7 @@ white-space: normal; vertical-align: top; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: 400px; } @@ -276,7 +274,7 @@ font-size: (26px / $issue-boards-font-size) * 1em; } -.card { +.board-card { position: relative; padding: 11px 10px 11px $gl-padding; background: $white-light; @@ -284,12 +282,15 @@ box-shadow: 0 1px 2px $issue-boards-card-shadow; list-style: none; + // as a fallback, hide overflow content so that dragging and dropping still works + overflow: hidden; + &:not(:last-child) { margin-bottom: 5px; } &.is-active, - &.is-active .card-assignee:hover a { + &.is-active .board-card-assignee:hover a { background-color: $row-hover; &:first-child:not(:only-child) { @@ -297,7 +298,7 @@ } } - .label { + .badge { border: 0; outline: 0; } @@ -309,24 +310,23 @@ } } -.card-title { +.board-card-title { + @include overflow-break-word(); margin: 0 30px 0 0; font-size: 1em; line-height: inherit; a { color: $gl-text-color; - word-wrap: break-word; - word-break: break-word; margin-right: 2px; } } -.card-header { +.board-card-header { display: flex; min-height: 20px; - .card-assignee { + .board-card-assignee { display: flex; justify-content: flex-end; position: absolute; @@ -397,16 +397,16 @@ } } -.card-footer { +.board-card-footer { margin: 0 0 5px; - .label { + .badge { margin-top: 5px; margin-right: 6px; } } -.card-number { +.board-card-number { font-size: 12px; color: $gl-text-color-secondary; } @@ -462,8 +462,8 @@ } .issuable-header-text { + @include overflow-break-word(); padding-right: 35px; - word-break: break-word; > strong { font-weight: $gl-font-weight-bold; @@ -564,11 +564,11 @@ .add-issues-list-column { width: 100%; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: 50%; } - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { width: (100% / 3); } } @@ -583,11 +583,11 @@ margin-right: -$gl-vert-padding; overflow-y: scroll; - .card-parent { + .board-card-parent { padding: 0 5px 5px; } - .card { + .board-card { border: 1px solid $border-gray-dark; box-shadow: 0 1px 2px rgba($issue-boards-card-shadow, 0.3); cursor: pointer; @@ -637,7 +637,7 @@ display: none; margin-right: 10px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: block; } } @@ -645,7 +645,7 @@ .dropdown-menu-toggle { width: 100px; - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { width: 140px; } } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 50f32660445..9ee02ca1d83 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -215,7 +215,7 @@ } .header-action-buttons { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .sidebar-toggle-btn { margin-top: 0; margin-left: 10px; @@ -277,10 +277,6 @@ &.coverage { padding: 0 16px 11px; } - - .btn-group-justified { - margin-top: 5px; - } } .block-last { @@ -305,7 +301,7 @@ background-color: $white-light; } - .label { + .badge.badge-pill { margin-left: 2px; } @@ -320,7 +316,7 @@ } } - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { display: block; .btn { diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index 3fd13078131..2b6d92016d5 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -63,7 +63,7 @@ text-decoration: none; } - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { > div { display: flex; align-items: center; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 944996159d7..a4ca82de90e 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -23,7 +23,7 @@ } .commit-hash-full { - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { width: 80px; white-space: nowrap; overflow: hidden; @@ -183,7 +183,7 @@ } .commit-actions { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { .fa-spinner { font-size: 12px; } @@ -327,8 +327,12 @@ } &.invalid { - @include status-color($gray-dark, $gray, $gray-darkest); + @include status-color($gray-dark, color("gray"), $gray-darkest); border-color: $gray-darkest; + + &:not(span):hover { + color: color("gray"); + } } } diff --git a/app/assets/stylesheets/pages/convdev_index.scss b/app/assets/stylesheets/pages/convdev_index.scss index fb1899284fd..bd338326154 100644 --- a/app/assets/stylesheets/pages/convdev_index.scss +++ b/app/assets/stylesheets/pages/convdev_index.scss @@ -53,19 +53,19 @@ $space-between-cards: 8px; padding: $space-between-cards / 2; position: relative; - @media (min-width: $screen-xs-min) { + @include media-breakpoint-up(xs) { width: percentage(1 / 4); } - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: percentage(1 / 5); } - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { width: percentage(1 / 6); } - @media (min-width: $screen-lg-min) { + @include media-breakpoint-up(lg) { width: percentage(1 / 10); } } @@ -82,7 +82,7 @@ $space-between-cards: 8px; .convdev-card-low { border-top-color: $color-low-score; - .card-score-big { + .board-card-score-big { background-color: $red-50; } } @@ -90,7 +90,7 @@ $space-between-cards: 8px; .convdev-card-average { border-top-color: $color-average-score; - .card-score-big { + .board-card-score-big { background-color: $orange-50; } } @@ -98,7 +98,7 @@ $space-between-cards: 8px; .convdev-card-high { border-top-color: $color-high-score; - .card-score-big { + .board-card-score-big { background-color: $green-50; } } @@ -112,14 +112,14 @@ $space-between-cards: 8px; margin: 0 0 2px; } - .text-light { + .light-text { font-size: 13px; line-height: 1.25; color: $gl-text-color-secondary; } } -.card-scores { +.board-card-scores { display: flex; justify-content: space-around; align-items: center; @@ -127,22 +127,22 @@ $space-between-cards: 8px; line-height: 1; } -.card-score { +.board-card-score { color: $gl-text-color-secondary; - .card-score-name { + .board-card-score-name { font-size: 13px; margin-top: 4px; } } -.card-score-value { +.board-card-score-value { font-size: 16px; color: $gl-text-color; font-weight: $gl-font-weight-normal; } -.card-score-big { +.board-card-score-big { border-top: 2px solid $border-color; border-bottom: 1px solid $border-color; font-size: 22px; @@ -150,7 +150,7 @@ $space-between-cards: 8px; font-weight: $gl-font-weight-normal; } -.card-buttons { +.board-card-buttons { display: flex; > * { diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index cfef6476d4d..a22c666a525 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -15,7 +15,7 @@ max-width: 480px; padding: 0 $gl-padding; - @media (max-width: $screen-sm-min) { + @include media-breakpoint-down(sm) { margin: 0 auto; } } @@ -75,13 +75,13 @@ } } - .panel { + .card { .content-block { padding: 24px 0; border-bottom: 0; position: relative; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { padding: 6px 0 24px; } } @@ -89,7 +89,7 @@ .column { text-align: center; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { padding: 15px 0; } @@ -106,7 +106,7 @@ } &:last-child { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { text-align: center; } } @@ -213,7 +213,7 @@ .stage-panel { min-width: 968px; - .panel-heading { + .card-header { padding: 0; background-color: transparent; } @@ -266,7 +266,9 @@ &.issue-title, &.commit-title, &.merge-merquest-title { - @include text-overflow(); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; max-width: 100%; display: block; diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 2f2c04206e2..7b36bcb3c7d 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -14,7 +14,7 @@ white-space: nowrap; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { display: block; } } @@ -25,7 +25,7 @@ display: flex; flex-grow: 1; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { padding-left: 0; padding-right: 0; } @@ -36,7 +36,7 @@ flex-shrink: 0; flex: 0 0 auto; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { width: 100%; margin-top: 10px; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 70ce5de6a6c..f06c9dcdf8c 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -86,13 +86,13 @@ &.left-side-selected { td.line_content.parallel.right-side { - @include user-select(none); + user-select: none; } } &.right-side-selected { td.line_content.parallel.left-side { - @include user-select(none); + user-select: none; } } } @@ -109,7 +109,7 @@ .old_line, .new_line { - @include user-select(none); + user-select: none; margin: 0; border: 0; padding: 0 5px; @@ -592,14 +592,14 @@ } .commit-stat-summary { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { margin-left: -$gl-padding; padding-left: $gl-padding; background-color: $white-light; } } - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { position: -webkit-sticky; position: sticky; top: 24px; @@ -610,10 +610,6 @@ top: 76px; } - &:not(.is-stuck) .diff-stats-additions-deletions-collapsed { - display: none; - } - &.is-stuck { padding-top: 0; padding-bottom: 0; @@ -622,13 +618,21 @@ .diff-stats-additions-deletions-expanded, .inline-parallel-buttons { - display: none; + display: none !important; + } + } + } + + @include media-breakpoint-up(lg) { + &.is-stuck { + .diff-stats-additions-deletions-collapsed { + display: block !important; } } } } -@media (min-width: $screen-sm-min) { +@include media-breakpoint-up(sm) { .with-performance-bar { .diff-files-changed.diff-files-changed-merge-request { top: 76px + $performance-bar-height; @@ -641,7 +645,7 @@ width: 100%; z-index: 150; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { left: $gl-padding; } @@ -772,9 +776,9 @@ } } -.frame .badge, -.image-diff-avatar-link .badge, -.notes > .badge { +.frame .badge.badge-pill, +.image-diff-avatar-link .badge.badge-pill, +.notes > .badge.badge-pill { position: absolute; background-color: $blue-400; color: $white-light; @@ -788,7 +792,7 @@ } } -.frame .badge, +.frame .badge.badge-pill, .frame .image-comment-badge { // Center align badges on the frame transform: translate(-50%, -50%); @@ -811,14 +815,14 @@ .image-diff-avatar-link { position: relative; - .badge, + .badge.badge-pill, .image-comment-badge { top: 25px; right: 8px; } } -.notes > .badge { +.notes > .badge.badge-pill { display: none; left: -13px; } @@ -840,7 +844,7 @@ display: none; } - .notes > .badge { + .notes > .badge.badge-pill { display: block; } } diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 8ecda50602d..437621299e0 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -37,7 +37,7 @@ padding-top: 7px; padding-bottom: 7px; - .pull-right { + .float-right { height: 20px; } } @@ -63,11 +63,11 @@ max-width: 450px; float: left; - @media(max-width: $screen-md-max) { + @media(max-width: map-get($grid-breakpoints, lg)-1) { width: 280px; } - @media(max-width: $screen-sm-max) { + @media(max-width: map-get($grid-breakpoints, md)-1) { width: 180px; } } @@ -111,10 +111,10 @@ } -@media(max-width: $screen-xs-max){ +@include media-breakpoint-down(xs) { .file-editor { .file-title { - .pull-right { + .float-right { height: auto; } } @@ -153,7 +153,7 @@ vertical-align: top; display: inline-block; - @media(max-width: $screen-sm-max) { + @media(max-width: map-get($grid-breakpoints, md)-1) { display: block; margin: 19px 0 12px; } @@ -166,7 +166,7 @@ padding: 0 0 0 14px; border-left: 1px solid $border-color; - @media(max-width: $screen-sm-max) { + @media(max-width: map-get($grid-breakpoints, md)-1) { display: block; width: 100%; margin: 5px 0; @@ -181,7 +181,7 @@ margin-top: 6px; line-height: 21px; - @media(max-width: $screen-sm-max) { + @media(max-width: map-get($grid-breakpoints, md)-1) { display: block; margin: 5px 0; } @@ -193,7 +193,7 @@ vertical-align: top; margin: 5px 0 0 8px; - @media(max-width: $screen-sm-max) { + @media(max-width: map-get($grid-breakpoints, md)-1) { display: block; width: 100%; margin: 0 0 16px; @@ -209,7 +209,7 @@ font-family: $regular_font; margin-top: -5px; - @media(max-width: $screen-sm-max) { + @media(max-width: map-get($grid-breakpoints, md)-1) { display: block; width: 100%; margin: 5px 0; @@ -223,7 +223,7 @@ width: 250px; vertical-align: top; - @media(max-width: $screen-sm-max) { + @media(max-width: map-get($grid-breakpoints, md)-1) { display: block; width: 100%; margin: 5px 0; @@ -237,7 +237,7 @@ display: inline-block; margin: 7px 0 0 10px; - @media(max-width: $screen-sm-max) { + @media(max-width: map-get($grid-breakpoints, md)-1) { display: block; width: 100%; margin: 20px 0; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 1f406cc1c2d..cd0d67613c3 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -1,4 +1,4 @@ -@media (max-width: $screen-md-max) { +@include media-breakpoint-down(md) { .deployments-container { width: 100%; overflow: auto; @@ -138,7 +138,7 @@ border-left: 0; border-right: 0; - @media (min-width: $screen-sm-max) { + @media (min-width: map-get($grid-breakpoints, md)-1) { border-top: 0; } } @@ -251,7 +251,7 @@ font-size: 16px; } - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { min-width: 100%; } } @@ -432,7 +432,7 @@ } } - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { .label-axis-text, .text-metric-usage, .legend-axis-text { diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index d9267f5cdf3..f79586b68b9 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -70,7 +70,7 @@ .md { font-size: $gl-font-size; - .label { + .badge.badge-pill { color: $gl-text-color; } @@ -173,7 +173,7 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .event-item { padding-left: 0; diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 6ee8b33bd39..c2b42e02eee 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -2,7 +2,7 @@ @include str-truncated(90%); } -.dashboard .side .panel .panel-heading .input-group { +.dashboard .side .card .card-header .input-group { .form-control { height: 42px; @@ -18,6 +18,10 @@ .group-row { @include basic-list-stats; + + .description p { + margin-bottom: 0; + } } .ldap-group-links { @@ -36,7 +40,7 @@ flex: 1; } - .dropdown-menu-align-right { + .dropdown-menu-right { margin-top: 0; } @@ -98,7 +102,7 @@ } } - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { &, .dropdown, .dropdown .dropdown-toggle, @@ -145,14 +149,14 @@ padding: 50px 100px; overflow: hidden; - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { padding: 50px 0; } svg { float: right; - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { float: none; display: block; width: 250px; @@ -167,7 +171,7 @@ width: 460px; margin-top: 120px; - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { float: none; margin-top: 60px; width: auto; @@ -201,7 +205,7 @@ max-width: 480px; padding: 0 $gl-padding; - @media (max-width: $screen-sm-min) { + @include media-breakpoint-down(sm) { margin: 0 auto; } } @@ -237,3 +241,231 @@ overflow-y: unset; } } + +.groups-list-tree-container { + .has-no-search-results { + text-align: center; + padding: $gl-padding; + font-style: italic; + color: $well-light-text-color; + } + + > .group-list-tree > .group-row.has-children:first-child { + border-top: 0; + } +} + +.group-list-tree { + .avatar-container.content-loading { + position: relative; + + > a, + > a .avatar { + height: 100%; + border-radius: 50%; + } + + > a { + padding: 2px; + + .avatar { + border: 2px solid $white-normal; + + &.identicon { + line-height: 15px; + } + } + } + + &::after { + content: ""; + position: absolute; + height: 100%; + width: 100%; + background-color: transparent; + border: 2px outset $kdb-border; + border-radius: 50%; + animation: spin-avatar 3s infinite linear; + } + } + + .folder-toggle-wrap { + float: left; + line-height: $list-text-height; + font-size: 0; + + span { + font-size: $gl-font-size; + } + } + + .folder-caret, + .item-type-icon { + display: inline-block; + } + + .folder-caret { + width: 15px; + + svg { + margin-bottom: 2px; + } + } + + .item-type-icon { + margin-top: 2px; + width: 20px; + } + + > .group-row:not(.has-children) { + .folder-caret { + opacity: 0; + } + } + + .content-list li:last-child { + padding-bottom: 0; + } + + .group-list-tree { + margin-bottom: 0; + margin-left: 30px; + position: relative; + + &::before { + content: ''; + display: block; + width: 0; + position: absolute; + top: 5px; + bottom: 0; + left: -16px; + border-left: 2px solid $border-white-normal; + } + + .group-row { + position: relative; + + &::before { + content: ""; + display: block; + width: 10px; + height: 0; + border-top: 2px solid $border-white-normal; + position: absolute; + top: 30px; + left: -16px; + } + + &:last-child::before { + background: $white-light; + height: auto; + top: 30px; + bottom: 0; + } + + &.being-removed { + opacity: 0.5; + } + } + } + + .group-row { + padding: 0; + + &.has-children { + border-top: 0; + } + + &:first-child { + border-top: 1px solid $white-normal; + } + } + + .group-row-contents { + padding: $gl-padding-top; + + &:hover { + border-color: $row-hover-border; + background-color: $row-hover; + cursor: pointer; + } + + .avatar-container > a { + width: 100%; + text-decoration: none; + } + + &.has-more-items { + display: block; + padding: 20px 10px; + } + + .stats { + position: relative; + line-height: 46px; + + > span { + display: inline-flex; + align-items: center; + height: 16px; + min-width: 30px; + } + + > span:last-child { + margin-right: 0; + } + + .stat-value { + margin: 2px 0 0 5px; + } + } + + .controls { + margin-left: 5px; + + > .btn { + margin-right: $btn-xs-side-margin; + } + } + } + + .project-row-contents .stats { + line-height: inherit; + + > span:first-child { + margin-left: 25px; + } + + .item-visibility { + margin-right: 0; + } + + .last-updated { + position: absolute; + right: 12px; + min-width: 250px; + text-align: right; + color: $gl-text-color-secondary; + } + } +} + +ul.group-list-tree { + li.group-row { + > .group-row-contents .title { + line-height: $list-text-height; + } + + &.has-description > .group-row-contents .title { + line-height: inherit; + } + } +} + +.js-groups-list-holder { + .groups-list-loading { + font-size: 34px; + text-align: center; + } +} diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index 9cc9e11bcd1..0350fe5752e 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -28,8 +28,8 @@ } .key { - @extend .label; - @extend .label-inverse; + @extend .badge.badge-pill; + background-color: $label-inverse-bg; font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace; padding: 3px 5px; } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index b2dad4a358a..4aea9740735 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -197,9 +197,21 @@ } &.assignee { - .author_link:hover { - .author { - text-decoration: underline; + .author_link { + display: block; + padding-left: 42px; + position: relative; + + &:hover { + .author { + text-decoration: underline; + } + } + + .avatar { + left: 0; + position: absolute; + top: 0; } } } @@ -361,7 +373,7 @@ /* Extra small devices (phones, less than 768px) */ display: none; /* Small devices (tablets, 768px and up) */ - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: block; } @@ -644,7 +656,7 @@ } .issuable-form-padding-top { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { padding-top: 7px; } } @@ -658,7 +670,7 @@ padding-left: 9px; padding-right: 9px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: inline-block; height: auto; align-self: center; @@ -666,7 +678,7 @@ } .issuable-gutter-toggle { - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { margin-left: $btn-side-margin; } } @@ -684,7 +696,7 @@ width: 100%; } - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { margin-bottom: $gl-padding; } } @@ -725,7 +737,7 @@ } } - @media(max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .issuable-meta { .controls li { margin-right: 0; @@ -760,7 +772,7 @@ } } - @media(max-width: $screen-md-max) { + @media(max-width: map-get($grid-breakpoints, lg)-1) { .task-status, .issuable-due-date, .project-ref-path { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index b9390450477..19fb99bfa93 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -1,5 +1,3 @@ -@import "./issues/issue_count_badge"; - .issues-list { .issue { padding: 10px 0 10px $gl-padding; @@ -143,7 +141,7 @@ ul.related-merge-requests > li { } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .detail-page-header { .issuable-meta { line-height: 18px; @@ -205,7 +203,7 @@ ul.related-merge-requests > li { } } - .btn-group:not(.hide) { + .btn-group:not(.hidden) { display: flex; } @@ -247,7 +245,7 @@ ul.related-merge-requests > li { display: block; } -@media (min-width: $screen-sm-min) { +@include media-breakpoint-up(sm) { .emoji-block .row { display: flex; @@ -258,8 +256,8 @@ ul.related-merge-requests > li { } .create-mr-dropdown-wrap { - .btn-group:not(.hide) { - display: inline-block; + .btn-group:not(.hidden) { + display: inline-flex; } } } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index d81236c5883..e178371d21f 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -62,13 +62,13 @@ display: inline-block; margin-bottom: 10px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: 200px; margin-left: $gl-padding * 2; margin-bottom: 0; } - .label { + .badge { overflow: hidden; text-overflow: ellipsis; max-width: 100%; @@ -80,7 +80,7 @@ margin-bottom: 10px; margin-left: 50px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: inline-block; width: 100px; margin-left: 10px; @@ -101,7 +101,7 @@ color: $blue-600; } - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: inline-block; max-width: 50%; margin-left: 10px; @@ -110,7 +110,7 @@ } } - .label { + .badge { padding: 4px $grid-size; font-size: $label-font-size; position: relative; @@ -122,6 +122,7 @@ padding: 0 $grid-size; line-height: 16px; border-radius: $label-border-radius; + color: $white-light; } .dropdown-labels-error { @@ -132,7 +133,7 @@ } .manage-labels-list { - @media(min-width: $screen-md-min) { + @media(min-width: map-get($grid-breakpoints, md)) { &.content-list li { padding: $gl-padding 0; } @@ -171,12 +172,12 @@ } .dropdown { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { float: right; } } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .dropdown-menu { min-width: 100%; } @@ -283,7 +284,7 @@ } .label-subscribe-button { - @media(min-width: $screen-md-min) { + @media(min-width: map-get($grid-breakpoints, md)) { min-width: 105px; margin-left: $gl-padding; } diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 97303d02666..c1b1d2e028d 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -191,9 +191,9 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .login-page { - .col-sm-5.pull-right { + .col-md-5.float-right { float: none !important; margin-bottom: 45px; } @@ -243,7 +243,7 @@ .navless-container { padding: 65px 15px; // height of footer + bottom padding of email confirmation link - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { padding: 0 15px 65px; } } diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index 3422829de58..de2b5701e2d 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -9,9 +9,13 @@ } } +.member-sort-dropdown { + margin-left: $gl-padding-8; +} + .member { .list-item-name { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { float: left; width: 50%; } @@ -22,12 +26,12 @@ } .controls { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: -webkit-flex; display: flex; } - .dropdown-menu.dropdown-menu-align-right { + .dropdown-menu.dropdown-menu-right { margin-top: -2px; } } @@ -35,7 +39,7 @@ .form-horizontal { margin-top: 20px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: -webkit-flex; display: flex; margin-top: 3px; @@ -45,20 +49,20 @@ .btn-remove { width: 100%; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: auto; } } &.existing-title { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { float: left; } } } .member-form-control { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { padding-bottom: 5px; margin-left: 0; margin-right: 0; @@ -73,7 +77,7 @@ .member-search-form { position: relative; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { float: right; } @@ -86,7 +90,7 @@ width: 100%; } - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { margin-top: 0; width: 155px; } @@ -96,19 +100,9 @@ width: 100%; padding-right: 35px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: 250px; } - - &.input-short { - @media (min-width: $screen-md-min) { - width: 170px; - } - - @media (min-width: $screen-lg-min) { - width: 210px; - } - } } } @@ -124,7 +118,7 @@ border: 0; outline: 0; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { right: 160px; } } @@ -135,7 +129,7 @@ align-items: center; justify-content: center; - @media (max-width: $screen-sm-min) { + @include media-breakpoint-down(sm) { display: block; .flex-project-title { @@ -151,7 +145,7 @@ text-overflow: ellipsis; } - .badge { + .badge.badge-pill { height: 17px; line-height: 16px; margin-right: 5px; @@ -166,14 +160,14 @@ } } -.panel { - .panel-heading { - .badge { +.card { + .card-header { + .badge.badge-pill { margin-top: 0; } - @media (max-width: $screen-sm-min) { - .badge { + @include media-breakpoint-down(sm) { + .badge.badge-pill { margin-right: 0; margin-left: 0; } @@ -217,7 +211,7 @@ margin-right: 0; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { display: block; .controls > .btn { @@ -245,7 +239,7 @@ } } -.panel-mobile { +.card-mobile { .content-list.members-list li { display: block; diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index 04bde64c752..e76525fdbf6 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -286,6 +286,14 @@ $colors: ( } .resolve-conflicts-form { - padding-top: $gl-padding; + h4 { + margin-top: 0; + } + + .resolve-info { + @media(max-width: map-get($grid-breakpoints, lg)-1) { + margin-bottom: $gl-padding; + } + } } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 3581dd36a10..9eceb3e9a33 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -131,7 +131,7 @@ color: $gl-text-color; display: flex; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { flex-wrap: wrap; } } @@ -282,7 +282,7 @@ display: inline-block; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { p { font-size: 13px; } @@ -494,18 +494,18 @@ } } -.panel-new-merge-request { - .panel-heading { +.card-new-merge-request { + .card-header { padding: 5px 10px; font-weight: $gl-font-weight-bold; line-height: 25px; } - .panel-body { + .card-body { padding: 10px 5px; } - .panel-footer { + .card-footer { padding: 0; .btn { @@ -519,7 +519,7 @@ } .item-title { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: 45%; } } @@ -550,7 +550,7 @@ margin-bottom: 0; } - @media (min-width: $screen-xs-min) { + @include media-breakpoint-up(xs) { float: left; width: 50%; margin-bottom: 0; @@ -648,7 +648,7 @@ background-color: $white-light; border-bottom: 1px solid $border-color; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { position: sticky; position: -webkit-sticky; } @@ -657,7 +657,7 @@ left: 0; transition: right .15s; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { right: 0; } @@ -700,7 +700,7 @@ display: flex; justify-content: space-between; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { flex-direction: column-reverse; } } @@ -736,7 +736,7 @@ display: flex; flex-wrap: wrap; - @media (min-width: $screen-xs) { + @include media-breakpoint-up(xs) { flex-wrap: nowrap; white-space: nowrap; } @@ -753,7 +753,7 @@ min-width: 100px; max-width: 150px; - @media (min-width: $screen-xs) { + @include media-breakpoint-up(xs) { min-width: 0; max-width: 100%; } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index bac3b70c734..dba83e56d72 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -31,7 +31,7 @@ } } - .panel-heading { + .card-header { line-height: $line-height-base; padding: 14px 16px; display: -webkit-flex; @@ -145,7 +145,7 @@ padding: 20px 0; } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .milestone-actions { @include clearfix(); padding-top: $gl-vert-padding; @@ -181,7 +181,7 @@ width: 100%; } - @media (min-width: $screen-xs-min) { + @include media-breakpoint-up(xs) { .milestone-buttons .verbose { display: inline; } @@ -229,7 +229,7 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .milestone-banner-text, .milestone-banner-link { display: inline; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 4a528bc2bb1..3b037d066dc 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -51,7 +51,7 @@ } .note-image-attach { - @extend .col-md-4; + @extend .col-lg-4; margin-left: 45px; float: none; } @@ -93,7 +93,7 @@ -webkit-flex-flow: row wrap; width: 100%; - .pull-right { + .float-right { // Flexbox quirk to make sure right-aligned items stay right-aligned. margin-left: auto; } @@ -185,12 +185,12 @@ } .notes.notes-form > li.timeline-entry { - @include notes-media('max', $screen-sm-max) { + @include notes-media('max', map-get($grid-breakpoints, md) - 1) { padding: 0; } .timeline-content { - @include notes-media('max', $screen-sm-max) { + @include notes-media('max', map-get($grid-breakpoints, md) - 1) { margin: 0; } } @@ -326,7 +326,7 @@ outline: 0; } - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { float: left; margin-right: $gl-padding; @@ -350,13 +350,13 @@ line-height: 16px; margin-top: 2px; - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { float: left; } } .note-form-actions { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .btn { float: none; width: 100%; @@ -375,7 +375,7 @@ left: 127px; top: 2px; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { position: relative; top: 0; left: 0; @@ -410,7 +410,7 @@ width: 298px; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { display: flex; width: 100%; margin-bottom: 10px; @@ -432,7 +432,7 @@ .uploading-container { float: right; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { float: left; margin-top: 5px; } @@ -444,7 +444,7 @@ } .uploading-error-message { - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { &::after { content: "\a"; white-space: pre; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 6d5c6cb136f..299eda53140 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -22,7 +22,7 @@ ul.notes { .discussion-body { padding-top: 8px; - .panel { + .card { margin-bottom: 0; } } @@ -42,7 +42,7 @@ ul.notes { position: relative; border-bottom: 0; - @include notes-media('min', $screen-sm-min) { + @include notes-media('min', map-get($grid-breakpoints, sm)) { padding-left: $note-icon-gutter-width; } @@ -66,7 +66,7 @@ ul.notes { } .timeline-icon { - @include notes-media('min', $screen-sm-min) { + @include notes-media('min', map-get($grid-breakpoints, sm)) { margin-left: -$note-icon-gutter-width; } } @@ -74,7 +74,7 @@ ul.notes { .timeline-content { margin-left: $note-icon-gutter-width; - @include notes-media('min', $screen-sm-min) { + @include notes-media('min', map-get($grid-breakpoints, sm)) { margin-left: 0; } } @@ -154,7 +154,7 @@ ul.notes { .note-header { - @include notes-media('max', $screen-xs-min) { + @include notes-media('max', map-get($grid-breakpoints, xs)) { .inline { display: block; } @@ -217,7 +217,7 @@ ul.notes { .timeline-icon { float: left; - @include notes-media('min', $screen-sm-min) { + @include notes-media('min', map-get($grid-breakpoints, sm)) { margin-left: 0; width: auto; } @@ -231,7 +231,7 @@ ul.notes { } .timeline-content { - @include notes-media('min', $screen-sm-min) { + @include notes-media('min', map-get($grid-breakpoints, sm)) { margin-left: 30px; } } @@ -407,10 +407,6 @@ ul.notes { .note-header { display: flex; justify-content: space-between; - - @include notes-media('max', $screen-xs-max) { - flex-flow: row wrap; - } } .note-header-info { @@ -427,7 +423,7 @@ ul.notes { } .note-header-author-name { - @include notes-media('max', $screen-xs-max) { + @include notes-media('max', map-get($grid-breakpoints, sm) - 1) { display: none; } } @@ -435,7 +431,7 @@ ul.notes { .note-headline-light { display: inline; - @include notes-media('max', $screen-xs-min) { + @include notes-media('max', map-get($grid-breakpoints, xs)) { display: block; } } @@ -459,6 +455,10 @@ ul.notes { white-space: normal; } + .system-note-separator { + color: $gl-text-color-disabled; + } + a:hover { text-decoration: underline; } @@ -473,11 +473,6 @@ ul.notes { margin-left: 10px; color: $gray-darkest; - @include notes-media('max', $screen-md-max) { - float: none; - margin-left: 0; - } - .btn-group > .discussion-next-btn { margin-left: -1px; } @@ -491,7 +486,7 @@ ul.notes { margin-left: 10px; color: $gray-darkest; - @include notes-media('max', $screen-xs-max) { + @include notes-media('max', map-get($grid-breakpoints, sm) - 1) { float: none; margin-left: 0; } @@ -639,6 +634,10 @@ ul.notes { margin-left: -55px; position: absolute; z-index: 10; + + .new & { + margin-top: -10px; + } } .discussion-body, @@ -673,7 +672,7 @@ ul.notes { } .line-resolve-all-container { - @include notes-media('min', $screen-sm-min) { + @include notes-media('min', map-get($grid-breakpoints, sm)) { margin-right: 0; padding-left: $gl-padding; } diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss index bdf07a99daf..e98cb444f0a 100644 --- a/app/assets/stylesheets/pages/notifications.scss +++ b/app/assets/stylesheets/pages/notifications.scss @@ -2,7 +2,7 @@ line-height: 34px; .dropdown-menu { - @extend .dropdown-menu-align-right; + @extend .dropdown-menu-right; } } diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss index bc7fa8a26d9..86e70955389 100644 --- a/app/assets/stylesheets/pages/pipeline_schedules.scss +++ b/app/assets/stylesheets/pages/pipeline_schedules.scss @@ -69,7 +69,7 @@ .cron-preset-radio-input { display: inline-block; - @media (max-width: $screen-md-max) { + @include media-breakpoint-down(md) { display: block; margin: 0 0 5px 5px; } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 02803e7b040..0188a55cbf3 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -16,13 +16,13 @@ margin: 0; white-space: normal; - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { justify-content: flex-end; } } .ci-table { - .label { + .badge { margin-bottom: 3px; } @@ -66,13 +66,9 @@ } } - .btn-group { - &.open { - .btn-default { - background-color: $white-normal; - border-color: $border-white-normal; - } - } + .btn-group.open .btn-default { + background-color: $white-normal; + border-color: $border-white-normal; } .btn .text-center { @@ -86,7 +82,7 @@ } } -@media (max-width: $screen-md-max) { +@include media-breakpoint-down(md) { .content-list { &.builds-content-list { width: 100%; @@ -154,14 +150,14 @@ color: $gl-link-color; } - .label { + .badge { margin-right: 4px; } .label-container { font-size: 0; - .label { + .badge { margin-top: 5px; } } @@ -230,7 +226,7 @@ .stage-cell { &.table-section { - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { min-width: 160px; /* Hack alert: Without this the mini graph pipeline won't work properly*/ margin-right: -4px; } @@ -361,16 +357,14 @@ &:not(:first-child) { margin-left: 44px; - .left-connector { - &::before { - content: ''; - position: absolute; - top: 48%; - left: -44px; - border-top: 2px solid $border-color; - width: 44px; - height: 1px; - } + .left-connector::before { + content: ''; + position: absolute; + top: 48%; + left: -44px; + border-top: 2px solid $border-color; + width: 44px; + height: 1px; } } } @@ -386,22 +380,16 @@ &:last-child { .build { // Remove right connecting horizontal line from first build in last stage - &:first-child { - &::after { - border: 0; - } + &:first-child::after { + border: 0; } // Remove right curved connectors from all builds in last stage - &:not(:first-child) { - &::after { - border: 0; - } + &:not(:first-child)::after { + border: 0; } // Remove opposite curve - .curve { - &::before { - display: none; - } + .curve::before { + display: none; } } } @@ -409,16 +397,12 @@ &:first-child { .build { // Remove left curved connectors from all builds in first stage - &:not(:first-child) { - &::before { - border: 0; - } + &:not(:first-child)::before { + border: 0; } // Remove opposite curve - .curve { - &::after { - display: none; - } + .curve::after { + display: none; } } } @@ -816,7 +800,7 @@ button.mini-pipeline-graph-dropdown-toggle { display: block; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { max-width: 60%; } } @@ -903,7 +887,7 @@ button.mini-pipeline-graph-dropdown-toggle { transform: translate(-50%, 0); border-width: 0 5px 6px; - @media (max-width: $screen-sm-max) { + @include media-breakpoint-down(sm) { left: 100%; margin-left: -12px; } @@ -925,7 +909,7 @@ button.mini-pipeline-graph-dropdown-toggle { &.dropdown-menu { transform: translate(-80%, 0); - @media (min-width: $screen-md-min) { + @media(min-width: map-get($grid-breakpoints, md)) { transform: translate(-50%, 0); right: auto; left: 50%; diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index b199f9876d3..06078f1d12e 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -5,7 +5,7 @@ } .avatar-image { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { float: left; margin-bottom: 0; } @@ -119,7 +119,7 @@ .key-list-item { .key-list-item-info { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { float: left; } } @@ -188,7 +188,7 @@ .modal-dialog { width: 380px; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { width: auto; } @@ -242,7 +242,7 @@ left: 0; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .cover-block { padding-top: 20px; } @@ -352,7 +352,7 @@ table.u2f-registrations { } } - @media(max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { text-align: center; .bordered-box { @@ -414,7 +414,7 @@ table.u2f-registrations { } &.unverified { - @include status-color($gray-dark, $gray, $common-gray-dark); + @include status-color($gray-dark, color("gray"), $common-gray-dark); } } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index dd0cb2c2613..22964163e95 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -9,7 +9,7 @@ .new_project, .edit-project, .import-project { - .help-block { + .form-text.text-muted { margin-bottom: 10px; } @@ -34,7 +34,7 @@ } } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .input-group > div { &:last-child { margin-bottom: 0; @@ -46,7 +46,8 @@ } } - .input-group-addon { + .input-group-prepend, + .input-group-append { overflow: hidden; text-overflow: ellipsis; line-height: unset; @@ -82,7 +83,7 @@ border: 1px solid $border-color; padding: 10px 32px; - @media (max-width: $screen-xs-min) { + @include media-breakpoint-down(xs) { padding: 10px 20px; } } @@ -134,7 +135,7 @@ max-width: 400px; } - @media (max-width: $screen-xs-min) { + @include media-breakpoint-down(xs) { padding-left: 20px; } } @@ -144,7 +145,7 @@ padding-top: 24px; padding-bottom: 24px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { border-bottom: 1px solid $border-color; } @@ -230,11 +231,11 @@ } .notification-dropdown .dropdown-menu { - @extend .dropdown-menu-align-right; + @extend .dropdown-menu-right; } .download-button { - @media (max-width: $screen-md-max) { + @include media-breakpoint-down(md) { margin-left: 0; } } @@ -444,11 +445,11 @@ height: 200px; width: calc((100% / 2) - #{$gl-padding * 2}); - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { width: calc((100% / 4) - #{$gl-padding * 2}); } - @media (min-width: $screen-lg-min) { + @include media-breakpoint-up(lg) { width: calc((100% / 5) - #{$gl-padding * 2}); } @@ -537,11 +538,12 @@ .template-input-group { position: relative; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: flex; } - .input-group-addon { + .input-group-prepend, + .input-group-append { flex: 1; text-align: left; padding-left: ($gl-padding * 3); @@ -601,12 +603,12 @@ margin: 0 auto 4px; font-size: 24px; - @media (min-width: $screen-xs-max) { + @media (min-width: map-get($grid-breakpoints, sm)-1) { top: 0; } } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .btn-template-icon { display: inline-block; height: 14px; @@ -625,31 +627,31 @@ .create-project-options { display: flex; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { display: block; } .first-column { - @media (min-width: $screen-xs-min) { + @include media-breakpoint-up(xs) { max-width: 50%; padding-right: 30px; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { max-width: 100%; width: 100%; } } .second-column { - @media (min-width: $screen-xs-min) { + @include media-breakpoint-up(xs) { width: 50%; flex: 1; padding-left: 30px; position: relative; } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { max-width: 100%; width: 100%; padding-left: 0; @@ -657,7 +659,7 @@ } // Mobile - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { padding-top: 30px; } @@ -677,7 +679,7 @@ line-height: 20px; // Mobile - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { left: 50%; top: 0; transform: translateX(-50%); @@ -697,7 +699,7 @@ top: 0; // Mobile - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { top: 10px; left: 10px; right: 10px; @@ -735,7 +737,7 @@ vertical-align: top; margin-top: 0; - @media (min-width: $screen-lg-min) { + @include media-breakpoint-up(lg) { float: right; } } @@ -866,19 +868,14 @@ pre.light-well { } } -.panel .projects-list li { +.card .projects-list li { padding: 10px 15px; margin: 0; } -.commits-search-form { - .input-short { - min-width: 200px; - } -} - .git-clone-holder { width: 380px; + height: 28px; .btn-clipboard { border: 1px solid $border-color; @@ -894,11 +891,11 @@ pre.light-well { .form-control { @extend .monospace; - background: $white-light; + background-color: $white-light; + border-color: $border-color; font-size: 14px; margin-left: -1px; cursor: auto; - width: 101%; } } @@ -927,7 +924,8 @@ pre.light-well { } .project-tip-command { - > .input-group-btn:first-child { + > .input-group-prepend:first-child, + > .input-group-append:first-child { width: auto; } } @@ -978,7 +976,7 @@ pre.light-well { .dropdown-menu-projects { width: 300px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: 500px; } @@ -992,7 +990,7 @@ pre.light-well { .inline-input-group { width: 100%; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: 300px; } } @@ -1003,8 +1001,8 @@ pre.light-well { text-align: center; margin-top: -20px; - @media (min-width: $screen-sm-min) { - margin-top: 0; + @include media-breakpoint-up(sm) { + margin: 0 $gl-padding-8; width: auto; } } @@ -1042,7 +1040,7 @@ pre.light-well { } &.form-group { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { margin-bottom: 0; } } @@ -1070,12 +1068,12 @@ pre.light-well { .project-feature { padding-top: 10px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { padding-left: 45px; } &.nested { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { padding-left: 90px; } } diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 00457717f00..17d7087bd85 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -22,7 +22,6 @@ height: calc(100vh - #{$header-height}); margin-top: 0; border-top: 1px solid $white-dark; - border-bottom: 1px solid $white-dark; padding-bottom: $ide-statusbar-height; &.is-collapsed { @@ -39,12 +38,15 @@ .ide-file-list { flex: 1; + padding-left: $gl-padding; + padding-right: $gl-padding; + padding-bottom: $grid-size; .file { cursor: pointer; &.file-open { - background: $link-active-background; + background: $white-normal; } &.file-active { @@ -84,12 +86,11 @@ .ide-new-btn { display: none; - margin-right: -8px; } &:hover, &:focus { - background: $link-active-background; + background: $white-normal; .ide-new-btn { display: block; @@ -111,12 +112,11 @@ } } -.file-name, -.file-col-commit-message { +.file-name { display: flex; overflow: visible; align-items: center; - padding: 6px 12px; + width: 100%; } .multi-file-loading-container { @@ -306,8 +306,18 @@ } .preview-container { - height: 100%; - overflow: auto; + flex-grow: 1; + position: relative; + + .md-previewer { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: auto; + padding: $gl-padding; + } .file-container { background-color: $gray-darker; @@ -347,10 +357,6 @@ color: $diff-image-info-color; } } - - .md-previewer { - padding: $gl-padding; - } } .ide-mode-tabs { @@ -373,7 +379,7 @@ .ide-status-bar { border-top: 1px solid $white-dark; - padding: $gl-bar-padding $gl-padding; + padding: 2px $gl-padding-8 0; background: $white-light; display: flex; justify-content: space-between; @@ -384,12 +390,19 @@ left: 0; width: 100%; + font-size: 12px; + line-height: 22px; + + * { + font-size: inherit; + } + > div + div { padding-left: $gl-padding; } svg { - vertical-align: middle; + vertical-align: sub; } } @@ -501,7 +514,7 @@ align-items: center; margin-bottom: 0; border-bottom: 1px solid $white-dark; - padding: $gl-btn-padding $gl-padding; + padding: 12px 0; } .multi-file-commit-panel-header-title { @@ -523,32 +536,31 @@ .multi-file-commit-list { flex: 1; overflow: auto; - padding: $gl-padding; + padding: $grid-size 0; + margin-left: -$grid-size; + margin-right: -$grid-size; min-height: 60px; + + .multi-file-commit-list-item { + margin-left: 0; + margin-right: 0; + } + + &.help-block { + margin-left: 0; + right: 0; + } } .multi-file-commit-list-item { - display: flex; - padding: 0; - align-items: center; - border-radius: $border-radius-default; - .multi-file-discard-btn { display: none; margin-top: -2px; margin-left: auto; - margin-right: $grid-size; color: $gl-link-color; - - &:focus, - &:hover { - text-decoration: underline; - } } &:hover { - background: $white-normal; - .multi-file-discard-btn { display: flex; } @@ -584,25 +596,39 @@ } } +.multi-file-commit-list-item, +.ide-file-list .file { + display: flex; + align-items: center; + margin-left: -$grid-size; + margin-right: -$grid-size; + padding: $grid-size / 2 $grid-size; + border-radius: $border-radius-default; + text-align: left; + + &:hover, + &:focus { + background: $white-normal; + } +} + .multi-file-commit-list-path { - padding: $grid-size / 2; - padding-left: $grid-size; + padding: 0; background: none; border: 0; text-align: left; width: 100%; - min-width: 0; + + &:hover, + &:focus { + outline: 0; + } svg { min-width: 16px; vertical-align: middle; display: inline-block; } - - &:hover, - &:focus { - outline: 0; - } } .multi-file-commit-list-file-path { @@ -619,12 +645,18 @@ .multi-file-commit-form { position: relative; - padding: $gl-padding; background-color: $white-light; - border-top: 1px solid $white-dark; border-left: 1px solid $white-dark; transition: all 0.3s ease; + > form, + > .commit-form-compact { + padding: $gl-padding 0; + margin-left: $gl-padding; + margin-right: $gl-padding; + border-top: 1px solid $white-dark; + } + .btn { font-size: $gl-font-size; } @@ -787,8 +819,9 @@ display: flex; flex: 1; flex-direction: column; - width: 100%; min-height: 140px; + margin-left: $gl-padding; + margin-right: $gl-padding; &.is-first { border-bottom: 1px solid $white-dark; @@ -821,7 +854,7 @@ } } - .help-block { + .form-text.text-muted { margin-top: 0; line-height: 0; } @@ -979,9 +1012,8 @@ .ide-tree-header { display: flex; align-items: center; - padding: 10px 0; - margin-left: 10px; - margin-right: 10px; + margin-bottom: 8px; + padding: 12px 0; border-bottom: 1px solid $white-dark; .ide-new-btn { @@ -1012,9 +1044,9 @@ .commit-form-slide-up-enter-active, .commit-form-slide-up-leave-active { position: absolute; - top: 16px; - left: 16px; - right: 16px; + top: 0; + left: 0; + right: 0; transition: all 0.3s ease; } diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss index 5fb97b13470..2734faec558 100644 --- a/app/assets/stylesheets/pages/runners.scss +++ b/app/assets/stylesheets/pages/runners.scss @@ -51,7 +51,7 @@ } } -@media (max-width: $screen-md-max) { +@include media-breakpoint-down(md) { .runners-content { width: 100%; overflow: auto; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index dbde0720993..a35c4ff7c80 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -47,6 +47,7 @@ input[type="checkbox"]:hover { } .location-badge { + white-space: nowrap; height: 32px; font-size: 12px; margin: -4px 4px -4px -4px; @@ -166,7 +167,7 @@ input[type="checkbox"]:hover { } .search-holder { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: -webkit-flex; display: flex; } @@ -178,7 +179,7 @@ input[type="checkbox"]:hover { position: relative; margin-right: 0; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { margin-right: 5px; } } @@ -202,7 +203,7 @@ input[type="checkbox"]:hover { width: 100%; margin-top: 5px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: auto; margin-top: 0; margin-left: 5px; @@ -210,7 +211,7 @@ input[type="checkbox"]:hover { } .dropdown { - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { margin-left: 5px; margin-right: 5px; } @@ -220,7 +221,7 @@ input[type="checkbox"]:hover { width: 100%; margin-top: 5px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: 180px; margin-top: 0; } diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index c410049bc0b..2b3773eebad 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -70,7 +70,7 @@ animation: none; } - @media(max-width: $screen-sm-max) { + @media(max-width: map-get($grid-breakpoints, md)-1) { padding-right: 20px; } @@ -98,8 +98,8 @@ } .bs-callout, - .checkbox:first-child, - .help-block { + .form-check:first-child, + .form-text.text-muted { margin-top: 0; } @@ -131,12 +131,12 @@ color: $gl-danger; } -.service-settings .control-label { +.service-settings .form-control-label { padding-top: 0; } .integration-settings-form { - .well { + .card.card-body { padding: $gl-padding / 2; box-shadow: none; } @@ -158,7 +158,7 @@ } .visibility-level-setting { - .radio { + .form-check { margin-bottom: 10px; i.fa { @@ -174,7 +174,7 @@ .option-description, .option-disabled-reason { - margin-left: 29px; + margin-left: 45px; color: $project-option-descr-color; } @@ -199,22 +199,22 @@ } .prometheus-metrics-monitoring { - .panel { - .panel-toggle { + .card { + .card-toggle { width: 14px; } - .badge { + .badge.badge-pill { font-size: 12px; line-height: 12px; } - .panel-heading .badge-count { + .card-header .label-count { color: $white-light; background: $common-gray-dark; } - .panel-body { + .card-body { padding: 0; } @@ -249,7 +249,7 @@ li { padding: $gl-padding; - .badge { + .badge.badge-pill { margin-left: 5px; background: $badge-bg; } diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss index 8e2c42c1bd3..3f6f5f06075 100644 --- a/app/assets/stylesheets/pages/stat_graph.scss +++ b/app/assets/stylesheets/pages/stat_graph.scss @@ -14,7 +14,10 @@ } #contributors-master { - @include make-md-column(12); + @include media-breakpoint-up(md) { + @include make-col-ready(); + @include make-col(12); + } svg { width: 100%; @@ -33,10 +36,14 @@ } .person { - @include make-md-column(6); + @include media-breakpoint-up(md) { + @include make-col-ready(); + @include make-col(6); + } + margin-top: 10px; - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { width: 100%; } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index ade5ddd147b..620297e589d 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -58,7 +58,7 @@ } } -.visible-xs-inline { +.d-block.d-sm-none-inline { .ci-status-link { position: relative; top: 2px; diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 4b9824fab0c..e5d7dd13915 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -126,7 +126,7 @@ color: $gl-grayish-blue; font-size: $gl-font-size; - .label { + .badge.badge-pill { color: $gl-text-color; } @@ -162,7 +162,7 @@ } } -@media (max-width: $screen-sm-max) { +@include media-breakpoint-down(sm) { .todos-filters { .dropdown-menu-toggle { width: 130px; @@ -174,7 +174,7 @@ } } -@media (max-width: $screen-xs-max) { +@include media-breakpoint-down(xs) { .todo { .avatar { display: none; @@ -214,7 +214,7 @@ margin-left: auto; margin-right: auto; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { -webkit-flex-direction: row; flex-direction: row; padding-top: 80px; @@ -233,7 +233,7 @@ margin-left: auto; margin-right: auto; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { width: 300px; margin-right: 0; -webkit-order: 2; @@ -244,7 +244,7 @@ .todos-all-done { padding-top: 20px; - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { padding-top: 50px; } diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index e0ee7e9aa3d..efd26cb1f81 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -7,7 +7,7 @@ color: $gl-text-color-secondary; } - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { display: flex; .tree-ref-container { @@ -41,15 +41,10 @@ position: relative; } } - - .add-to-tree-dropdown { - position: absolute; - left: 18px; - } } } - @media (max-width: $screen-xs-max) { + @include media-breakpoint-down(xs) { .repo-breadcrumb { margin-top: 10px; position: relative; @@ -121,7 +116,7 @@ margin-left: 5px; } - @media (min-width: $screen-md-min) and (max-width: $screen-md-max) { + @include media-breakpoint-only(md) { @include str-truncated(450px); } diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index e70a57c2a67..800f5c68e39 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -65,7 +65,7 @@ display: block; } - @media (min-width: $screen-sm-min) { + @include media-breakpoint-up(sm) { &.has-sidebar-toggle { padding-right: 40px; } @@ -81,7 +81,7 @@ } } - @media (min-width: $screen-md-min) { + @include media-breakpoint-up(md) { &.has-sidebar-toggle { padding-right: 0; } diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 8958eab0423..cdfe3d6ab1e 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -52,7 +52,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController private def set_application_setting - @application_setting = ApplicationSetting.current + @application_setting = ApplicationSetting.current_without_cache end def application_setting_params diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index e85cdcb8db7..d6a6bc7d4a1 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -1,4 +1,6 @@ class Admin::DashboardController < Admin::ApplicationController + include CountHelper + def index @projects = Project.order_id_desc.without_deleted.with_route.limit(10) @users = User.order_id_desc.limit(10) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2caffec66ac..2843d70c645 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -13,8 +13,7 @@ class ApplicationController < ActionController::Base before_action :authenticate_sessionless_user! before_action :authenticate_user! - before_action :enforce_terms!, if: -> { Gitlab::CurrentSettings.current_application_settings.enforce_terms }, - unless: :peek_request? + before_action :enforce_terms!, if: :should_enforce_terms? before_action :validate_user_service_ticket! before_action :check_password_expiration before_action :ldap_security_check @@ -373,4 +372,10 @@ class ApplicationController < ActionController::Base def peek_request? request.path.start_with?('/-/peek') end + + def should_enforce_terms? + return false unless Gitlab::CurrentSettings.current_application_settings.enforce_terms + + !(peek_request? || devise_controller?) + end end diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb index 7d7ff217e5d..09e143c23e8 100644 --- a/app/controllers/boards/issues_controller.rb +++ b/app/controllers/boards/issues_controller.rb @@ -94,7 +94,7 @@ module Boards def serialize_as_json(resource) resource.as_json( - only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position], + only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position, :weight], labels: true, issue_endpoints: true, include_full_project_path: board.group_board?, diff --git a/app/controllers/concerns/accepts_pending_invitations.rb b/app/controllers/concerns/accepts_pending_invitations.rb new file mode 100644 index 00000000000..6e8aef52b52 --- /dev/null +++ b/app/controllers/concerns/accepts_pending_invitations.rb @@ -0,0 +1,15 @@ +module AcceptsPendingInvitations + extend ActiveSupport::Concern + + def accept_pending_invitations + return unless resource.active_for_authentication? + + clear_stored_location_for_resource if resource.accept_pending_invitations!.any? + end + + def clear_stored_location_for_resource + session_key = stored_location_key_for(resource) + + session.delete(session_key) + end +end diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb index 55011c89886..237c93daee8 100644 --- a/app/controllers/concerns/send_file_upload.rb +++ b/app/controllers/concerns/send_file_upload.rb @@ -2,6 +2,10 @@ module SendFileUpload def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, disposition: 'attachment') if attachment redirect_params[:query] = { "response-content-disposition" => "#{disposition};filename=#{attachment.inspect}" } + # By default, Rails will send uploads with an extension of .js with a + # content-type of text/javascript, which will trigger Rails' + # cross-origin JavaScript protection. + send_params[:content_type] = 'text/plain' if File.extname(attachment) == '.js' send_params.merge!(filename: attachment, disposition: disposition) end diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 6d9c38d9581..7bc46a6ccc0 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -1,4 +1,6 @@ class ConfirmationsController < Devise::ConfirmationsController + include AcceptsPendingInvitations + def almost_there flash[:notice] = nil render layout: "devise_empty" @@ -11,6 +13,8 @@ class ConfirmationsController < Devise::ConfirmationsController end def after_confirmation_path_for(resource_name, resource) + accept_pending_invitations + # incoming resource can either be a :user or an :email if signed_in?(:user) after_sign_in(resource) diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb index 7c2016f0326..e892d1f8dbf 100644 --- a/app/controllers/groups/boards_controller.rb +++ b/app/controllers/groups/boards_controller.rb @@ -2,19 +2,24 @@ class Groups::BoardsController < Groups::ApplicationController include BoardsResponses before_action :assign_endpoint_vars + before_action :boards, only: :index def index - @boards = Boards::ListService.new(group, current_user).execute - respond_with_boards end def show - @board = group.boards.find(params[:id]) + @board = boards.find(params[:id]) respond_with_board end + private + + def boards + @boards ||= Boards::ListService.new(group, current_user).execute + end + def assign_endpoint_vars @boards_endpoint = group_boards_url(group) @namespace_path = group.to_param diff --git a/app/controllers/groups/settings/badges_controller.rb b/app/controllers/groups/settings/badges_controller.rb index edb334a3d88..ccbd0a3bc02 100644 --- a/app/controllers/groups/settings/badges_controller.rb +++ b/app/controllers/groups/settings/badges_controller.rb @@ -1,12 +1,12 @@ module Groups module Settings class BadgesController < Groups::ApplicationController - include GrapeRouteHelpers::NamedRouteMatcher + include API::Helpers::RelatedResourcesHelpers before_action :authorize_admin_group! def index - @badge_api_endpoint = api_v4_groups_badges_path(id: @group.id) + @badge_api_endpoint = expose_url(api_v4_groups_badges_path(id: @group.id)) end end end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index ed89bed029b..27fd5f7ba37 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -26,11 +26,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController # Extend the standard message generation to accept our custom exception def failure_message - exception = env["omniauth.error"] + exception = request.env["omniauth.error"] error = exception.error_reason if exception.respond_to?(:error_reason) error ||= exception.error if exception.respond_to?(:error) error ||= exception.message if exception.respond_to?(:message) - error ||= env["omniauth.error.type"].to_s + error ||= request.env["omniauth.error.type"].to_s error.to_s.humanize if error end diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index f0e5d2aa94e..12a6cd11f80 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -23,7 +23,7 @@ class Profiles::KeysController < Profiles::ApplicationController def destroy @key = current_user.keys.find(params[:id]) - @key.destroy + Keys::DestroyService.new(current_user).execute(@key) respond_to do |format| format.html { redirect_to profile_keys_url, status: 302 } diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 949e54ff819..e7354a9e1f7 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -4,22 +4,25 @@ class Projects::BoardsController < Projects::ApplicationController before_action :check_issues_available! before_action :authorize_read_board!, only: [:index, :show] + before_action :boards, only: :index before_action :assign_endpoint_vars def index - @boards = Boards::ListService.new(project, current_user).execute - respond_with_boards end def show - @board = project.boards.find(params[:id]) + @board = boards.find(params[:id]) respond_with_board end private + def boards + @boards ||= Boards::ListService.new(project, current_user).execute + end + def assign_endpoint_vars @boards_endpoint = project_boards_path(project) @bulk_issues_path = bulk_update_project_issues_path(project) diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb index 90c7fa62216..35885543622 100644 --- a/app/controllers/projects/clusters/applications_controller.rb +++ b/app/controllers/projects/clusters/applications_controller.rb @@ -5,9 +5,10 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll before_action :authorize_create_cluster!, only: [:create] def create - Clusters::Applications::ScheduleInstallationService.new(project, current_user, - application_class: @application_class, - cluster: @cluster).execute + application = @application_class.find_or_create_by!(cluster: @cluster) + + Clusters::Applications::ScheduleInstallationService.new(project, current_user).execute(application) + head :no_content rescue StandardError head :bad_request diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index b7f548e0e63..1d1184d46d1 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -23,8 +23,12 @@ class Projects::CommitController < Projects::ApplicationController respond_to do |format| format.html { render } - format.diff { render text: @commit.to_diff } - format.patch { render text: @commit.to_patch } + format.diff do + send_git_diff(@project.repository, @commit.diff_refs) + end + format.patch do + send_git_patch(@project.repository, @commit.diff_refs) + end end end diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index bc13b8ad7ba..4d4c2af2415 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -8,19 +8,6 @@ class Projects::NotesController < Projects::ApplicationController before_action :authorize_create_note!, only: [:create] before_action :authorize_resolve_note!, only: [:resolve, :unresolve] - # - # This is a fix to make spinach feature tests passing: - # Controller actions are returned from AbstractController::Base and methods of parent classes are - # excluded in order to return only specific controller related methods. - # That is ok for the app (no :create method in ancestors) - # but fails for tests because there is a :create method on FactoryBot (one of the ancestors) - # - # see https://github.com/rails/rails/blob/v4.2.7/actionpack/lib/abstract_controller/base.rb#L78 - # - def create - super - end - def delete_attachment note.remove_attachment! note.update_attribute(:attachment, nil) diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 0b1b46944aa..6b40fc2fe68 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -18,19 +18,12 @@ class Projects::PipelinesController < Projects::ApplicationController .page(params[:page]) .per(30) - @running_count = PipelinesFinder - .new(project, scope: 'running').execute.count + @running_count = limited_pipelines_count(project, 'running') + @pending_count = limited_pipelines_count(project, 'pending') + @finished_count = limited_pipelines_count(project, 'finished') + @pipelines_count = limited_pipelines_count(project) - @pending_count = PipelinesFinder - .new(project, scope: 'pending').execute.count - - @finished_count = PipelinesFinder - .new(project, scope: 'finished').execute.count - - @pipelines_count = PipelinesFinder - .new(project).execute.count - - @pipelines.map(&:commit) # List commits for batch loading + Gitlab::Ci::Pipeline::Preloader.preload(@pipelines) respond_to do |format| format.html @@ -41,7 +34,7 @@ class Projects::PipelinesController < Projects::ApplicationController pipelines: PipelineSerializer .new(project: @project, current_user: @current_user) .with_pagination(request, response) - .represent(@pipelines), + .represent(@pipelines, disable_coverage: true), count: { all: @pipelines_count, running: @running_count, @@ -181,4 +174,14 @@ class Projects::PipelinesController < Projects::ApplicationController # Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42343 Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42339') end + + def authorize_update_pipeline! + return access_denied! unless can?(current_user, :update_pipeline, @pipeline) + end + + def limited_pipelines_count(project, scope = nil) + finder = PipelinesFinder.new(project, scope: scope) + + view_context.limited_counter_with_delimiter(finder.execute) + end end diff --git a/app/controllers/projects/prometheus/metrics_controller.rb b/app/controllers/projects/prometheus/metrics_controller.rb index 1dd886409a5..c6b6243b553 100644 --- a/app/controllers/projects/prometheus/metrics_controller.rb +++ b/app/controllers/projects/prometheus/metrics_controller.rb @@ -25,7 +25,7 @@ module Projects end def require_prometheus_metrics! - render_404 unless prometheus_adapter.can_query? + render_404 unless prometheus_adapter&.can_query? end end end diff --git a/app/controllers/projects/settings/badges_controller.rb b/app/controllers/projects/settings/badges_controller.rb index f7b70dd4b7b..7887bee49c5 100644 --- a/app/controllers/projects/settings/badges_controller.rb +++ b/app/controllers/projects/settings/badges_controller.rb @@ -1,12 +1,12 @@ module Projects module Settings class BadgesController < Projects::ApplicationController - include GrapeRouteHelpers::NamedRouteMatcher + include API::Helpers::RelatedResourcesHelpers before_action :authorize_admin_project! def index - @badge_api_endpoint = api_v4_projects_badges_path(id: @project.id) + @badge_api_endpoint = expose_url(api_v4_projects_badges_path(id: @project.id)) end end end diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 177c8a54099..1d850baf012 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -69,7 +69,7 @@ module Projects @project_runners = @project.runners.ordered @assignable_runners = current_user - .ci_authorized_runners + .ci_owned_runners .assignable_for(project) .ordered .page(params[:page]).per(20) diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb index 1ff08cce8cb..d9fecfecc40 100644 --- a/app/controllers/projects/settings/integrations_controller.rb +++ b/app/controllers/projects/settings/integrations_controller.rb @@ -11,7 +11,14 @@ module Projects @hook = ProjectHook.new # Services - @services = @project.find_or_initialize_services + @services = @project.find_or_initialize_services(exceptions: service_exceptions) + end + + private + + # Returns a list of services that should be hidden from the list + def service_exceptions + @project.disabled_services.dup end end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 1848c806c41..f5a222b3a48 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -1,5 +1,6 @@ class RegistrationsController < Devise::RegistrationsController include Recaptcha::Verify + include AcceptsPendingInvitations before_action :whitelist_query_limiting, only: [:destroy] @@ -16,6 +17,7 @@ class RegistrationsController < Devise::RegistrationsController end if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha + accept_pending_invitations super else flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' @@ -60,7 +62,7 @@ class RegistrationsController < Devise::RegistrationsController def after_sign_up_path_for(user) Gitlab::AppLogger.info("User Created: username=#{user.username} email=#{user.email} ip=#{request.remote_ip} confirmed:#{user.confirmed?}") - user.confirmed? ? dashboard_projects_path : users_almost_there_path + user.confirmed? ? stored_location_for(user) || dashboard_projects_path : users_almost_there_path end def after_inactive_sign_up_path_for(resource) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 7ed9b1fc6d0..c6ef79ce15e 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -6,7 +6,7 @@ # klass - actual class like Issue or MergeRequest # current_user - which user use # params: -# scope: 'created-by-me' or 'assigned-to-me' or 'all' +# scope: 'created_by_me' or 'assigned_to_me' or 'all' # state: 'opened' or 'closed' or 'all' # group_id: integer # project_id: integer @@ -282,9 +282,9 @@ class IssuableFinder return items.none if current_user_related? && !current_user case params[:scope] - when 'created-by-me', 'authored' + when 'created_by_me', 'authored' items.where(author_id: current_user.id) - when 'assigned-to-me' + when 'assigned_to_me' items.assigned_to(current_user) else items @@ -426,6 +426,7 @@ class IssuableFinder end def current_user_related? - params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' + scope = params[:scope] + scope == 'created_by_me' || scope == 'authored' || scope == 'assigned_to_me' end end diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 2a27ff0e386..1787b4899cd 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -5,7 +5,7 @@ # Arguments: # current_user - which user use # params: -# scope: 'created-by-me' or 'assigned-to-me' or 'all' +# scope: 'created_by_me' or 'assigned_to_me' or 'all' # state: 'open' or 'closed' or 'all' # group_id: integer # project_id: integer diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 64dc1e6af0f..e2240e5e0d8 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -5,7 +5,7 @@ # Arguments: # current_user - which user use # params: -# scope: 'created-by-me' or 'assigned-to-me' or 'all' +# scope: 'created_by_me' or 'assigned_to_me' or 'all' # state: 'open', 'closed', 'merged', or 'all' # group_id: integer # project_id: integer diff --git a/app/finders/personal_projects_finder.rb b/app/finders/personal_projects_finder.rb index 3ad4bd5f066..5aea0cb8192 100644 --- a/app/finders/personal_projects_finder.rb +++ b/app/finders/personal_projects_finder.rb @@ -13,7 +13,7 @@ class PersonalProjectsFinder < UnionFinder def execute(current_user = nil) segments = all_projects(current_user) - find_union(segments, Project).includes(:namespace).order_id_desc + find_union(segments, Project).includes(:namespace).order_updated_desc end private diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index aa4569500b8..f5d94ad96a1 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -2,6 +2,11 @@ require 'digest/md5' require 'uri' module ApplicationHelper + # See https://docs.gitlab.com/ee/development/ee_features.html#code-in-app-views + def render_if_exists(partial, locals = {}) + render(partial, locals) if lookup_context.exists?(partial, [], true) + end + # Check if a particular controller is the current one # # args - One or more controller names to check diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb index 16451993e93..7b076728685 100644 --- a/app/helpers/auto_devops_helper.rb +++ b/app/helpers/auto_devops_helper.rb @@ -24,6 +24,15 @@ module AutoDevopsHelper end end + def cluster_ingress_ip(project) + project + .cluster_ingresses + .where("external_ip is not null") + .limit(1) + .pluck(:external_ip) + .first + end + private def missing_auto_devops_domain?(project) diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index d339c01d492..43d92bde064 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -78,7 +78,7 @@ module AvatarsHelper user: commit_or_event.author, user_name: commit_or_event.author_name, user_email: commit_or_event.author_email, - css_class: 'hidden-xs' + css_class: 'd-none d-sm-inline' })) end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index e7a36e20050..3db28fd6da3 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -17,7 +17,9 @@ module BlobHelper end def ide_edit_path(project = @project, ref = @ref, path = @path, options = {}) - "#{ide_path}/project#{url_for([project, "edit", "blob", id: [ref, path], script_name: "/"])}" + segments = [ide_path, 'project', project.full_path, 'edit', ref] + segments.concat(['-', path]) if path.present? + File.join(segments) end def edit_blob_button(project = @project, ref = @ref, path = @path, options = {}) @@ -331,7 +333,6 @@ module BlobHelper if !on_top_of_branch?(project, ref) edit_disabled_button_tag(text, common_classes) # This condition only applies to users who are logged in - # Web IDE (Beta) requires the user to have this feature enabled elsif !current_user || (current_user && can_modify_blob?(blob, project, ref)) edit_link_tag(text, edit_path, common_classes) elsif can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index f0afcac5986..5fce97164ae 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -92,7 +92,7 @@ module CiStatusHelper "pipeline-status/#{pipeline_status.sha}-#{pipeline_status.status}" end - def render_project_pipeline_status(pipeline_status, tooltip_placement: 'auto left') + def render_project_pipeline_status(pipeline_status, tooltip_placement: 'left') project = pipeline_status.project path = pipelines_project_commit_path(project, pipeline_status.sha, ref: pipeline_status.ref) @@ -103,7 +103,7 @@ module CiStatusHelper tooltip_placement: tooltip_placement) end - def render_commit_status(commit, ref: nil, tooltip_placement: 'auto left') + def render_commit_status(commit, ref: nil, tooltip_placement: 'left') project = commit.project path = pipelines_project_commit_path(project, commit, ref: ref) @@ -114,7 +114,7 @@ module CiStatusHelper tooltip_placement: tooltip_placement) end - def render_pipeline_status(pipeline, tooltip_placement: 'auto left') + def render_pipeline_status(pipeline, tooltip_placement: 'left') project = pipeline.project path = project_pipeline_path(project, pipeline) render_status_with_link('pipeline', pipeline.status, path, tooltip_placement: tooltip_placement) @@ -125,7 +125,7 @@ module CiStatusHelper Ci::Runner.shared.blank? end - def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '', container: 'body') + def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body') klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}" title = "#{type.titleize}: #{ci_label_for_status(status)}" data = { toggle: 'tooltip', placement: tooltip_placement, container: container } diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index e594a1d0ba3..e5c3be47801 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -27,7 +27,7 @@ module CommitsHelper return unless @project && @ref # Add the root project link and the arrow icon - crumbs = content_tag(:li) do + crumbs = content_tag(:li, class: 'breadcrumb-item') do link_to( @project.path, project_commits_path(@project, @ref) @@ -38,7 +38,7 @@ module CommitsHelper parts = @path.split('/') parts.each_with_index do |part, i| - crumbs << content_tag(:li) do + crumbs << content_tag(:li, class: 'breadcrumb-item') do # The text is just the individual part, but the link needs all the parts before it link_to( part, @@ -62,8 +62,8 @@ module CommitsHelper # Returns a link formatted as a commit branch link def commit_branch_link(url, text) - link_to(url, class: 'label label-gray ref-name branch-link') do - sprite_icon('fork', size: 12, css_class: 'fork-svg') + "#{text}" + link_to(url, class: 'badge badge-gray ref-name branch-link') do + sprite_icon('branch', size: 12, css_class: 'fork-svg') + "#{text}" end end @@ -76,8 +76,8 @@ module CommitsHelper # Returns a link formatted as a commit tag link def commit_tag_link(url, text) - link_to(url, class: 'label label-gray ref-name') do - icon('tag', class: 'append-right-5') + "#{text}" + link_to(url, class: 'badge badge-gray ref-name') do + sprite_icon('tag', size: 12, css_class: 'append-right-5 vertical-align-middle') + "#{text}" end end diff --git a/app/helpers/count_helper.rb b/app/helpers/count_helper.rb new file mode 100644 index 00000000000..24ee62e68ba --- /dev/null +++ b/app/helpers/count_helper.rb @@ -0,0 +1,5 @@ +module CountHelper + def approximate_count_with_delimiters(model) + number_with_delimiter(Gitlab::Database::Count.approximate_count(model)) + end +end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 079b3cd3aa0..cb6f709c604 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -41,7 +41,7 @@ module EventsHelper key = key.to_s active = 'active' if @event_filter.active?(key) link_opts = { - class: "event-filter-link has-tooltip", + class: "event-filter-link", id: "#{key}_event_filter", title: tooltip } diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 2f304b040c7..58372edff3c 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -60,7 +60,7 @@ module IconsHelper def spinner(text = nil, visible = false) css_class = 'loading' - css_class << ' hide' unless visible + css_class << ' hidden' unless visible content_tag :div, class: css_class do icon('spinner spin') + text diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index f39a62bccc8..8572c2b7276 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -157,15 +157,15 @@ module IssuablesHelper output = "" output << "Opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe output << content_tag(:strong) do - author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true) - author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg") + author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline-block", tooltip: true) + author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-block d-sm-none") end output << " ".html_safe output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip', title: _('1st contribution!')) - output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "hidden-xs hidden-sm") - output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "hidden-md hidden-lg") + output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block") + output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none d-lg-none d-xl-inline-block") output.html_safe end @@ -199,7 +199,7 @@ module IssuablesHelper if display_count count = issuables_count_for_state(issuable_type, state) - html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge') + html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge badge-pill') end html.html_safe @@ -359,7 +359,8 @@ module IssuablesHelper url: project_todos_path(@project), delete_path: (dashboard_todo_path(todo) if todo), placement: (is_collapsed ? 'left' : nil), - container: (is_collapsed ? 'body' : nil) + container: (is_collapsed ? 'body' : nil), + boundary: 'viewport' } end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index c4a6a1e4bb3..e1b0e7a4a3e 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -81,7 +81,7 @@ module LabelsHelper # Intentionally not using content_tag here so that this method can be called # by LabelReferenceFilter - span = %(<span class="label color-label #{"has-tooltip" if tooltip}" ) + + span = %(<span class="badge color-label #{"has-tooltip" if tooltip}" ) + %(style="background-color: #{label.color}; color: #{text_color}" ) + %(title="#{escape_once(label.description)}" data-container="body">) + %(#{escape_once(label.name)}#{label_suffix}</span>) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index e8caab3e50c..15a15405f1d 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -89,7 +89,7 @@ module MilestonesHelper def milestone_progress_bar(milestone) options = { - class: 'progress-bar progress-bar-success', + class: 'progress-bar bg-success', style: "width: #{milestone.percent_complete(current_user)}%;" } @@ -112,8 +112,6 @@ module MilestonesHelper def milestone_tooltip_title(milestone) if milestone "#{milestone.title}<br />#{milestone_tooltip_due_date(milestone)}" - else - _('Milestone') end end @@ -173,6 +171,8 @@ module MilestonesHelper def milestone_tooltip_due_date(milestone) if milestone.due_date "#{milestone.due_date.to_s(:medium)} (#{remaining_days_in_words(milestone)})" + else + _('Milestone') end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index eb81dc2de43..fa54eafd3a3 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -257,6 +257,7 @@ module ProjectsHelper if project.builds_enabled? && can?(current_user, :read_pipeline, project) nav_tabs << :pipelines + nav_tabs << :operations end if project.external_issue_tracker diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index b3f2aeb08ca..5ba3a4a322c 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -56,6 +56,14 @@ module Emails mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason)) end + def merge_request_unmergeable_email(recipient_id, merge_request_id, reason = nil) + setup_merge_request_mail(merge_request_id, recipient_id) + + @reasons = MergeRequestPresenter.new(@merge_request, current_user: current_user).unmergeable_reasons + + mail_answer_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id, reason)) + end + def resolved_all_discussions_email(recipient_id, merge_request_id, resolved_by_user_id, reason = nil) setup_merge_request_mail(merge_request_id, recipient_id) diff --git a/app/models/appearance.rb b/app/models/appearance.rb index fb66dd0b766..67cc84a9140 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -1,7 +1,8 @@ class Appearance < ActiveRecord::Base + include CacheableAttributes include CacheMarkdownField - include AfterCommitQueue include ObjectStorage::BackgroundMove + include WithUploads cache_markdown_field :description cache_markdown_field :new_project_guidelines @@ -14,18 +15,9 @@ class Appearance < ActiveRecord::Base mount_uploader :logo, AttachmentUploader mount_uploader :header_logo, AttachmentUploader - has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - - CACHE_KEY = "current_appearance:#{Gitlab::VERSION}".freeze - - after_commit :flush_redis_cache - - def self.current - Rails.cache.fetch(CACHE_KEY) { first } - end - - def flush_redis_cache - Rails.cache.delete(CACHE_KEY) + # Overrides CacheableAttributes.current_without_cache + def self.current_without_cache + first end def single_appearance_row diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 451e512aef7..e8ccb320fae 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -1,11 +1,11 @@ class ApplicationSetting < ActiveRecord::Base + include CacheableAttributes include CacheMarkdownField include TokenAuthenticatable add_authentication_token_field :runners_registration_token add_authentication_token_field :health_check_access_token - CACHE_KEY = 'application_setting.last'.freeze DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace | # or \s # any whitespace character @@ -229,40 +229,6 @@ class ApplicationSetting < ActiveRecord::Base after_commit do reset_memoized_terms - Rails.cache.write(CACHE_KEY, self) - end - - def self.current - ensure_cache_setup - - Rails.cache.fetch(CACHE_KEY) do - ApplicationSetting.last.tap do |settings| - # do not cache nils - raise 'missing settings' unless settings - end - end - rescue - # Fall back to an uncached value if there are any problems (e.g. redis down) - ApplicationSetting.last - end - - def self.expire - Rails.cache.delete(CACHE_KEY) - rescue - # Gracefully handle when Redis is not available. For example, - # omnibus may fail here during gitlab:assets:compile. - end - - def self.cached - value = Rails.cache.read(CACHE_KEY) - ensure_cache_setup if value.present? - value - end - - def self.ensure_cache_setup - # This is a workaround for a Rails bug that causes attribute methods not - # to be loaded when read from cache: https://github.com/rails/rails/issues/27348 - ApplicationSetting.define_attribute_methods end def self.defaults diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 61c10c427dd..75fd55a8f7b 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -184,7 +184,7 @@ module Ci end def playable? - action? && (manual? || complete?) + action? && (manual? || retryable?) end def action? @@ -599,6 +599,7 @@ module Ci break variables unless persisted? variables + .concat(pipeline.persisted_variables) .append(key: 'CI_JOB_ID', value: id.to_s) .append(key: 'CI_JOB_TOKEN', value: token, public: false) .append(key: 'CI_BUILD_ID', value: id.to_s) @@ -617,7 +618,7 @@ module Ci variables.append(key: 'GITLAB_FEATURES', value: project.licensed_features.join(',')) variables.append(key: 'CI_SERVER_NAME', value: 'GitLab') variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION) - variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION) + variables.append(key: 'CI_SERVER_REVISION', value: Gitlab.revision) variables.append(key: 'CI_JOB_NAME', value: name) variables.append(key: 'CI_JOB_STAGE', value: stage) variables.append(key: 'CI_COMMIT_SHA', value: sha) @@ -661,7 +662,7 @@ module Ci Gitlab::Ci::Variables::Collection.new.tap do |variables| break variables unless gitlab_deploy_token - variables.append(key: 'CI_DEPLOY_USER', value: gitlab_deploy_token.name) + variables.append(key: 'CI_DEPLOY_USER', value: gitlab_deploy_token.username) variables.append(key: 'CI_DEPLOY_PASSWORD', value: gitlab_deploy_token.token, public: false) end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 0b90834d415..53af87a271a 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -37,12 +37,16 @@ module Ci delegate :id, to: :project, prefix: true delegate :full_path, to: :project, prefix: true - validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create validates :sha, presence: { unless: :importing? } validates :ref, presence: { unless: :importing? } validates :status, presence: { unless: :importing? } validate :valid_commit_sha, unless: :importing? + # Replace validator below with + # `validates :source, presence: { unless: :importing? }, on: :create` + # when removing Gitlab.rails5? code. + validate :valid_source, unless: :importing?, on: :create + after_create :keep_around_commits, unless: :importing? enum source: { @@ -402,7 +406,18 @@ module Ci end def has_warnings? - builds.latest.failed_but_allowed.any? + number_of_warnings.positive? + end + + def number_of_warnings + BatchLoader.for(id).batch(default_value: 0) do |pipeline_ids, loader| + Build.where(commit_id: pipeline_ids) + .latest + .failed_but_allowed + .group(:commit_id) + .count + .each { |id, amount| loader.call(id, amount) } + end end def set_config_source @@ -508,9 +523,14 @@ module Ci strong_memoize(:legacy_trigger) { trigger_requests.first } end + def persisted_variables + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables.append(key: 'CI_PIPELINE_ID', value: id.to_s) if persisted? + end + end + def predefined_variables Gitlab::Ci::Variables::Collection.new - .append(key: 'CI_PIPELINE_ID', value: id.to_s) .append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path) .append(key: 'CI_PIPELINE_SOURCE', value: source.to_s) .append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message) @@ -601,5 +621,11 @@ module Ci project.repository.keep_around(self.sha) project.repository.keep_around(self.before_sha) end + + def valid_source + if source.nil? || source == "unknown" + errors.add(:source, "invalid source") + end + end end end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index bda69f85a78..530eacf4be0 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -52,7 +52,7 @@ module Ci # Without that, placeholders would miss one and couldn't match. where(locked: false) .where.not("ci_runners.id IN (#{project.runners.select(:id).to_sql})") - .specific + .project_type end validate :tag_constraints @@ -75,7 +75,7 @@ module Ci project_type: 3 } - cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address + cached_attr_reader :version, :revision, :platform, :architecture, :ip_address, :contacted_at chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index 7b25d8c4089..c702c4ee807 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -49,6 +49,11 @@ module Clusters # ensures headers containing auth data are appended to original k8s client options options = kube_client.rest_client.options.merge(headers: kube_client.headers) RestClient::Resource.new(proxy_url, options) + rescue Kubeclient::HttpError + # If users have mistakenly set parameters or removed the depended clusters, + # `proxy_url` could raise an exception because gitlab can not communicate with the cluster. + # Since `PrometheusAdapter#can_query?` is eargely loaded on environement pages in gitlab, + # we need to silence the exceptions end private diff --git a/app/models/commit.rb b/app/models/commit.rb index b46f9f34689..56d4c86774e 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -224,8 +224,34 @@ class Commit Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message) end + def lazy_author + BatchLoader.for(author_email.downcase).batch do |emails, loader| + # A Hash that maps user Emails to the corresponding User objects. The + # Emails at this point are the _primary_ Emails of the Users. + users_for_emails = User + .by_any_email(emails) + .each_with_object({}) { |user, hash| hash[user.email] = user } + + users_for_ids = users_for_emails + .values + .each_with_object({}) { |user, hash| hash[user.id] = user } + + # Some commits may have used an alternative Email address. In this case we + # need to query the "emails" table to map those addresses to User objects. + Email + .where(email: emails - users_for_emails.keys) + .pluck(:email, :user_id) + .each { |(email, id)| users_for_emails[email] = users_for_ids[id] } + + users_for_emails.each { |email, user| loader.call(email, user) } + end + end + def author - User.find_by_any_email(author_email.downcase) + # We use __sync so that we get the actual objects back (including an actual + # nil), instead of a wrapper, as returning a wrapped nil breaks a lot of + # code. + lazy_author.__sync end request_cache(:author) { author_email.downcase } diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 97d89422594..a7d05722287 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -2,6 +2,7 @@ class CommitStatus < ActiveRecord::Base include HasStatus include Importable include AfterCommitQueue + include Presentable self.table_name = 'ci_builds' diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb new file mode 100644 index 00000000000..b32459fdabf --- /dev/null +++ b/app/models/concerns/cacheable_attributes.rb @@ -0,0 +1,54 @@ +module CacheableAttributes + extend ActiveSupport::Concern + + included do + after_commit { self.class.expire } + end + + class_methods do + # Can be overriden + def current_without_cache + last + end + + def cache_key + "#{name}:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:json".freeze + end + + def defaults + {} + end + + def build_from_defaults(attributes = {}) + new(defaults.merge(attributes)) + end + + def cached + json_attributes = Rails.cache.read(cache_key) + return nil unless json_attributes.present? + + build_from_defaults(JSON.parse(json_attributes)) + end + + def current + cached_record = cached + return cached_record if cached_record.present? + + current_without_cache.tap { |current_record| current_record&.cache! } + rescue + # Fall back to an uncached value if there are any problems (e.g. Redis down) + current_without_cache + end + + def expire + Rails.cache.delete(cache_key) + rescue + # Gracefully handle when Redis is not available. For example, + # omnibus may fail here during gitlab:assets:compile. + end + end + + def cache! + Rails.cache.write(self.class.cache_key, attributes.to_json) + end +end diff --git a/app/models/concerns/redis_cacheable.rb b/app/models/concerns/redis_cacheable.rb index b889f4202dc..b5425295130 100644 --- a/app/models/concerns/redis_cacheable.rb +++ b/app/models/concerns/redis_cacheable.rb @@ -7,7 +7,11 @@ module RedisCacheable class_methods do def cached_attr_reader(*attributes) attributes.each do |attribute| - define_method("#{attribute}") do + define_method(attribute) do + unless self.has_attribute?(attribute) + raise ArgumentError, "`cached_attr_reader` requires the #{self.class.name}\##{attribute} attribute to have a database column" + end + cached_attribute(attribute) || read_attribute(attribute) end end @@ -15,13 +19,16 @@ module RedisCacheable end def cached_attribute(attribute) - (cached_attributes || {})[attribute] + cached_value = (cached_attributes || {})[attribute] + cast_value_from_cache(attribute, cached_value) if cached_value end def cache_attributes(values) Gitlab::Redis::SharedState.with do |redis| redis.set(cache_attribute_key, values.to_json, ex: CACHED_ATTRIBUTES_EXPIRY_TIME) end + + clear_memoization(:cached_attributes) end private @@ -38,4 +45,12 @@ module RedisCacheable end end end + + def cast_value_from_cache(attribute, value) + if Gitlab.rails5? + self.class.type_for_attribute(attribute).cast(value) + else + self.class.column_for_attribute(attribute).type_cast_from_database(value) + end + end end diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb index 668c5a079e3..4a0f8b92b3a 100644 --- a/app/models/concerns/resolvable_note.rb +++ b/app/models/concerns/resolvable_note.rb @@ -32,7 +32,7 @@ module ResolvableNote # Keep this method in sync with the `potentially_resolvable` scope def potentially_resolvable? - RESOLVABLE_TYPES.include?(self.class.name) && noteable.supports_resolvable_notes? + RESOLVABLE_TYPES.include?(self.class.name) && noteable&.supports_resolvable_notes? end # Keep this method in sync with the `resolvable` scope diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index cefa5c13c5f..db7254c27e0 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -12,8 +12,8 @@ module Sortable scope :order_created_asc, -> { reorder(created_at: :asc) } scope :order_updated_desc, -> { reorder(updated_at: :desc) } scope :order_updated_asc, -> { reorder(updated_at: :asc) } - scope :order_name_asc, -> { reorder(name: :asc) } - scope :order_name_desc, -> { reorder(name: :desc) } + scope :order_name_asc, -> { reorder("lower(name) asc") } + scope :order_name_desc, -> { reorder("lower(name) desc") } end module ClassMethods diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb index 73fc5048dcf..1caf47072bc 100644 --- a/app/models/concerns/time_trackable.rb +++ b/app/models/concerns/time_trackable.rb @@ -53,6 +53,10 @@ module TimeTrackable Gitlab::TimeTrackingFormatter.output(time_estimate) end + def time_estimate=(val) + val.is_a?(Integer) ? super([val, Gitlab::Database::MAX_INT_VALUE].min) : super(val) + end + private def touchable? diff --git a/app/models/concerns/with_uploads.rb b/app/models/concerns/with_uploads.rb new file mode 100644 index 00000000000..e7cfffb775b --- /dev/null +++ b/app/models/concerns/with_uploads.rb @@ -0,0 +1,39 @@ +# Mounted uploaders are destroyed by carrierwave's after_commit +# hook. This hook fetches upload location (local vs remote) from +# Upload model. So it's neccessary to make sure that during that +# after_commit hook model's associated uploads are not deleted yet. +# IOW we can not use dependent: :destroy : +# has_many :uploads, as: :model, dependent: :destroy +# +# And because not-mounted uploads require presence of upload's +# object model when destroying them (FileUploader's `build_upload` method +# references `model` on delete), we can not use after_commit hook for these +# uploads. +# +# Instead FileUploads are destroyed in before_destroy hook and remaining uploads +# are destroyed by the carrierwave's after_commit hook. + +module WithUploads + extend ActiveSupport::Concern + + # Currently there is no simple way how to select only not-mounted + # uploads, it should be all FileUploaders so we select them by + # `uploader` class + FILE_UPLOADERS = %w(PersonalFileUploader NamespaceFileUploader FileUploader).freeze + + included do + has_many :uploads, as: :model + + before_destroy :destroy_file_uploads + end + + # mounted uploads are deleted in carrierwave's after_commit hook, + # but FileUploaders which are not mounted must be deleted explicitly and + # it can not be done in after_commit because FileUploader requires loads + # associated model on destroy (which is already deleted in after_commit) + def destroy_file_uploads + self.uploads.where(uploader: FILE_UPLOADERS).find_each do |upload| + upload.destroy + end + end +end diff --git a/app/models/group.rb b/app/models/group.rb index cefca316399..8fb77a7869d 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -10,6 +10,7 @@ class Group < Namespace include LoadedInGroupList include GroupDescendant include TokenAuthenticatable + include WithUploads has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent alias_method :members, :group_members @@ -30,8 +31,6 @@ class Group < Namespace has_many :variables, class_name: 'Ci::GroupVariable' has_many :custom_attributes, class_name: 'GroupCustomAttribute' - has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - has_many :boards has_many :badges, class_name: 'GroupBadge' diff --git a/app/models/list.rb b/app/models/list.rb index 918275be142..5daf35ef845 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -31,7 +31,8 @@ class List < ActiveRecord::Base if options.key?(:label) json[:label] = label.as_json( project: board.project, - only: [:id, :title, :description, :color] + only: [:id, :title, :description, :color], + methods: [:text_color] ) end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 628c61d5d69..9c4384a6e42 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -104,24 +104,35 @@ class MergeRequest < ActiveRecord::Base state_machine :merge_status, initial: :unchecked do event :mark_as_unchecked do - transition [:can_be_merged, :cannot_be_merged] => :unchecked + transition [:can_be_merged, :unchecked] => :unchecked + transition [:cannot_be_merged, :cannot_be_merged_recheck] => :cannot_be_merged_recheck end event :mark_as_mergeable do - transition [:unchecked, :cannot_be_merged] => :can_be_merged + transition [:unchecked, :cannot_be_merged_recheck] => :can_be_merged end event :mark_as_unmergeable do - transition [:unchecked, :can_be_merged] => :cannot_be_merged + transition [:unchecked, :cannot_be_merged_recheck] => :cannot_be_merged end state :unchecked + state :cannot_be_merged_recheck state :can_be_merged state :cannot_be_merged around_transition do |merge_request, transition, block| Gitlab::Timeless.timeless(merge_request, &block) end + + after_transition unchecked: :cannot_be_merged do |merge_request, transition| + NotificationService.new.merge_request_unmergeable(merge_request) + TodoService.new.merge_request_became_unmergeable(merge_request) + end + + def check_state?(merge_status) + [:unchecked, :cannot_be_merged_recheck].include?(merge_status.to_sym) + end end validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?] @@ -327,6 +338,16 @@ class MergeRequest < ActiveRecord::Base update_column(:merge_jid, jid) end + def merge_participants + participants = [author] + + if merge_when_pipeline_succeeds? && !participants.include?(merge_user) + participants << merge_user + end + + participants + end + def first_commit merge_request_diff ? merge_request_diff.first_commit : compare_commits.first end @@ -602,7 +623,7 @@ class MergeRequest < ActiveRecord::Base end def check_if_can_be_merged - return unless unchecked? && Gitlab::Database.read_write? + return unless self.class.state_machines[:merge_status].check_state?(merge_status) && Gitlab::Database.read_write? can_be_merged = !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch) diff --git a/app/models/project.rb b/app/models/project.rb index 32d34f5e9b8..0fe9f8880b4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -23,6 +23,7 @@ class Project < ActiveRecord::Base include ::Gitlab::Utils::StrongMemoize include ChronicDurationAttribute include FastDestroyAll::Helpers + include WithUploads extend Gitlab::ConfigHelper @@ -217,6 +218,7 @@ class Project < ActiveRecord::Base has_one :cluster_project, class_name: 'Clusters::Project' has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster' + has_many :cluster_ingresses, through: :clusters, source: :application_ingress, class_name: 'Clusters::Applications::Ingress' # Container repositories need to remove data from the container registry, # which is not managed by the DB. Hence we're still using dependent: :destroy @@ -300,8 +302,6 @@ class Project < ActiveRecord::Base inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } } validates :variables, variable_duplicates: { scope: :environment_scope } - has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - # Scopes scope :pending_delete, -> { where(pending_delete: true) } scope :without_deleted, -> { where(pending_delete: false) } @@ -894,6 +894,13 @@ class Project < ActiveRecord::Base Gitlab::Routing.url_helpers.project_url(self) end + def readme_url + readme = repository.readme + if readme + Gitlab::Routing.url_helpers.project_blob_url(self, File.join(default_branch, readme.path)) + end + end + def new_issuable_address(author, address_type) return unless Gitlab::IncomingEmail.supports_issue_creation? && author @@ -997,7 +1004,7 @@ class Project < ActiveRecord::Base available_services_names = Service.available_services_names - exceptions - available_services_names.map do |service_name| + available_services = available_services_names.map do |service_name| service = find_service(services, service_name) if service @@ -1014,6 +1021,14 @@ class Project < ActiveRecord::Base end end end + + available_services.reject do |service| + disabled_services.include?(service.to_param) + end + end + + def disabled_services + [] end def find_or_initialize_service(name) @@ -1416,8 +1431,8 @@ class Project < ActiveRecord::Base self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token) end - def open_issues_count - Projects::OpenIssuesCountService.new(self).count + def open_issues_count(current_user = nil) + Projects::OpenIssuesCountService.new(self, current_user).count end def open_merge_requests_count diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index 71b10fc6bc1..a4bf427ac0b 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -115,6 +115,6 @@ class DroneCiService < CiService def merge_request_valid?(data) data[:object_attributes][:state] == 'opened' && - data[:object_attributes][:merge_status] == 'unchecked' + MergeRequest.state_machines[:merge_status].check_state?(data[:object_attributes][:merge_status]) end end diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index 26cbfd784ad..84248f9590b 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -3,6 +3,7 @@ require "gemnasium/gitlab_service" class GemnasiumService < Service prop_accessor :token, :api_key validates :token, :api_key, presence: true, if: :activated? + validate :deprecation_validation def title 'Gemnasium' @@ -27,6 +28,18 @@ class GemnasiumService < Service %w(push) end + def deprecated? + true + end + + def deprecation_message + "Gemnasium has been acquired by GitLab in January 2018. Since May 15, 2018, the service provided by Gemnasium is no longer available." + end + + def deprecation_validation + errors[:base] << deprecation_message + end + def execute(data) return unless supported_events.include?(data[:object_kind]) diff --git a/app/models/repository.rb b/app/models/repository.rb index 44c6bff6b66..0e1bf11d7c0 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -596,7 +596,7 @@ class Repository cache_method :gitlab_ci_yml def xcode_project? - file_on_head(:xcode_config).present? + file_on_head(:xcode_config, :tree).present? end cache_method :xcode_project? @@ -920,11 +920,21 @@ class Repository end end - def file_on_head(type) - if head = tree(:head) - head.blobs.find do |blob| - Gitlab::FileDetector.type_of(blob.path) == type + def file_on_head(type, object_type = :blob) + return unless head = tree(:head) + + objects = + case object_type + when :blob + head.blobs + when :tree + head.trees + else + raise ArgumentError, "Object type #{object_type} is not supported" end + + objects.find do |object| + Gitlab::FileDetector.type_of(object.path) == type end end diff --git a/app/models/service.rb b/app/models/service.rb index f7e3f7590ad..831c2ea1141 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -253,7 +253,6 @@ class Service < ActiveRecord::Base emails_on_push external_wiki flowdock - gemnasium hipchat irker jira diff --git a/app/models/user.rb b/app/models/user.rb index dfef065f094..0a838d34054 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -17,6 +17,7 @@ class User < ActiveRecord::Base include IgnorableColumn include BulkMemberAccessLoad include BlocksJsonSerialization + include WithUploads DEFAULT_NOTIFICATION_LEVEL = :participating @@ -108,7 +109,7 @@ class User < ActiveRecord::Base has_many :created_projects, foreign_key: :creator_id, class_name: 'Project' has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :starred_projects, through: :users_star_projects, source: :project - has_many :project_authorizations + has_many :project_authorizations, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_many :authorized_projects, through: :project_authorizations, source: :project has_many :user_interacted_projects @@ -137,7 +138,6 @@ class User < ActiveRecord::Base has_many :custom_attributes, class_name: 'UserCustomAttribute' has_many :callouts, class_name: 'UserCallout' - has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :term_agreements belongs_to :accepted_term, class_name: 'ApplicationSetting::Term' @@ -165,8 +165,7 @@ class User < ActiveRecord::Base validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id } before_validation :sanitize_attrs - before_validation :set_notification_email, if: :email_changed? - before_save :set_notification_email, if: :email_changed? # in case validation is skipped + before_validation :set_notification_email, if: :new_record? before_validation :set_public_email, if: :public_email_changed? before_save :set_public_email, if: :public_email_changed? # in case validation is skipped before_save :ensure_incoming_email_token @@ -179,8 +178,21 @@ class User < ActiveRecord::Base after_update :username_changed_hook, if: :username_changed? after_destroy :post_destroy_hook after_destroy :remove_key_cache - after_commit :update_emails_with_primary_email, on: :update, if: -> { previous_changes.key?('email') } - after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') } + after_commit(on: :update) do + if previous_changes.key?('email') + # Grab previous_email here since previous_changes changes after + # #update_emails_with_primary_email and #update_notification_email are called + previous_email = previous_changes[:email][0] + + update_emails_with_primary_email(previous_email) + update_invalid_gpg_signatures + + if previous_email == notification_email + self.notification_email = email + save + end + end + end after_initialize :set_projects_limit @@ -546,8 +558,7 @@ class User < ActiveRecord::Base # hash and `_was` variables getting munged. # By using an `after_commit` instead of `after_update`, we avoid the recursive callback # scenario, though it then requires us to use the `previous_changes` hash - def update_emails_with_primary_email - previous_email = previous_changes[:email][0] # grab this before the DestroyService is called + def update_emails_with_primary_email(previous_email) primary_email_record = emails.find_by(email: email) Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record @@ -772,13 +783,13 @@ class User < ActiveRecord::Base end def set_notification_email - if notification_email.blank? || !all_emails.include?(notification_email) + if notification_email.blank? || all_emails.exclude?(notification_email) self.notification_email = email end end def set_public_email - if public_email.blank? || !all_emails.include?(public_email) + if public_email.blank? || all_emails.exclude?(public_email) self.public_email = '' end end @@ -860,6 +871,16 @@ class User < ActiveRecord::Base confirmed? && !temp_oauth_email? end + def accept_pending_invitations! + pending_invitations.select do |member| + member.accept_invite!(self) + end + end + + def pending_invitations + Member.where(invite_email: verified_emails).invite + end + def all_emails all_emails = [] all_emails << email unless temp_oauth_email? @@ -999,12 +1020,19 @@ class User < ActiveRecord::Base !solo_owned_groups.present? end - def ci_authorized_runners - @ci_authorized_runners ||= begin - runner_ids = Ci::RunnerProject + def ci_owned_runners + @ci_owned_runners ||= begin + project_runner_ids = Ci::RunnerProject .where(project: authorized_projects(Gitlab::Access::MASTER)) .select(:runner_id) - Ci::Runner.specific.where(id: runner_ids) + + group_runner_ids = Ci::RunnerNamespace + .where(namespace_id: owned_or_masters_groups.select(:id)) + .select(:runner_id) + + union = Gitlab::SQL::Union.new([project_runner_ids, group_runner_ids]) + + Ci::Runner.specific.where("ci_runners.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection end end @@ -1097,8 +1125,11 @@ class User < ActiveRecord::Base # <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92> # def increment_failed_attempts! + return if ::Gitlab::Database.read_only? + self.failed_attempts ||= 0 self.failed_attempts += 1 + if attempts_exceeded? lock_access! unless access_locked? else @@ -1202,6 +1233,11 @@ class User < ActiveRecord::Base !terms_accepted? end + def owned_or_masters_groups + union = Gitlab::SQL::Union.new([owned_groups, masters_groups]) + Group.from("(#{union.to_sql}) namespaces") + end + protected # override, from Devise::Validatable diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 808a81cbbf9..8b65758f3e8 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -14,11 +14,20 @@ module Ci @subject.triggered_by?(@user) end + condition(:branch_allows_maintainer_push) do + @subject.project.branch_allows_maintainer_push?(@user, @subject.ref) + end + rule { protected_ref }.policy do prevent :update_build prevent :erase_build end rule { can?(:admin_build) | (can?(:update_build) & owner_of_job) }.enable :erase_build + + rule { can?(:public_access) & branch_allows_maintainer_push }.policy do + enable :update_build + enable :update_commit_status + end end end diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb index 6363c382ff8..540e4235299 100644 --- a/app/policies/ci/pipeline_policy.rb +++ b/app/policies/ci/pipeline_policy.rb @@ -4,8 +4,16 @@ module Ci condition(:protected_ref) { ref_protected?(@user, @subject.project, @subject.tag?, @subject.ref) } + condition(:branch_allows_maintainer_push) do + @subject.project.branch_allows_maintainer_push?(@user, @subject.ref) + end + rule { protected_ref }.prevent :update_pipeline + rule { can?(:public_access) & branch_allows_maintainer_push }.policy do + enable :update_pipeline + end + def ref_protected?(user, project, tag, ref) access = ::Gitlab::UserAccess.new(user, project: project) diff --git a/app/policies/ci/runner_policy.rb b/app/policies/ci/runner_policy.rb index 7dff8470e23..895abe87d86 100644 --- a/app/policies/ci/runner_policy.rb +++ b/app/policies/ci/runner_policy.rb @@ -1,16 +1,19 @@ module Ci class RunnerPolicy < BasePolicy with_options scope: :subject, score: 0 - condition(:shared) { @subject.is_shared? } - - with_options scope: :subject, score: 0 condition(:locked, scope: :subject) { @subject.locked? } - condition(:authorized_runner) { @user.ci_authorized_runners.include?(@subject) } + condition(:owned_runner) { @user.ci_owned_runners.exists?(@subject.id) } rule { anonymous }.prevent_all - rule { admin | authorized_runner }.enable :assign_runner - rule { ~admin & shared }.prevent :assign_runner + + rule { admin | owned_runner }.policy do + enable :assign_runner + enable :read_runner + enable :update_runner + enable :delete_runner + end + rule { ~admin & locked }.prevent :assign_runner end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 5759b1a376f..99a0d7118f2 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -76,7 +76,7 @@ class ProjectPolicy < BasePolicy condition(:request_access_enabled, scope: :subject, score: 0) { project.request_access_enabled } desc "Has merge requests allowing pushes to user" - condition(:has_merge_requests_allowing_pushes, scope: :subject) do + condition(:has_merge_requests_allowing_pushes) do project.merge_requests_allowing_push_to_user(user).any? end @@ -354,9 +354,7 @@ class ProjectPolicy < BasePolicy # to run pipelines for the branches they have access to. rule { can?(:public_access) & has_merge_requests_allowing_pushes }.policy do enable :create_build - enable :update_build enable :create_pipeline - enable :update_pipeline end rule do diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb index 4873d7ce662..e0aaa5cb736 100644 --- a/app/presenters/ci/build_presenter.rb +++ b/app/presenters/ci/build_presenter.rb @@ -1,16 +1,5 @@ module Ci - class BuildPresenter < Gitlab::View::Presenter::Delegated - CALLOUT_FAILURE_MESSAGES = { - unknown_failure: 'There is an unknown failure, please try again', - script_failure: 'There has been a script failure. Check the job log for more information', - api_failure: 'There has been an API failure, please try again', - stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again', - runner_system_failure: 'There has been a runner system failure, please try again', - missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information' - }.freeze - - presents :build - + class BuildPresenter < CommitStatusPresenter def erased_by_user? # Build can be erased through API, therefore it does not have # `erased_by` user assigned in that case. @@ -44,14 +33,6 @@ module Ci "#{subject.name} - #{detailed_status.status_tooltip}" end - def callout_failure_message - CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym] - end - - def recoverable? - failed? && !unrecoverable? - end - private def tooltip_for_badge @@ -61,9 +42,5 @@ module Ci def detailed_status @detailed_status ||= subject.detailed_status(user) end - - def unrecoverable? - script_failure? || missing_dependency_failure? - end end end diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb new file mode 100644 index 00000000000..c7f7aa836bd --- /dev/null +++ b/app/presenters/commit_status_presenter.rb @@ -0,0 +1,24 @@ +class CommitStatusPresenter < Gitlab::View::Presenter::Delegated + CALLOUT_FAILURE_MESSAGES = { + unknown_failure: 'There is an unknown failure, please try again', + script_failure: 'There has been a script failure. Check the job log for more information', + api_failure: 'There has been an API failure, please try again', + stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again', + runner_system_failure: 'There has been a runner system failure, please try again', + missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information' + }.freeze + + presents :build + + def callout_failure_message + CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym] + end + + def recoverable? + failed? && !unrecoverable? + end + + def unrecoverable? + script_failure? || missing_dependency_failure? + end +end diff --git a/app/presenters/generic_commit_status_presenter.rb b/app/presenters/generic_commit_status_presenter.rb new file mode 100644 index 00000000000..da09df29a37 --- /dev/null +++ b/app/presenters/generic_commit_status_presenter.rb @@ -0,0 +1,2 @@ +class GenericCommitStatusPresenter < CommitStatusPresenter +end diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index 4b4132af2d0..ad839d9840a 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -20,6 +20,17 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated end end + def unmergeable_reasons + strong_memoize(:unmergeable_reasons) do + reasons = [] + reasons << "no commits" if merge_request.has_no_commits? + reasons << "source branch is missing" unless merge_request.source_branch_exists? + reasons << "target branch is missing" unless merge_request.target_branch_exists? + reasons << "has merge conflicts" unless merge_request.project.repository.can_be_merged?(merge_request.diff_head_sha, merge_request.target_branch) + reasons + end + end + def cancel_merge_when_pipeline_succeeds_path if can_cancel_merge_when_pipeline_succeeds?(current_user) cancel_merge_when_pipeline_succeeds_project_merge_request_path(project, merge_request) diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb index 6457294b285..f782b411b84 100644 --- a/app/serializers/pipeline_entity.rb +++ b/app/serializers/pipeline_entity.rb @@ -4,7 +4,11 @@ class PipelineEntity < Grape::Entity expose :id expose :user, using: UserEntity expose :active?, as: :active - expose :coverage + + # Coverage isn't always necessary (e.g. when displaying project pipelines in + # the UI). Instead of creating an entirely different entity we just allow the + # disabling of this specific field whenever necessary. + expose :coverage, unless: proc { options[:disable_coverage] } expose :source expose :created_at, :updated_at diff --git a/app/services/boards/issues/create_service.rb b/app/services/boards/issues/create_service.rb index 7c4a79f555e..3025029755c 100644 --- a/app/services/boards/issues/create_service.rb +++ b/app/services/boards/issues/create_service.rb @@ -10,11 +10,15 @@ module Boards end def execute - create_issue(params.merge(label_ids: [list.label_id])) + create_issue(params.merge(issue_params)) end private + def issue_params + { label_ids: [list.label_id] } + end + def board @board ||= parent.boards.find(params.delete(:board_id)) end diff --git a/app/services/clusters/applications/schedule_installation_service.rb b/app/services/clusters/applications/schedule_installation_service.rb index eb8caa68ef7..9c5461e85e1 100644 --- a/app/services/clusters/applications/schedule_installation_service.rb +++ b/app/services/clusters/applications/schedule_installation_service.rb @@ -1,21 +1,10 @@ module Clusters module Applications class ScheduleInstallationService < ::BaseService - def execute - application_class.find_or_create_by!(cluster: cluster).try do |application| - application.make_scheduled! - ClusterInstallAppWorker.perform_async(application.name, application.id) - end - end - - private - - def application_class - params[:application_class] - end + def execute(application) + application.make_scheduled! - def cluster - params[:cluster] + ClusterInstallAppWorker.perform_async(application.name, application.id) end end end diff --git a/app/services/keys/base_service.rb b/app/services/keys/base_service.rb index f78791932a7..df8e82f5f60 100644 --- a/app/services/keys/base_service.rb +++ b/app/services/keys/base_service.rb @@ -2,7 +2,7 @@ module Keys class BaseService attr_accessor :user, :params - def initialize(user, params) + def initialize(user, params = {}) @user, @params = user, params @ip_address = @params.delete(:ip_address) end diff --git a/app/services/keys/destroy_service.rb b/app/services/keys/destroy_service.rb new file mode 100644 index 00000000000..785cfa3a1d8 --- /dev/null +++ b/app/services/keys/destroy_service.rb @@ -0,0 +1,12 @@ +module Keys + class DestroyService < ::Keys::BaseService + def execute(key) + key.destroy if destroy_possible?(key) + end + + # overriden in EE::Keys::DestroyService + def destroy_possible?(key) + true + end + end +end diff --git a/app/services/lfs/unlock_file_service.rb b/app/services/lfs/unlock_file_service.rb index 6c93dc69bb0..7eb89339a92 100644 --- a/app/services/lfs/unlock_file_service.rb +++ b/app/services/lfs/unlock_file_service.rb @@ -2,14 +2,14 @@ module Lfs class UnlockFileService < BaseService def execute unless can?(current_user, :push_code, project) - raise Gitlab::GitAccess::UnauthorizedError, 'You have no permissions' + raise Gitlab::GitAccess::UnauthorizedError, _('You have no permissions') end unlock_file rescue Gitlab::GitAccess::UnauthorizedError => ex error(ex.message, 403) rescue ActiveRecord::RecordNotFound - error('Lock not found', 404) + error(_('Lock not found'), 404) rescue => ex error(ex.message, 500) end @@ -24,9 +24,9 @@ module Lfs success(lock: lock, http_status: :ok) elsif forced - error('You must have master access to force delete a lock', 403) + error(_('You must have master access to force delete a lock'), 403) else - error("#{lock.path} is locked by GitLab User #{lock.user_id}", 403) + error(_("%{lock_path} is locked by GitLab User %{lock_user_id}") % { lock_path: lock.path, lock_user_id: lock.user_id }, 403) end end diff --git a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb index 850deb0ac7a..9a4e6eb2e88 100644 --- a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb +++ b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb @@ -24,11 +24,7 @@ module MergeRequests pipeline_merge_requests(pipeline) do |merge_request| next unless merge_request.merge_when_pipeline_succeeds? - - unless merge_request.mergeable? - todo_service.merge_request_became_unmergeable(merge_request) - next - end + next unless merge_request.mergeable? merge_request.merge_async(merge_request.merge_user_id, merge_request.merge_params) end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 1fb1796b56c..0127d781686 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -3,6 +3,12 @@ module MergeRequests def execute(oldrev, newrev, ref) return true unless Gitlab::Git.branch_ref?(ref) + do_execute(oldrev, newrev, ref) + end + + private + + def do_execute(oldrev, newrev, ref) @oldrev, @newrev = oldrev, newrev @branch_name = Gitlab::Git.ref_name(ref) @@ -28,8 +34,6 @@ module MergeRequests true end - private - def close_upon_missing_source_branch_ref # MergeRequest#reload_diff ignores not opened MRs. This means it won't # create an `empty` diff for `closed` MRs without a source branch, keeping diff --git a/app/services/milestones/base_service.rb b/app/services/milestones/base_service.rb index 4963601ea8b..cce0863d611 100644 --- a/app/services/milestones/base_service.rb +++ b/app/services/milestones/base_service.rb @@ -5,6 +5,7 @@ module Milestones def initialize(parent, user, params = {}) @parent, @current_user, @params = parent, user, params.dup + super end end end diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 5658699664d..4fa38665abc 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -18,6 +18,10 @@ module NotificationRecipientService Builder::NewNote.new(*a).notification_recipients end + def self.build_merge_request_unmergeable_recipients(*a) + Builder::MergeRequestUnmergeable.new(*a).notification_recipients + end + module Builder class Base def initialize(*) @@ -330,5 +334,26 @@ module NotificationRecipientService note.author end end + + class MergeRequestUnmergeable < Base + attr_reader :target + def initialize(merge_request) + @target = merge_request + end + + def build! + target.merge_participants.each do |user| + add_recipients(user, :participating, nil) + end + end + + def custom_action + :unmergeable_merge_request + end + + def acting_user + nil + end + end end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 55a1735e54b..636cfbf5b45 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -129,6 +129,7 @@ class NotificationService # When create a merge request we should send an email to: # + # * mr author # * mr assignee if their notification level is not Disabled # * project team members with notification level higher then Participating # * watchers of the mr's labels @@ -148,6 +149,15 @@ class NotificationService end end + # When a merge request is found to be unmergeable, we should send an email to: + # + # * mr author + # * mr merge user if set + # + def merge_request_unmergeable(merge_request) + merge_request_unmergeable_email(merge_request) + end + # When merge request text is updated, we should send an email to: # # * newly mentioned project team members with notification level higher than Participating @@ -484,6 +494,14 @@ class NotificationService end end + def merge_request_unmergeable_email(merge_request) + recipients = NotificationRecipientService.build_merge_request_unmergeable_recipients(merge_request) + + recipients.each do |recipient| + mailer.merge_request_unmergeable_email(recipient.user.id, merge_request.id).deliver_later + end + end + def mailer Notify end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index adbc498d0bf..077d27c5836 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -51,11 +51,11 @@ module Projects flush_caches(@project) - unless mv_repository(removal_path(repo_path), repo_path) + unless rollback_repository(removal_path(repo_path), repo_path) raise_error('Failed to restore project repository. Please contact the administrator.') end - unless mv_repository(removal_path(wiki_path), wiki_path) + unless rollback_repository(removal_path(wiki_path), wiki_path) raise_error('Failed to restore wiki repository. Please contact the administrator.') end end @@ -84,6 +84,9 @@ module Projects # Skip repository removal. We use this flag when remove user or group return true if params[:skip_repo] == true + # There is a possibility project does not have repository or wiki + return true unless repo_exists?(path) + new_path = removal_path(path) if mv_repository(path, new_path) @@ -98,8 +101,18 @@ module Projects end end - def mv_repository(from_path, to_path) + def rollback_repository(old_path, new_path) # There is a possibility project does not have repository or wiki + return true unless repo_exists?(old_path) + + mv_repository(old_path, new_path) + end + + def repo_exists?(path) + gitlab_shell.exists?(project.repository_storage, path + '.git') + end + + def mv_repository(from_path, to_path) return true unless gitlab_shell.exists?(project.repository_storage, from_path + '.git') gitlab_shell.mv_repository(project.repository_storage, from_path, to_path) diff --git a/app/services/projects/open_issues_count_service.rb b/app/services/projects/open_issues_count_service.rb index a975a06a05c..0a004677417 100644 --- a/app/services/projects/open_issues_count_service.rb +++ b/app/services/projects/open_issues_count_service.rb @@ -2,14 +2,42 @@ module Projects # Service class for counting and caching the number of open issues of a # project. class OpenIssuesCountService < Projects::CountService + include Gitlab::Utils::StrongMemoize + + def initialize(project, user = nil) + @user = user + + super(project) + end + def cache_key_name - 'open_issues_count' + public_only? ? 'public_open_issues_count' : 'total_open_issues_count' + end + + def public_only? + !user_is_at_least_reporter? + end + + def relation_for_count + self.class.query(@project, public_only: public_only?) + end + + def user_is_at_least_reporter? + strong_memoize(:user_is_at_least_reporter) do + @user && @project.team.member?(@user, Gitlab::Access::REPORTER) + end end - def self.query(project_ids) - # We don't include confidential issues in this number since this would - # expose the number of confidential issues to non project members. - Issue.opened.public_only.where(project: project_ids) + # We only show total issues count for reporters + # which are allowed to view confidential issues + # This will still show a discrepancy on issues number but should be less than before. + # Check https://gitlab.com/gitlab-org/gitlab-ce/issues/38418 description. + def self.query(projects, public_only: true) + if public_only + Issue.opened.public_only.where(project: projects) + else + Issue.opened.where(project: projects) + end end end end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index ffd48e842c2..f91cd03bf5c 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -98,12 +98,12 @@ class TodoService # When a build fails on the HEAD of a merge request we should: # - # * create a todo for author of MR to fix it - # * create a todo for merge_user to keep an eye on it + # * create a todo for each merge participant # def merge_request_build_failed(merge_request) - create_build_failed_todo(merge_request, merge_request.author) - create_build_failed_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds? + merge_request.merge_participants.each do |user| + create_build_failed_todo(merge_request, user) + end end # When a new commit is pushed to a merge request we should: @@ -116,20 +116,22 @@ class TodoService # When a build is retried to a merge request we should: # - # * mark all pending todos related to the merge request for the author as done - # * mark all pending todos related to the merge request for the merge_user as done + # * mark all pending todos related to the merge request as done for each merge participant # def merge_request_build_retried(merge_request) - mark_pending_todos_as_done(merge_request, merge_request.author) - mark_pending_todos_as_done(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds? + merge_request.merge_participants.each do |user| + mark_pending_todos_as_done(merge_request, user) + end end - # When a merge request could not be automatically merged due to its unmergeable state we should: + # When a merge request could not be merged due to its unmergeable state we should: # - # * create a todo for a merge_user + # * create a todo for each merge participant # def merge_request_became_unmergeable(merge_request) - create_unmergeable_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds? + merge_request.merge_participants.each do |user| + create_unmergeable_todo(merge_request, user) + end end # When create a note we should: @@ -275,9 +277,9 @@ class TodoService create_todos(todo_author, attributes) end - def create_unmergeable_todo(merge_request, merge_user) - attributes = attributes_for_todo(merge_request.project, merge_request, merge_user, Todo::UNMERGEABLE) - create_todos(merge_user, attributes) + def create_unmergeable_todo(merge_request, todo_author) + attributes = attributes_for_todo(merge_request.project, merge_request, todo_author, Todo::UNMERGEABLE) + create_todos(todo_author, attributes) end def attributes_for_target(target) diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb index a3549cada95..f2a8afccdeb 100644 --- a/app/uploaders/object_storage.rb +++ b/app/uploaders/object_storage.rb @@ -103,6 +103,7 @@ module ObjectStorage end included do + include AfterCommitQueue after_save on: [:create, :update] do background_upload(changed_mounts) end diff --git a/app/views/abuse_report_mailer/notify.html.haml b/app/views/abuse_report_mailer/notify.html.haml index d50b4071745..5abb0dc9182 100644 --- a/app/views/abuse_report_mailer/notify.html.haml +++ b/app/views/abuse_report_mailer/notify.html.haml @@ -4,7 +4,7 @@ #{link_to @abuse_report.reporter.name, user_url(@abuse_report.reporter)} (@#{@abuse_report.reporter.username}). -%blockquote +.blockquote = @abuse_report.message %p diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml index 06be1a53318..278ad210543 100644 --- a/app/views/abuse_reports/new.html.haml +++ b/app/views/abuse_reports/new.html.haml @@ -2,20 +2,20 @@ %h3.page-title Report abuse %p Please use this form to report users who create spam issues, comments or behave inappropriately. %hr -= form_for @abuse_report, html: { class: 'form-horizontal js-quick-submit js-requires-input'} do |f| += form_for @abuse_report, html: { class: 'js-quick-submit js-requires-input'} do |f| = form_errors(@abuse_report) = f.hidden_field :user_id - .form-group - = f.label :user_id, class: 'control-label' + .form-group.row + = f.label :user_id, class: 'col-sm-2 col-form-label' .col-sm-10 - name = "#{@abuse_report.user.name} (@#{@abuse_report.user.username})" = text_field_tag :user_name, name, class: "form-control", readonly: true - .form-group - = f.label :message, class: 'control-label' + .form-group.row + = f.label :message, class: 'col-sm-2 col-form-label' .col-sm-10 = f.text_area :message, class: "form-control", rows: 2, required: true, value: sanitize(@ref_url) - .help-block + .form-text.text-muted Explain the problem with this user. If appropriate, provide a link to the relevant issue or comment. .form-actions diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml index 18c6c559049..89a87f38968 100644 --- a/app/views/admin/abuse_reports/_abuse_report.html.haml +++ b/app/views/admin/abuse_reports/_abuse_report.html.haml @@ -1,7 +1,7 @@ - reporter = abuse_report.reporter - user = abuse_report.user %tr - %th.visible-xs-block.visible-sm-block + %th.d-block.d-sm-none.d-md-none %strong User %td - if user @@ -11,7 +11,7 @@ - else (removed) %td - %strong.subheading.visible-xs-block.visible-sm-block Reported by + %strong.subheading.d-block.d-sm-none.d-md-none Reported by - if reporter = link_to reporter.name, reporter - else @@ -19,7 +19,7 @@ .light.small = time_ago_with_tooltip(abuse_report.created_at) %td - %strong.subheading.visible-xs-block.visible-sm-block Message + %strong.subheading.d-block.d-sm-none.d-md-none Message .message = markdown_field(abuse_report, :message) %td diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml index 6a5e170ddd8..cc29657a439 100644 --- a/app/views/admin/abuse_reports/index.html.haml +++ b/app/views/admin/abuse_reports/index.html.haml @@ -5,7 +5,7 @@ - if @abuse_reports.present? .table-holder %table.table.responsive-table - %thead.hidden-sm.hidden-xs + %thead.d-none.d-sm-none.d-md-table-header-group %tr %th User %th Reported by diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index 15bda97c3b5..5e08837255f 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -1,11 +1,11 @@ -= form_for @appearance, url: admin_appearances_path, html: { class: 'form-horizontal'} do |f| += form_for @appearance, url: admin_appearances_path do |f| = form_errors(@appearance) %fieldset.app_logo %legend Navigation bar: - .form-group - = f.label :header_logo, 'Header logo', class: 'control-label' + .form-group.row + = f.label :header_logo, 'Header logo', class: 'col-sm-2 col-form-label' .col-sm-10 - if @appearance.header_logo? = image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview' @@ -21,18 +21,18 @@ %fieldset.sign-in %legend Sign in/Sign up pages: - .form-group - = f.label :title, class: 'control-label' + .form-group.row + = f.label :title, class: 'col-sm-2 col-form-label' .col-sm-10 = f.text_field :title, class: "form-control" - .form-group - = f.label :description, class: 'control-label' + .form-group.row + = f.label :description, class: 'col-sm-2 col-form-label' .col-sm-10 = f.text_area :description, class: "form-control", rows: 10 .hint Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}. - .form-group - = f.label :logo, class: 'control-label' + .form-group.row + = f.label :logo, class: 'col-sm-2 col-form-label' .col-sm-10 - if @appearance.logo? = image_tag @appearance.logo_url, class: 'appearance-logo-preview' @@ -48,8 +48,8 @@ %fieldset %legend New project pages: - .form-group - = f.label :new_project_guidelines, class: 'control-label' + .form-group.row + = f.label :new_project_guidelines, class: 'col-sm-2 col-form-label' .col-sm-10 = f.text_area :new_project_guidelines, class: "form-control", rows: 10 .hint @@ -63,5 +63,5 @@ = link_to 'New project page', new_project_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer' - if @appearance.updated_at - %span.pull-right + %span.float-right Last edit #{time_ago_with_tooltip(@appearance.updated_at)} diff --git a/app/views/admin/application_settings/_abuse.html.haml b/app/views/admin/application_settings/_abuse.html.haml index bb3fa26a33e..6b9b2a17dd9 100644 --- a/app/views/admin/application_settings/_abuse.html.haml +++ b/app/views/admin/application_settings/_abuse.html.haml @@ -1,12 +1,12 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - = f.label :admin_notification_email, 'Abuse reports notification email', class: 'control-label col-sm-2' + .form-group.row + = f.label :admin_notification_email, 'Abuse reports notification email', class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :admin_notification_email, class: 'form-control' - .help-block + .form-text.text-muted Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index dd86c9ed2eb..f40dd347eb3 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -1,37 +1,37 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :gravatar_enabled do = f.check_box :gravatar_enabled Gravatar enabled - .form-group - = f.label :default_projects_limit, class: 'control-label col-sm-2' + .form-group.row + = f.label :default_projects_limit, class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :default_projects_limit, class: 'form-control' - .form-group - = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'control-label col-sm-2' + .form-group.row + = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :max_attachment_size, class: 'form-control' - .form-group - = f.label :session_expire_delay, 'Session duration (minutes)', class: 'control-label col-sm-2' + .form-group.row + = f.label :session_expire_delay, 'Session duration (minutes)', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :session_expire_delay, class: 'form-control' - %span.help-block#session_expire_delay_help_block GitLab restart is required to apply changes - .form-group - = f.label :user_oauth_applications, 'User OAuth applications', class: 'control-label col-sm-2' + %span.form-text.text-muted#session_expire_delay_help_block GitLab restart is required to apply changes + .form-group.row + = f.label :user_oauth_applications, 'User OAuth applications', class: 'col-form-label col-sm-2' .col-sm-10 - .checkbox + .form-check = f.label :user_oauth_applications do = f.check_box :user_oauth_applications Allow users to register any application to use GitLab as an OAuth provider - .form-group - = f.label :user_default_external, 'New users set to external', class: 'control-label col-sm-2' + .form-group.row + = f.label :user_default_external, 'New users set to external', class: 'col-form-label col-sm-2' .col-sm-10 - .checkbox + .form-check = f.label :user_default_external do = f.check_box :user_default_external Newly registered users will by default be external diff --git a/app/views/admin/application_settings/_background_jobs.html.haml b/app/views/admin/application_settings/_background_jobs.html.haml index 8198a822a10..da7248337d2 100644 --- a/app/views/admin/application_settings/_background_jobs.html.haml +++ b/app/views/admin/application_settings/_background_jobs.html.haml @@ -1,4 +1,4 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset @@ -6,25 +6,25 @@ These settings require a = link_to 'restart', help_page_path('administration/restart_gitlab') to take effect. - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :sidekiq_throttling_enabled do = f.check_box :sidekiq_throttling_enabled Enable Sidekiq Job Throttling - .help-block + .form-text.text-muted Limit the amount of resources slow running jobs are assigned. - .form-group - = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'control-label col-sm-2' + .form-group.row + = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'col-form-label col-sm-2' .col-sm-10 = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' } - .help-block + .form-text.text-muted Choose which queues you wish to throttle. - .form-group - = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'control-label col-sm-2' + .form-group.row + = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01' - .help-block + .form-text.text-muted The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml index b4d2a789df0..6a06271de2a 100644 --- a/app/views/admin/application_settings/_ci_cd.html.haml +++ b/app/views/admin/application_settings/_ci_cd.html.haml @@ -1,45 +1,45 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :auto_devops_enabled do = f.check_box :auto_devops_enabled - Enabled Auto DevOps (Beta) for projects by default - .help-block + Enabled Auto DevOps for projects by default + .form-text.text-muted It will automatically build, test, and deploy applications based on a predefined CI/CD configuration = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md') - .form-group - = f.label :auto_devops_domain, class: 'control-label col-sm-2' + .form-group.row + = f.label :auto_devops_domain, class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :auto_devops_domain, class: 'form-control', placeholder: 'domain.com' - .help-block + .form-text.text-muted = s_("AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages.") - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :shared_runners_enabled do = f.check_box :shared_runners_enabled Enable shared runners for new projects - .form-group - = f.label :shared_runners_text, class: 'control-label col-sm-2' + .form-group.row + = f.label :shared_runners_text, class: 'col-form-label col-sm-2' .col-sm-10 = f.text_area :shared_runners_text, class: 'form-control', rows: 4 - .help-block Markdown enabled - .form-group - = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'control-label col-sm-2' + .form-text.text-muted Markdown enabled + .form-group.row + = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :max_artifacts_size, class: 'form-control' - .help-block + .form-text.text-muted Set the maximum file size for each job's artifacts = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size') - .form-group - = f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'control-label col-sm-2' + .form-group.row + = f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :default_artifacts_expire_in, class: 'form-control' - .help-block + .form-text.text-muted Set the default expiration time for each job's artifacts. 0 for unlimited. = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml index 6c89f1c4e98..7443f02ad15 100644 --- a/app/views/admin/application_settings/_email.html.haml +++ b/app/views/admin/application_settings/_email.html.haml @@ -1,24 +1,24 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :email_author_in_body do = f.check_box :email_author_in_body Include author name in notification email body - .help-block + .form-text.text-muted Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :html_emails_enabled do = f.check_box :html_emails_enabled Enable HTML emails - .help-block + .form-text.text-muted By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format. diff --git a/app/views/admin/application_settings/_gitaly.html.haml b/app/views/admin/application_settings/_gitaly.html.haml index 4acc5b3a0c5..859a1c6f45c 100644 --- a/app/views/admin/application_settings/_gitaly.html.haml +++ b/app/views/admin/application_settings/_gitaly.html.haml @@ -1,27 +1,27 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'control-label col-sm-2' + .form-group.row + = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :gitaly_timeout_default, class: 'form-control' - .help-block + .form-text.text-muted Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced for git fetch/push operations or Sidekiq jobs. - .form-group - = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'control-label col-sm-2' + .form-group.row + = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :gitaly_timeout_fast, class: 'form-control' - .help-block + .form-text.text-muted Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast. If they exceed this threshold, there may be a problem with a storage shard and 'failing fast' can help maintain the stability of the GitLab instance. - .form-group - = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'control-label col-sm-2' + .form-group.row + = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :gitaly_timeout_medium, class: 'form-control' - .help-block + .form-text.text-muted Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_help_page.html.haml b/app/views/admin/application_settings/_help_page.html.haml index 3bc101ddf04..97d397788c6 100644 --- a/app/views/admin/application_settings/_help_page.html.haml +++ b/app/views/admin/application_settings/_help_page.html.haml @@ -1,22 +1,22 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - = f.label :help_page_text, class: 'control-label col-sm-2' + .form-group.row + = f.label :help_page_text, class: 'col-form-label col-sm-2' .col-sm-10 = f.text_area :help_page_text, class: 'form-control', rows: 4 - .help-block Markdown enabled - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-text.text-muted Markdown enabled + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :help_page_hide_commercial_content do = f.check_box :help_page_hide_commercial_content Hide marketing-related entries from help - .form-group - = f.label :help_page_support_url, 'Support page URL', class: 'control-label col-sm-2' + .form-group.row + = f.label :help_page_support_url, 'Support page URL', class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :help_page_support_url, class: 'form-control', placeholder: 'http://company.example.com/getting-help', :'aria-describedby' => 'support_help_block' - %span.help-block#support_help_block Alternate support URL for help page + %span.form-text.text-muted#support_help_block Alternate support URL for help page = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_influx.html.haml b/app/views/admin/application_settings/_influx.html.haml index a173fd38a9c..e5d699f9a2f 100644 --- a/app/views/admin/application_settings/_influx.html.haml +++ b/app/views/admin/application_settings/_influx.html.haml @@ -1,4 +1,4 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset @@ -8,60 +8,60 @@ = link_to 'restart', help_page_path('administration/restart_gitlab') to take effect. = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/introduction') - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :metrics_enabled do = f.check_box :metrics_enabled Enable InfluxDB Metrics - .form-group - = f.label :metrics_host, 'InfluxDB host', class: 'control-label col-sm-2' + .form-group.row + = f.label :metrics_host, 'InfluxDB host', class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com' - .form-group - = f.label :metrics_port, 'InfluxDB port', class: 'control-label col-sm-2' + .form-group.row + = f.label :metrics_port, 'InfluxDB port', class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :metrics_port, class: 'form-control', placeholder: '8089' - .help-block + .form-text.text-muted The UDP port to use for connecting to InfluxDB. InfluxDB requires that your server configuration specifies a database to store data in when sending messages to this port, without it metrics data will not be saved. - .form-group - = f.label :metrics_pool_size, 'Connection pool size', class: 'control-label col-sm-2' + .form-group.row + = f.label :metrics_pool_size, 'Connection pool size', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :metrics_pool_size, class: 'form-control' - .help-block + .form-text.text-muted The amount of InfluxDB connections to open. Connections are opened lazily. Users using multi-threaded application servers should ensure enough connections are available (at minimum the amount of application server threads). - .form-group - = f.label :metrics_timeout, 'Connection timeout', class: 'control-label col-sm-2' + .form-group.row + = f.label :metrics_timeout, 'Connection timeout', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :metrics_timeout, class: 'form-control' - .help-block + .form-text.text-muted The amount of seconds after which an InfluxDB connection will time out. - .form-group - = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'control-label col-sm-2' + .form-group.row + = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :metrics_method_call_threshold, class: 'form-control' - .help-block + .form-text.text-muted A method call is only tracked when it takes longer to complete than the given amount of milliseconds. - .form-group - = f.label :metrics_sample_interval, 'Sampler Interval (sec)', class: 'control-label col-sm-2' + .form-group.row + = f.label :metrics_sample_interval, 'Sampler Interval (sec)', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :metrics_sample_interval, class: 'form-control' - .help-block + .form-text.text-muted The sampling interval in seconds. Sampled data includes memory usage, retained Ruby objects, file descriptors and so on. - .form-group - = f.label :metrics_packet_size, 'Metrics per packet', class: 'control-label col-sm-2' + .form-group.row + = f.label :metrics_packet_size, 'Metrics per packet', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :metrics_packet_size, class: 'form-control' - .help-block + .form-text.text-muted The amount of points to store in a single UDP packet. More points results in fewer but larger UDP packets being sent. diff --git a/app/views/admin/application_settings/_ip_limits.html.haml b/app/views/admin/application_settings/_ip_limits.html.haml index b83ffc375d9..539ff9b5168 100644 --- a/app/views/admin/application_settings/_ip_limits.html.haml +++ b/app/views/admin/application_settings/_ip_limits.html.haml @@ -1,53 +1,53 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :throttle_unauthenticated_enabled do = f.check_box :throttle_unauthenticated_enabled Enable unauthenticated request rate limit - %span.help-block + %span.form-text.text-muted Helps reduce request volume (e.g. from crawlers or abusive bots) - .form-group - = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'control-label col-sm-2' + .form-group.row + = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control' - .form-group - = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2' + .form-group.row + = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control' - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :throttle_authenticated_api_enabled do = f.check_box :throttle_authenticated_api_enabled Enable authenticated API request rate limit - %span.help-block + %span.form-text.text-muted Helps reduce request volume (e.g. from crawlers or abusive bots) - .form-group - = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2' + .form-group.row + = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control' - .form-group - = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2' + .form-group.row + = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control' - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :throttle_authenticated_web_enabled do = f.check_box :throttle_authenticated_web_enabled Enable authenticated web request rate limit - %span.help-block + %span.form-text.text-muted Helps reduce request volume (e.g. from crawlers or abusive bots) - .form-group - = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2' + .form-group.row + = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control' - .form-group - = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2' + .form-group.row + = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control' diff --git a/app/views/admin/application_settings/_koding.html.haml b/app/views/admin/application_settings/_koding.html.haml index 17358cf775b..0532f49fd5b 100644 --- a/app/views/admin/application_settings/_koding.html.haml +++ b/app/views/admin/application_settings/_koding.html.haml @@ -1,20 +1,20 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :koding_enabled do = f.check_box :koding_enabled Enable Koding - .help-block + .form-text.text-muted Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again. - .form-group - = f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2' + .form-group.row + = f.label :koding_url, 'Koding URL', class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090' - .help-block + .form-text.text-muted Koding has integration enabled out of the box for the %strong gitlab team, and you need to provide that team's URL here. Learn more in the diff --git a/app/views/admin/application_settings/_logging.html.haml b/app/views/admin/application_settings/_logging.html.haml index 44a11ddc120..4341801b045 100644 --- a/app/views/admin/application_settings/_logging.html.haml +++ b/app/views/admin/application_settings/_logging.html.haml @@ -1,35 +1,35 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :sentry_enabled do = f.check_box :sentry_enabled Enable Sentry - .help-block + .form-text.text-muted %p This setting requires a restart to take effect. Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com - .form-group - = f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2' + .form-group.row + = f.label :sentry_dsn, 'Sentry DSN', class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :sentry_dsn, class: 'form-control' - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :clientside_sentry_enabled do = f.check_box :clientside_sentry_enabled Enable Clientside Sentry - .help-block + .form-text.text-muted Sentry can also be used for reporting and logging clientside exceptions. %a{ href: 'https://sentry.io/for/javascript/', target: '_blank', rel: 'noopener noreferrer' } https://sentry.io/for/javascript/ - .form-group - = f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'control-label col-sm-2' + .form-group.row + = f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :clientside_sentry_dsn, class: 'form-control' diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml index d10f609006d..176cdfe9e1a 100644 --- a/app/views/admin/application_settings/_outbound.html.haml +++ b/app/views/admin/application_settings/_outbound.html.haml @@ -1,10 +1,10 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :allow_local_requests_from_hooks_and_services do = f.check_box :allow_local_requests_from_hooks_and_services Allow requests to the local network from hooks and services diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml index b28ecf9a039..0100ef038e2 100644 --- a/app/views/admin/application_settings/_pages.html.haml +++ b/app/views/admin/application_settings/_pages.html.haml @@ -1,19 +1,19 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'control-label col-sm-2' + .form-group.row + = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :max_pages_size, class: 'form-control' - .help-block 0 for unlimited - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-text.text-muted 0 for unlimited + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :pages_domain_verification_enabled do = f.check_box :pages_domain_verification_enabled Require users to prove ownership of custom domains - .help-block + .form-text.text-muted Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml index 01d5a31aa9f..07d3a8b7cdf 100644 --- a/app/views/admin/application_settings/_performance.html.haml +++ b/app/views/admin/application_settings/_performance.html.haml @@ -1,14 +1,14 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :authorized_keys_enabled do = f.check_box :authorized_keys_enabled Write to "authorized_keys" file - .help-block + .form-text.text-muted By default, we write to the "authorized_keys" file to support Git over SSH without additional configuration. GitLab can be optimized to authenticate SSH keys via the database file. Only uncheck this diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml index 5344f030c97..8001b42e2f9 100644 --- a/app/views/admin/application_settings/_performance_bar.html.haml +++ b/app/views/admin/application_settings/_performance_bar.html.haml @@ -1,15 +1,15 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :performance_bar_enabled do = f.check_box :performance_bar_enabled Enable the Performance Bar - .form-group - = f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'control-label col-sm-2' + .form-group.row + = f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :performance_bar_allowed_group_id, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml index 56764b3fb81..ee6d1d1a888 100644 --- a/app/views/admin/application_settings/_plantuml.html.haml +++ b/app/views/admin/application_settings/_plantuml.html.haml @@ -1,18 +1,18 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :plantuml_enabled do = f.check_box :plantuml_enabled Enable PlantUML - .form-group - = f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2' + .form-group.row + = f.label :plantuml_url, 'PlantUML URL', class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080' - .help-block + .form-text.text-muted Allow rendering of = link_to "PlantUML", "http://plantuml.com" diagrams in Asciidoc documents using an external PlantUML service. diff --git a/app/views/admin/application_settings/_prometheus.html.haml b/app/views/admin/application_settings/_prometheus.html.haml index 48745db2991..8c95597e787 100644 --- a/app/views/admin/application_settings/_prometheus.html.haml +++ b/app/views/admin/application_settings/_prometheus.html.haml @@ -1,4 +1,4 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset @@ -11,14 +11,14 @@ = link_to 'restart', help_page_path('administration/restart_gitlab') to take effect. = link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/index') - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :prometheus_metrics_enabled do = f.check_box :prometheus_metrics_enabled Enable Prometheus Metrics - unless Gitlab::Metrics.metrics_folder_present? - .help-block + .form-text.text-muted %strong.cred WARNING: Environment variable %code prometheus_multiproc_dir diff --git a/app/views/admin/application_settings/_realtime.html.haml b/app/views/admin/application_settings/_realtime.html.haml index 0a53a75119e..63a592cc2fd 100644 --- a/app/views/admin/application_settings/_realtime.html.haml +++ b/app/views/admin/application_settings/_realtime.html.haml @@ -1,12 +1,12 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'control-label col-sm-2' + .form-group.row + = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :polling_interval_multiplier, class: 'form-control' - .help-block + .form-text.text-muted Change this value to influence how frequently the GitLab UI polls for updates. If you set the value to 2 all polling intervals are multiplied by 2, which means that polling happens half as frequently. diff --git a/app/views/admin/application_settings/_registry.html.haml b/app/views/admin/application_settings/_registry.html.haml index 3451ef62458..8524cbfe4d9 100644 --- a/app/views/admin/application_settings/_registry.html.haml +++ b/app/views/admin/application_settings/_registry.html.haml @@ -1,9 +1,9 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'control-label col-sm-2' + .form-group.row + = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :container_registry_token_expire_delay, class: 'form-control' diff --git a/app/views/admin/application_settings/_repository_check.html.haml b/app/views/admin/application_settings/_repository_check.html.haml index fe335f30a62..739ed359e16 100644 --- a/app/views/admin/application_settings/_repository_check.html.haml +++ b/app/views/admin/application_settings/_repository_check.html.haml @@ -1,62 +1,62 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset .sub-section %h4 Repository checks - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :repository_checks_enabled do = f.check_box :repository_checks_enabled Enable Repository Checks - .help-block + .form-text.text-muted GitLab will periodically run %a{ href: 'https://git-scm.com/docs/git-fsck', target: 'blank' } 'git fsck' in all project and wiki repositories to look for silent disk corruption issues. - .form-group - .col-sm-offset-2.col-sm-10 + .form-group.row + .offset-sm-2.col-sm-10 = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove" - .help-block + .form-text.text-muted If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database. .sub-section %h4 Housekeeping - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :housekeeping_enabled do = f.check_box :housekeeping_enabled Enable automatic repository housekeeping (git repack, git gc) - .help-block + .form-text.text-muted If you keep automatic housekeeping disabled for a long time Git repository access on your GitLab server will become slower and your repositories will use more disk space. We recommend to always leave this enabled. - .checkbox + .form-check = f.label :housekeeping_bitmaps_enabled do = f.check_box :housekeeping_bitmaps_enabled Enable Git pack file bitmap creation - .help-block + .form-text.text-muted Creating pack file bitmaps makes housekeeping take a little longer but bitmaps should accelerate 'git clone' performance. - .form-group - = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'control-label col-sm-2' + .form-group.row + = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :housekeeping_incremental_repack_period, class: 'form-control' - .help-block + .form-text.text-muted Number of Git pushes after which an incremental 'git repack' is run. - .form-group - = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'control-label col-sm-2' + .form-group.row + = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :housekeeping_full_repack_period, class: 'form-control' - .help-block + .form-text.text-muted Number of Git pushes after which a full 'git repack' is run. - .form-group - = f.label :housekeeping_gc_period, 'Git GC period', class: 'control-label col-sm-2' + .form-group.row + = f.label :housekeeping_gc_period, 'Git GC period', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :housekeeping_gc_period, class: 'form-control' - .help-block + .form-text.text-muted Number of Git pushes after which 'git gc' is run. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_repository_mirrors_form.html.haml b/app/views/admin/application_settings/_repository_mirrors_form.html.haml index 09183ec6260..9d05a5aa234 100644 --- a/app/views/admin/application_settings/_repository_mirrors_form.html.haml +++ b/app/views/admin/application_settings/_repository_mirrors_form.html.haml @@ -5,7 +5,7 @@ .form-group = f.label :mirror_available, 'Enable mirror configuration', class: 'control-label col-sm-2' .col-sm-10 - .checkbox + .form-check = f.label :mirror_available do = f.check_box :mirror_available Allow mirrors to be setup for projects diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml index 4eebb59110a..be85210934c 100644 --- a/app/views/admin/application_settings/_repository_storage.html.haml +++ b/app/views/admin/application_settings/_repository_storage.html.haml @@ -1,58 +1,58 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset .sub-section - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :hashed_storage_enabled do = f.check_box :hashed_storage_enabled Create new projects using hashed storage paths - .help-block + .form-text.text-muted Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance. %em (EXPERIMENTAL) - .form-group - = f.label :repository_storages, 'Storage paths for new projects', class: 'control-label col-sm-2' + .form-group.row + = f.label :repository_storages, 'Storage paths for new projects', class: 'col-form-label col-sm-2' .col-sm-10 = f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages), {include_hidden: false}, multiple: true, class: 'form-control' - .help-block + .form-text.text-muted Manage repository storage paths. Learn more in the = succeed "." do = link_to "repository storages documentation", help_page_path("administration/repository_storage_paths") .sub-section %h4 Circuit breaker - .form-group - = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'control-label col-sm-2' + .form-group.row + = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :circuitbreaker_check_interval, class: 'form-control' - .help-block + .form-text.text-muted = circuitbreaker_check_interval_help_text - .form-group - = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'control-label col-sm-2' + .form-group.row + = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :circuitbreaker_access_retries, class: 'form-control' - .help-block + .form-text.text-muted = circuitbreaker_access_retries_help_text - .form-group - = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'control-label col-sm-2' + .form-group.row + = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :circuitbreaker_storage_timeout, class: 'form-control' - .help-block + .form-text.text-muted = circuitbreaker_storage_timeout_help_text - .form-group - = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'control-label col-sm-2' + .form-group.row + = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control' - .help-block + .form-text.text-muted = circuitbreaker_failure_count_help_text - .form-group - = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'control-label col-sm-2' + .form-group.row + = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control' - .help-block + .form-text.text-muted = circuitbreaker_failure_reset_time_help_text = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml index 48331c40bca..4d74568d69a 100644 --- a/app/views/admin/application_settings/_signin.html.haml +++ b/app/views/admin/application_settings/_signin.html.haml @@ -1,60 +1,60 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :password_authentication_enabled_for_web do = f.check_box :password_authentication_enabled_for_web Password authentication enabled for web interface - .help-block + .form-text.text-muted When disabled, an external authentication provider must be used. - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :password_authentication_enabled_for_git do = f.check_box :password_authentication_enabled_for_git Password authentication enabled for Git over HTTP(S) - .help-block + .form-text.text-muted When disabled, a Personal Access Token - if Gitlab::Auth::LDAP::Config.enabled? or LDAP password must be used to authenticate. - if omniauth_enabled? && button_based_providers.any? - .form-group + .form-group.row = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2' = hidden_field_tag 'application_setting[enabled_oauth_sign_in_sources][]' .col-sm-10 .btn-group{ data: { toggle: 'buttons' } } - oauth_providers_checkboxes.each do |source| = source - .form-group - = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' + .form-group.row + = f.label :two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2' .col-sm-10 - .checkbox + .form-check = f.label :require_two_factor_authentication do = f.check_box :require_two_factor_authentication Require all users to setup Two-factor authentication - .form-group - = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'control-label col-sm-2' + .form-group.row + = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0' - .help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication - .form-group - = f.label :home_page_url, 'Home page URL', class: 'control-label col-sm-2' + .form-text.text-muted Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication + .form-group.row + = f.label :home_page_url, 'Home page URL', class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block' - %span.help-block#home_help_block We will redirect non-logged in users to this page - .form-group - = f.label :after_sign_out_path, class: 'control-label col-sm-2' + %span.form-text.text-muted#home_help_block We will redirect non-logged in users to this page + .form-group.row + = f.label :after_sign_out_path, class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :after_sign_out_path, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block' - %span.help-block#after_sign_out_path_help_block We will redirect users to this page after they sign out - .form-group - = f.label :sign_in_text, class: 'control-label col-sm-2' + %span.form-text.text-muted#after_sign_out_path_help_block We will redirect users to this page after they sign out + .form-group.row + = f.label :sign_in_text, class: 'col-form-label col-sm-2' .col-sm-10 = f.text_area :sign_in_text, class: 'form-control', rows: 4 - .help-block Markdown enabled + .form-text.text-muted Markdown enabled = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_signup.html.haml b/app/views/admin/application_settings/_signup.html.haml index 85f311dd894..50c455f8686 100644 --- a/app/views/admin/application_settings/_signup.html.haml +++ b/app/views/admin/application_settings/_signup.html.haml @@ -1,58 +1,58 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :signup_enabled do = f.check_box :signup_enabled Sign-up enabled - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :send_user_confirmation_email do = f.check_box :send_user_confirmation_email Send confirmation email on sign-up - .form-group - = f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'control-label col-sm-2' + .form-group.row + = f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'col-form-label col-sm-2' .col-sm-10 = f.text_area :domain_whitelist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8 - .help-block ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com - .form-group - = f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'control-label col-sm-2' + .form-text.text-muted ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com + .form-group.row + = f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'col-form-label col-sm-2' .col-sm-10 - .checkbox + .form-check = f.label :domain_blacklist_enabled do = f.check_box :domain_blacklist_enabled Enable domain blacklist for sign ups - .form-group - .col-sm-offset-2.col-sm-10 - .radio + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = label_tag :blacklist_type_file do = radio_button_tag :blacklist_type, :file .option-title Upload blacklist file - .radio + .form-check = label_tag :blacklist_type_raw do = radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank? .option-title Enter blacklist manually - .form-group.blacklist-file - = f.label :domain_blacklist_file, 'Blacklist file', class: 'control-label col-sm-2' + .form-group.row.blacklist-file + = f.label :domain_blacklist_file, 'Blacklist file', class: 'col-form-label col-sm-2' .col-sm-10 = f.file_field :domain_blacklist_file, class: 'form-control', accept: '.txt,.conf' - .help-block Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries. - .form-group.blacklist-raw - = f.label :domain_blacklist, 'Blacklisted domains for sign-ups', class: 'control-label col-sm-2' + .form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries. + .form-group.row.blacklist-raw + = f.label :domain_blacklist, 'Blacklisted domains for sign-ups', class: 'col-form-label col-sm-2' .col-sm-10 = f.text_area :domain_blacklist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8 - .help-block Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com + .form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com - .form-group - = f.label :after_sign_up_text, class: 'control-label col-sm-2' + .form-group.row + = f.label :after_sign_up_text, class: 'col-form-label col-sm-2' .col-sm-10 = f.text_area :after_sign_up_text, class: 'form-control', rows: 4 - .help-block Markdown enabled + .form-text.text-muted Markdown enabled = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml index 25e89097dfe..58543a0359a 100644 --- a/app/views/admin/application_settings/_spam.html.haml +++ b/app/views/admin/application_settings/_spam.html.haml @@ -1,65 +1,65 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :recaptcha_enabled do = f.check_box :recaptcha_enabled Enable reCAPTCHA - %span.help-block#recaptcha_help_block Helps prevent bots from creating accounts + %span.form-text.text-muted#recaptcha_help_block Helps prevent bots from creating accounts - .form-group - = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2' + .form-group.row + = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :recaptcha_site_key, class: 'form-control' - .help-block + .form-text.text-muted Generate site and private keys at %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha - .form-group - = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2' + .form-group.row + = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :recaptcha_private_key, class: 'form-control' - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :akismet_enabled do = f.check_box :akismet_enabled Enable Akismet - %span.help-block#akismet_help_block Helps prevent bots from creating issues + %span.form-text.text-muted#akismet_help_block Helps prevent bots from creating issues - .form-group - = f.label :akismet_api_key, 'Akismet API Key', class: 'control-label col-sm-2' + .form-group.row + = f.label :akismet_api_key, 'Akismet API Key', class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :akismet_api_key, class: 'form-control' - .help-block + .form-text.text-muted Generate API key at %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :unique_ips_limit_enabled do = f.check_box :unique_ips_limit_enabled Limit sign in from multiple ips - %span.help-block#unique_ip_help_block + %span.form-text.text-muted#unique_ip_help_block Helps prevent malicious users hide their activity - .form-group - = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'control-label col-sm-2' + .form-group.row + = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :unique_ips_limit_per_user, class: 'form-control' - .help-block + .form-text.text-muted Maximum number of unique IPs per user - .form-group - = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'control-label col-sm-2' + .form-group.row + = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :unique_ips_limit_time_window, class: 'form-control' - .help-block + .form-text.text-muted How many seconds an IP will be counted towards the limit = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_terminal.html.haml b/app/views/admin/application_settings/_terminal.html.haml index 36d8838803f..ae02d07e556 100644 --- a/app/views/admin/application_settings/_terminal.html.haml +++ b/app/views/admin/application_settings/_terminal.html.haml @@ -1,12 +1,12 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - = f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2' + .form-group.row + = f.label :terminal_max_session_time, 'Max session time', class: 'col-form-label col-sm-2' .col-sm-10 = f.number_field :terminal_max_session_time, class: 'form-control' - .help-block + .form-text.text-muted Maximum time for web terminal websocket connection (in seconds). 0 for unlimited. diff --git a/app/views/admin/application_settings/_terms.html.haml b/app/views/admin/application_settings/_terms.html.haml index 724246ab7e7..32b060972ec 100644 --- a/app/views/admin/application_settings/_terms.html.haml +++ b/app/views/admin/application_settings/_terms.html.haml @@ -4,7 +4,7 @@ %fieldset .form-group .col-sm-12 - .checkbox + .form-check = f.label :enforce_terms do = f.check_box :enforce_terms = _("Require all users to accept Terms of Service when they access GitLab.") diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml index 7684e2cfdd1..316c8b04dea 100644 --- a/app/views/admin/application_settings/_usage.html.haml +++ b/app/views/admin/application_settings/_usage.html.haml @@ -1,25 +1,25 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :version_check_enabled do = f.check_box :version_check_enabled Enable version check - .help-block + .form-text.text-muted GitLab will inform you if a new version is available. = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check") about what information is shared with GitLab Inc. - .form-group - .col-sm-offset-2.col-sm-10 + .form-group.row + .offset-sm-2.col-sm-10 - can_be_configured = @application_setting.usage_ping_can_be_configured? - .checkbox + .form-check = f.label :usage_ping_enabled do = f.check_box :usage_ping_enabled, disabled: !can_be_configured Enable usage ping - .help-block + .form-text.text-muted - if can_be_configured To help improve GitLab and its user experience, GitLab will periodically collect usage information. diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml index a75dd90fe6b..c37a89237f0 100644 --- a/app/views/admin/application_settings/_visibility_and_access.html.haml +++ b/app/views/admin/application_settings/_visibility_and_access.html.haml @@ -1,41 +1,41 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group - = f.label :default_branch_protection, class: 'control-label col-sm-2' + .form-group.row + = f.label :default_branch_protection, class: 'col-form-label col-sm-2' .col-sm-10 = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control' - .form-group.visibility-level-setting - = f.label :default_project_visibility, class: 'control-label col-sm-2' + .form-group.row.visibility-level-setting + = f.label :default_project_visibility, class: 'col-form-label col-sm-2' .col-sm-10 = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new) - .form-group.visibility-level-setting - = f.label :default_snippet_visibility, class: 'control-label col-sm-2' + .form-group.row.visibility-level-setting + = f.label :default_snippet_visibility, class: 'col-form-label col-sm-2' .col-sm-10 = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new) - .form-group.visibility-level-setting - = f.label :default_group_visibility, class: 'control-label col-sm-2' + .form-group.row.visibility-level-setting + = f.label :default_group_visibility, class: 'col-form-label col-sm-2' .col-sm-10 = render('shared/visibility_radios', model_method: :default_group_visibility, form: f, selected_level: @application_setting.default_group_visibility, form_model: Group.new) - .form-group - = f.label :restricted_visibility_levels, class: 'control-label col-sm-2' + .form-group.row + = f.label :restricted_visibility_levels, class: 'col-form-label col-sm-2' .col-sm-10 - checkbox_name = 'application_setting[restricted_visibility_levels][]' = hidden_field_tag(checkbox_name) - restricted_level_checkboxes('restricted-visibility-help', checkbox_name).each do |level| - .checkbox + .form-check = level - %span.help-block#restricted-visibility-help + %span.form-text.text-muted#restricted-visibility-help Selected levels cannot be used by non-admin users for projects or snippets. If the public level is restricted, user profiles are only visible to logged in users. - .form-group - = f.label :import_sources, class: 'control-label col-sm-2' + .form-group.row + = f.label :import_sources, class: 'col-form-label col-sm-2' .col-sm-10 = hidden_field_tag 'application_setting[import_sources][]' - import_sources_checkboxes('import-sources-help').each do |source| - .checkbox= source - %span.help-block#import-sources-help + .form-check= source + %span.form-text.text-muted#import-sources-help Enabled sources for code import during project creation. OmniAuth must be configured for GitHub = link_to "(?)", help_page_path("integration/github") , Bitbucket @@ -43,24 +43,24 @@ and GitLab.com = link_to "(?)", help_page_path("integration/gitlab") - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.label :project_export_enabled do = f.check_box :project_export_enabled Project export enabled - .form-group - %label.control-label.col-sm-2 Enabled Git access protocols + .form-group.row + %label.col-form-label.col-sm-2 Enabled Git access protocols .col-sm-10 = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control') - %span.help-block#clone-protocol-help + %span.form-text.text-muted#clone-protocol-help Allow only the selected protocols to be used for Git access. - ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type| - field_name = :"#{type}_key_restriction" - .form-group - = f.label field_name, "#{type.upcase} SSH keys", class: 'control-label col-sm-2' + .form-group.row + = f.label field_name, "#{type.upcase} SSH keys", class: 'col-form-label col-sm-2' .col-sm-10 = f.select field_name, key_restriction_options_for_select(type), {}, class: 'form-control' diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml index 93827d6a1ab..b5d79e1da13 100644 --- a/app/views/admin/applications/_form.html.haml +++ b/app/views/admin/applications/_form.html.haml @@ -1,34 +1,34 @@ -= form_for [:admin, @application], url: @url, html: {class: 'form-horizontal', role: 'form'} do |f| += form_for [:admin, @application], url: @url, html: {role: 'form'} do |f| = form_errors(application) = content_tag :div, class: 'form-group' do - = f.label :name, class: 'col-sm-2 control-label' + = f.label :name, class: 'col-sm-2 col-form-label' .col-sm-10 = f.text_field :name, class: 'form-control' = doorkeeper_errors_for application, :name = content_tag :div, class: 'form-group' do - = f.label :redirect_uri, class: 'col-sm-2 control-label' + = f.label :redirect_uri, class: 'col-sm-2 col-form-label' .col-sm-10 = f.text_area :redirect_uri, class: 'form-control' = doorkeeper_errors_for application, :redirect_uri - %span.help-block + %span.form-text.text-muted Use one line per URI - if Doorkeeper.configuration.native_redirect_uri - %span.help-block + %span.form-text.text-muted Use %code= Doorkeeper.configuration.native_redirect_uri for local tests = content_tag :div, class: 'form-group' do - = f.label :trusted, class: 'col-sm-2 control-label' + = f.label :trusted, class: 'col-sm-2 col-form-label' .col-sm-10 = f.check_box :trusted - %span.help-block + %span.form-text.text-muted Trusted applications are automatically authorized on GitLab OAuth flow. - .form-group - = f.label :scopes, class: 'col-sm-2 control-label' + .form-group.row + = f.label :scopes, class: 'col-sm-2 col-form-label' .col-sm-10 = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml index 5125aa21b06..593a6d816e3 100644 --- a/app/views/admin/applications/show.html.haml +++ b/app/views/admin/applications/show.html.haml @@ -32,5 +32,5 @@ = render "shared/tokens/scopes_list", token: @application .form-actions - = link_to 'Edit', edit_admin_application_path(@application), class: 'btn btn-primary wide pull-left' + = link_to 'Edit', edit_admin_application_path(@application), class: 'btn btn-primary wide float-left' = render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10' diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml index f0cc4d7ee62..faa5854bb40 100644 --- a/app/views/admin/background_jobs/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -7,9 +7,9 @@ %hr - .panel.panel-default - .panel-heading Sidekiq running processes - .panel-body + .card + .card-header Sidekiq running processes + .card-body - if @sidekiq_processes.empty? %h4.cred %i.fa.fa-exclamation-triangle @@ -41,5 +41,5 @@ - .panel.panel-default + .card %iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: 0" } diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml index 5a4ed1c3a2a..7f34357f147 100644 --- a/app/views/admin/broadcast_messages/_form.html.haml +++ b/app/views/admin/broadcast_messages/_form.html.haml @@ -6,32 +6,32 @@ - else Your message here -= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f| += form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form js-quick-submit js-requires-input'} do |f| = form_errors(@broadcast_message) - .form-group - = f.label :message, class: 'control-label' + .form-group.row + = f.label :message, class: 'col-form-label col-sm-2' .col-sm-10 = f.text_area :message, class: "form-control js-autosize", required: true, data: { preview_path: preview_admin_broadcast_messages_path } - .form-group.js-toggle-colors-container - .col-sm-10.col-sm-offset-2 + .form-group.row.js-toggle-colors-container + .col-sm-10.offset-sm-2 = link_to 'Customize colors', '#', class: 'js-toggle-colors-link' - .form-group.js-toggle-colors-container.hide - = f.label :color, "Background Color", class: 'control-label' + .form-group.row.js-toggle-colors-container.toggle-colors.hide + = f.label :color, "Background Color", class: 'col-form-label col-sm-2' .col-sm-10 = f.color_field :color, class: "form-control" - .form-group.js-toggle-colors-container.hide - = f.label :font, "Font Color", class: 'control-label' + .form-group.row.js-toggle-colors-container.toggle-colors.hide + = f.label :font, "Font Color", class: 'col-form-label col-sm-2' .col-sm-10 = f.color_field :font, class: "form-control" - .form-group - = f.label :starts_at, class: 'control-label' + .form-group.row + = f.label :starts_at, _("Starts at (UTC)"), class: 'col-form-label col-sm-2' .col-sm-10.datetime-controls = f.datetime_select :starts_at, {}, class: 'form-control form-control-inline' - .form-group - = f.label :ends_at, class: 'control-label' + .form-group.row + = f.label :ends_at, _("Ends at (UTC)"), class: 'col-form-label col-sm-2' .col-sm-10.datetime-controls = f.datetime_select :ends_at, {}, class: 'form-control form-control-inline' .form-actions diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml index b806882eee3..9ef58faf8cc 100644 --- a/app/views/admin/broadcast_messages/index.html.haml +++ b/app/views/admin/broadcast_messages/index.html.haml @@ -32,7 +32,7 @@ %td = message.ends_at %td - = link_to icon('pencil-square-o'), edit_admin_broadcast_message_path(message), title: 'Edit', class: 'btn btn-xs' - = link_to icon('times'), admin_broadcast_message_path(message), method: :delete, remote: true, title: 'Remove', class: 'js-remove-tr btn btn-xs btn-danger' + = link_to icon('pencil-square-o'), edit_admin_broadcast_message_path(message), title: 'Edit', class: 'btn btn-sm' + = link_to icon('times'), admin_broadcast_message_path(message), method: :delete, remote: true, title: 'Remove', class: 'js-remove-tr btn btn-sm btn-danger' = paginate @broadcast_messages, theme: 'gitlab' diff --git a/app/views/admin/conversational_development_index/_card.html.haml b/app/views/admin/conversational_development_index/_card.html.haml index 6c8688e06ae..57eda06630b 100644 --- a/app/views/admin/conversational_development_index/_card.html.haml +++ b/app/views/admin/conversational_development_index/_card.html.haml @@ -3,20 +3,20 @@ .convdev-card-title %h3 = card.title - .text-light + .light-text = card.description - .card-scores - .card-score - .card-score-value + .board-card-scores + .board-card-score + .board-card-score-value = format_score(card.instance_score) - .card-score-name You - .card-score - .card-score-value + .board-card-score-name You + .board-card-score + .board-card-score-value = format_score(card.leader_score) - .card-score-name Lead - .card-score-big + .board-card-score-name Lead + .board-card-score-big = number_to_percentage(card.percentage_score, precision: 1) - .card-buttons + .board-card-buttons - if card.blog %a{ href: card.blog } = icon('info-circle', 'aria-hidden' => 'true') diff --git a/app/views/admin/conversational_development_index/_disabled.html.haml b/app/views/admin/conversational_development_index/_disabled.html.haml index 975d7df3da6..0a741b50960 100644 --- a/app/views/admin/conversational_development_index/_disabled.html.haml +++ b/app/views/admin/conversational_development_index/_disabled.html.haml @@ -1,5 +1,5 @@ .container.convdev-empty - .col-sm-6.col-sm-push-3.text-center + .col-sm-12.justify-content-center.text-center = custom_icon('convdev_no_index') %h4 Usage ping is not enabled %p diff --git a/app/views/admin/conversational_development_index/_no_data.html.haml b/app/views/admin/conversational_development_index/_no_data.html.haml index b23d2b5ec3a..d69c46194b4 100644 --- a/app/views/admin/conversational_development_index/_no_data.html.haml +++ b/app/views/admin/conversational_development_index/_no_data.html.haml @@ -1,5 +1,5 @@ .container.convdev-empty - .col-sm-6.col-sm-push-3.text-center + .col-sm-12.justify-content-center.text-center = custom_icon('convdev_no_data') %h4 Data is still calculating... %p diff --git a/app/views/admin/conversational_development_index/show.html.haml b/app/views/admin/conversational_development_index/show.html.haml index ed40e7b4d00..e3d1aa31dc2 100644 --- a/app/views/admin/conversational_development_index/show.html.haml +++ b/app/views/admin/conversational_development_index/show.html.haml @@ -21,11 +21,11 @@ score = link_to icon('question-circle', 'aria-hidden' => 'true'), help_page_path('user/admin_area/monitoring/convdev') - .convdev-cards.card-container + .convdev-cards.board-card-container - @metric.cards.each do |card| = render 'card', card: card - .convdev-steps.visible-lg + .convdev-steps.d-none.d-lg-block.d-xl-block - @metric.idea_to_production_steps.each_with_index do |step, index| .convdev-step{ class: "convdev-#{score_level(step.percentage_score)}-score" } = custom_icon("i2p_step_#{index + 1}") diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index bbf0e0fb95c..3df4ce93fa8 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -2,6 +2,8 @@ - breadcrumb_title "Dashboard" %div{ class: container_class } + = render_if_exists "admin/licenses/breakdown", license: @license + .admin-dashboard.prepend-top-default .row .col-sm-4 @@ -10,7 +12,7 @@ = link_to admin_projects_path do %h3.text-center Projects: - = number_with_delimiter(Project.cached_count) + = approximate_count_with_delimiters(Project) %hr = link_to('New project', new_project_path, class: "btn btn-new") .col-sm-4 @@ -19,7 +21,8 @@ = link_to admin_users_path do %h3.text-center Users: - = number_with_delimiter(User.count) + = approximate_count_with_delimiters(User) + = render_if_exists 'users_statistics' %hr = link_to 'New user', new_admin_user_path, class: "btn btn-new" .col-sm-4 @@ -28,7 +31,7 @@ = link_to admin_groups_path do %h3.text-center Groups: - = number_with_delimiter(Group.count) + = approximate_count_with_delimiters(Group) %hr = link_to 'New group', new_admin_group_path, class: "btn btn-new" .row @@ -38,35 +41,35 @@ %h4 Statistics %p Forks - %span.light.pull-right - = number_with_delimiter(ForkedProjectLink.count) + %span.light.float-right + = approximate_count_with_delimiters(ForkedProjectLink) %p Issues - %span.light.pull-right - = number_with_delimiter(Issue.count) + %span.light.float-right + = approximate_count_with_delimiters(Issue) %p Merge Requests - %span.light.pull-right - = number_with_delimiter(MergeRequest.count) + %span.light.float-right + = approximate_count_with_delimiters(MergeRequest) %p Notes - %span.light.pull-right - = number_with_delimiter(Note.count) + %span.light.float-right + = approximate_count_with_delimiters(Note) %p Snippets - %span.light.pull-right - = number_with_delimiter(Snippet.count) + %span.light.float-right + = approximate_count_with_delimiters(Snippet) %p SSH Keys - %span.light.pull-right - = number_with_delimiter(Key.count) + %span.light.float-right + = approximate_count_with_delimiters(Key) %p Milestones - %span.light.pull-right - = number_with_delimiter(Milestone.count) + %span.light.float-right + = approximate_count_with_delimiters(Milestone) %p Active Users - %span.light.pull-right + %span.light.float-right = number_with_delimiter(User.active.count) .col-md-4 .info-well @@ -75,44 +78,47 @@ - sign_up = "Sign up" %p{ "aria-label" => "#{sign_up}: status " + (allow_signup? ? "on" : "off") } = sign_up - %span.light.pull-right + %span.light.float-right = boolean_to_icon allow_signup? - ldap = "LDAP" %p{ "aria-label" => "#{ldap}: status " + (Gitlab.config.ldap.enabled ? "on" : "off") } = ldap - %span.light.pull-right + %span.light.float-right = boolean_to_icon Gitlab.config.ldap.enabled - gravatar = "Gravatar" %p{ "aria-label" => "#{gravatar}: status " + (gravatar_enabled? ? "on" : "off") } = gravatar - %span.light.pull-right + %span.light.float-right = boolean_to_icon gravatar_enabled? - omniauth = "OmniAuth" %p{ "aria-label" => "#{omniauth}: status " + (Gitlab.config.omniauth.enabled ? "on" : "off") } = omniauth - %span.light.pull-right + %span.light.float-right = boolean_to_icon Gitlab.config.omniauth.enabled - reply_email = "Reply by email" %p{ "aria-label" => "#{reply_email}: status " + (Gitlab::IncomingEmail.enabled? ? "on" : "off") } = reply_email - %span.light.pull-right + %span.light.float-right = boolean_to_icon Gitlab::IncomingEmail.enabled? + + = render_if_exists 'elastic_and_geo' + - container_reg = "Container Registry" %p{ "aria-label" => "#{container_reg}: status " + (Gitlab.config.registry.enabled ? "on" : "off") } = container_reg - %span.light.pull-right + %span.light.float-right = boolean_to_icon Gitlab.config.registry.enabled - gitlab_pages = 'GitLab Pages' - gitlab_pages_enabled = Gitlab.config.pages.enabled %p{ "aria-label" => "#{gitlab_pages}: status " + (gitlab_pages_enabled ? "on" : "off") } = gitlab_pages - %span.light.pull-right + %span.light.float-right = boolean_to_icon gitlab_pages_enabled - gitlab_shared_runners = 'Shared Runners' - gitlab_shared_runners_enabled = Gitlab.config.gitlab_ci.shared_runners_enabled %p{ "aria-label" => "#{gitlab_shared_runners}: status " + (gitlab_shared_runners_enabled ? "on" : "off") } = gitlab_shared_runners - %span.light.pull-right + %span.light.float-right = boolean_to_icon gitlab_shared_runners_enabled .col-md-4 .info-well @@ -120,41 +126,44 @@ %h4 Components - if Gitlab::CurrentSettings.version_check_enabled - .pull-right + .float-right = version_status_badge %p GitLab - %span.pull-right + %span.float-right = Gitlab::VERSION - = "(#{Gitlab::REVISION})" + = "(#{Gitlab.revision})" %p GitLab Shell - %span.pull-right + %span.float-right = Gitlab::Shell.new.version %p GitLab Workhorse - %span.pull-right + %span.float-right = gitlab_workhorse_version %p GitLab API - %span.pull-right + %span.float-right = API::API::version - if Gitlab.config.pages.enabled %p GitLab Pages - %span.pull-right + %span.float-right = Gitlab::Pages::VERSION + + = render_if_exists 'geo' + %p Ruby - %span.pull-right + %span.float-right #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} %p Rails - %span.pull-right + %span.float-right #{Rails::VERSION::STRING} %p = Gitlab::Database.adapter_name - %span.pull-right + %span.float-right = Gitlab::Database.version %p = link_to "Gitaly Servers", admin_gitaly_servers_path @@ -166,7 +175,7 @@ - @projects.each do |project| %p = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60' - %span.light.pull-right + %span.light.float-right #{time_ago_with_tooltip(project.created_at)} .col-md-4 .info-well @@ -176,7 +185,7 @@ %p = link_to [:admin, user], class: 'str-truncated-60' do = user.name - %span.light.pull-right + %span.light.float-right #{time_ago_with_tooltip(user.created_at)} .col-md-4 .info-well @@ -186,5 +195,5 @@ %p = link_to [:admin, group], class: 'str-truncated-60' do = group.full_name - %span.light.pull-right + %span.light.float-right #{time_ago_with_tooltip(group.created_at)} diff --git a/app/views/admin/deploy_keys/edit.html.haml b/app/views/admin/deploy_keys/edit.html.haml index 3a59282e578..b50adef362f 100644 --- a/app/views/admin/deploy_keys/edit.html.haml +++ b/app/views/admin/deploy_keys/edit.html.haml @@ -3,7 +3,7 @@ %hr %div - = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form form-horizontal' } do |f| + = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f| = render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key } .form-actions = f.submit 'Save changes', class: 'btn-save btn' diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml index 1420163fd5a..52ab8bae119 100644 --- a/app/views/admin/deploy_keys/index.html.haml +++ b/app/views/admin/deploy_keys/index.html.haml @@ -2,7 +2,7 @@ %h3.page-title.deploy-keys-title Public deploy keys (#{@deploy_keys.count}) - .pull-right + .float-right = link_to 'New deploy key', new_admin_deploy_key_path, class: 'btn btn-new btn-sm btn-inverted' - if @deploy_keys.any? @@ -29,6 +29,6 @@ %span.cgray added #{time_ago_with_tooltip(deploy_key.created_at)} %td - .pull-right + .float-right = link_to 'Edit', edit_admin_deploy_key_path(deploy_key), class: 'btn btn-sm' = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove delete-key' diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml index 13f5259698f..d4f8e340b69 100644 --- a/app/views/admin/deploy_keys/new.html.haml +++ b/app/views/admin/deploy_keys/new.html.haml @@ -3,7 +3,7 @@ %hr %div - = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form form-horizontal' } do |f| + = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f| = render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key } .form-actions = f.submit 'Create', class: 'btn-create btn' diff --git a/app/views/admin/gitaly_servers/index.html.haml b/app/views/admin/gitaly_servers/index.html.haml index 231f94dc95d..d0cf5761726 100644 --- a/app/views/admin/gitaly_servers/index.html.haml +++ b/app/views/admin/gitaly_servers/index.html.haml @@ -6,7 +6,7 @@ - if @gitaly_servers.any? .table-holder %table.table.responsive-table - %thead.hidden-sm.hidden-xs + %thead.d-none.d-sm-none.d-md-block %tr %th= _("Storage") %th= n_("Gitaly|Address") diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index d9f05003904..dc4dccc9e0d 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -1,23 +1,23 @@ -= form_for [:admin, @group], html: { class: "form-horizontal" } do |f| += form_for [:admin, @group] do |f| = form_errors(@group) = render 'shared/group_form', f: f - .form-group.group-description-holder - = f.label :avatar, "Group avatar", class: 'control-label' + .form-group.row.group-description-holder + = f.label :avatar, "Group avatar", class: 'col-form-label col-sm-2' .col-sm-10 = render 'shared/choose_group_avatar_button', f: f = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group - .form-group - .col-sm-offset-2.col-sm-10 + .form-group.row + .offset-sm-2.col-sm-10 = render 'shared/allow_request_access', form: f = render 'groups/group_admin_settings', f: f - if @group.new_record? - .form-group - .col-sm-offset-2.col-sm-10 + .form-group.row + .offset-sm-2.col-sm-10 .alert.alert-info = render 'shared/group_tips' .form-actions diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index 47cc2d4d27e..e7c70a6f187 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -5,7 +5,7 @@ = link_to 'Edit', admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn' = link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove' .stats - %span.badge + %span.badge.badge-pill = storage_counter(group.storage_size) %span @@ -20,7 +20,7 @@ = visibility_level_icon(group.visibility_level, fw: false) .avatar-container.s40 - = group_icon(group, class: "avatar s40 hidden-xs") + = group_icon(group, class: "avatar s40 d-none d-sm-block") .title = link_to [:admin, group], class: 'group-name' do = group.full_name diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 324f3c0a22f..5ec612d0c72 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -4,14 +4,14 @@ %h3.page-title Group: #{@group.full_name} - = link_to admin_group_edit_path(@group), class: "btn pull-right" do + = link_to admin_group_edit_path(@group), class: "btn float-right" do %i.fa.fa-pencil-square-o Edit %hr .row .col-md-6 - .panel.panel-default - .panel-heading + .card + .card-header Group info: %ul.well-list %li @@ -58,46 +58,46 @@ = group_lfs_status(@group) = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') - .panel.panel-default - .panel-heading - %h3.panel-title + .card + .card-header + %h3.card-title Projects - %span.badge + %span.badge.badge-pill #{@group.projects.count} %ul.well-list - @projects.each do |project| %li %strong = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project] - %span.badge + %span.badge.badge-pill = storage_counter(project.statistics.storage_size) - %span.pull-right.light + %span.float-right.light %span.monospace= project.full_path + '.git' - .panel-footer + .card-footer = paginate @projects, param_name: 'projects_page', theme: 'gitlab' - if @group.shared_projects.any? - .panel.panel-default - .panel-heading + .card + .card-header Projects shared with #{@group.name} - %span.badge + %span.badge.badge-pill #{@group.shared_projects.count} %ul.well-list - @group.shared_projects.sort_by(&:name).each do |project| %li %strong = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project] - %span.badge + %span.badge.badge-pill = storage_counter(project.statistics.storage_size) - %span.pull-right.light + %span.float-right.light %span.monospace= project.full_path + '.git' .col-md-6 - if can?(current_user, :admin_group_member, @group) - .panel.panel-default - .panel-heading + .card + .card-header Add user(s) to the group: - .panel-body.form-holder + .card-body.form-holder %p.light Read more about project permissions %strong= link_to "here", help_page_path("user/permissions"), class: "vlink" @@ -111,14 +111,14 @@ = button_tag 'Add users to group', class: "btn btn-create" = render 'shared/members/requests', membership_source: @group, requesters: @requesters, force_mobile_view: true - .panel.panel-default - .panel-heading + .card + .card-header %strong= @group.name group members - %span.badge= @group.members.size - .pull-right - = link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@group, :members]), class: "btn btn-xs" + %span.badge.badge-pill= @group.members.size + .float-right + = link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@group, :members]), class: "btn btn-sm" %ul.well-list.group-users-list.content-list.members-list = render partial: 'shared/members/member', collection: @members, as: :member, locals: { show_controls: false } - .panel-footer + .card-footer = paginate @members, param_name: 'members_page', theme: 'gitlab' diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml index e31fb58b205..d51ac854b04 100644 --- a/app/views/admin/health_check/show.html.haml +++ b/app/views/admin/health_check/show.html.haml @@ -5,7 +5,7 @@ %div{ class: container_class } %h3.page-title= page_title .bs-callout.clearfix - .pull-left + .float-left %p #{ s_('HealthCheck|Access token is') } %code#health-check-token= Gitlab::CurrentSettings.health_check_access_token @@ -25,8 +25,8 @@ %code= metrics_url(token: Gitlab::CurrentSettings.health_check_access_token) %hr - .panel.panel-default - .panel-heading + .card + .card-header Current Status: - if no_errors = icon('circle', class: 'cgreen') @@ -34,7 +34,7 @@ - else = icon('warning', class: 'cred') #{ s_('HealthCheck|Unhealthy') } - .panel-body + .card-body - if no_errors #{ s_('HealthCheck|No Health Problems Detected') } - else diff --git a/app/views/admin/hook_logs/_index.html.haml b/app/views/admin/hook_logs/_index.html.haml index 91a8c0c62fe..1d7c9930b6a 100644 --- a/app/views/admin/hook_logs/_index.html.haml +++ b/app/views/admin/hook_logs/_index.html.haml @@ -18,8 +18,8 @@ %tr %td = render partial: 'shared/hook_logs/status_label', locals: { hook_log: hook_log } - %td.hidden-xs - %span.label.label-gray.deploy-project-label + %td.d-none.d-sm-block + %span.badge.badge-gray.deploy-project-label = hook_log.trigger.singularize.titleize %td = truncate(hook_log.url, length: 50) diff --git a/app/views/admin/hook_logs/show.html.haml b/app/views/admin/hook_logs/show.html.haml index 56127bacda2..2eb3ac85722 100644 --- a/app/views/admin/hook_logs/show.html.haml +++ b/app/views/admin/hook_logs/show.html.haml @@ -4,7 +4,7 @@ %hr -= link_to 'Resend Request', retry_admin_hook_hook_log_path(@hook, @hook_log), class: "btn btn-default pull-right prepend-left-10" += link_to 'Resend Request', retry_admin_hook_hook_log_path(@hook, @hook_log), class: "btn btn-default float-right prepend-left-10" = render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log } diff --git a/app/views/admin/hooks/_form.html.haml b/app/views/admin/hooks/_form.html.haml index a6324a97fd5..e54dbd20ef4 100644 --- a/app/views/admin/hooks/_form.html.haml +++ b/app/views/admin/hooks/_form.html.haml @@ -6,39 +6,39 @@ .form-group = form.label :token, 'Secret Token', class: 'label-light' = form.text_field :token, class: 'form-control' - %p.help-block + %p.form-text.text-muted Use this token to validate received payloads .form-group = form.label :url, 'Trigger', class: 'label-light' %ul.list-unstyled %li - .help-block + .form-text.text-muted System hook will be triggered on set of events like creating project or adding ssh key. But you can also enable extra triggers like Push events. .prepend-top-default - = form.check_box :repository_update_events, class: 'pull-left' + = form.check_box :repository_update_events, class: 'float-left' .prepend-left-20 = form.label :repository_update_events, class: 'list-label' do %strong Repository update events %p.light This URL will be triggered when repository is updated %li - = form.check_box :push_events, class: 'pull-left' + = form.check_box :push_events, class: 'float-left' .prepend-left-20 = form.label :push_events, class: 'list-label' do %strong Push events %p.light This URL will be triggered for each branch updated to the repository %li - = form.check_box :tag_push_events, class: 'pull-left' + = form.check_box :tag_push_events, class: 'float-left' .prepend-left-20 = form.label :tag_push_events, class: 'list-label' do %strong Tag push events %p.light This URL will be triggered when a new tag is pushed to the repository %li - = form.check_box :merge_requests_events, class: 'pull-left' + = form.check_box :merge_requests_events, class: 'float-left' .prepend-left-20 = form.label :merge_requests_events, class: 'list-label' do %strong Merge request events @@ -46,7 +46,7 @@ This URL will be triggered when a merge request is created/updated/merged .form-group = form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox' - .checkbox + .form-check = form.label :enable_ssl_verification do = form.check_box :enable_ssl_verification %strong Enable SSL verification diff --git a/app/views/admin/hooks/edit.html.haml b/app/views/admin/hooks/edit.html.haml index 629b1a9940f..b9a650e1f1f 100644 --- a/app/views/admin/hooks/edit.html.haml +++ b/app/views/admin/hooks/edit.html.haml @@ -9,12 +9,12 @@ %hr -= form_for @hook, as: :hook, url: admin_hook_path, html: { class: 'form-horizontal' } do |f| += form_for @hook, as: :hook, url: admin_hook_path do |f| = render partial: 'form', locals: { form: f, hook: @hook } .form-actions = f.submit 'Save changes', class: 'btn btn-create' = render 'shared/web_hooks/test_button', triggers: SystemHook.triggers, hook: @hook - = link_to 'Remove', admin_hook_path(@hook), method: :delete, class: 'btn btn-remove pull-right', data: { confirm: 'Are you sure?' } + = link_to 'Remove', admin_hook_path(@hook), method: :delete, class: 'btn btn-remove float-right', data: { confirm: 'Are you sure?' } %hr diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index d9e2ce5e74c..87f9b0e86a7 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -15,8 +15,8 @@ %hr - if @hooks.any? - .panel.panel-default - .panel-heading + .card + .card-header System hooks (#{@hooks.count}) %ul.content-list - @hooks.each do |hook| @@ -29,7 +29,7 @@ %div - SystemHook.triggers.each_value do |event| - if hook.public_send(event) - %span.label.label-gray= event.to_s.titleize - %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'} + %span.badge.badge-gray= event.to_s.titleize + %span.badge.badge-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'} = render 'shared/plugins/index' diff --git a/app/views/admin/identities/_form.html.haml b/app/views/admin/identities/_form.html.haml index 5381b854f5c..231c0f70882 100644 --- a/app/views/admin/identities/_form.html.haml +++ b/app/views/admin/identities/_form.html.haml @@ -1,13 +1,13 @@ -= form_for [:admin, @user, @identity], html: { class: 'form-horizontal fieldset-form' } do |f| += form_for [:admin, @user, @identity], html: { class: 'fieldset-form' } do |f| = form_errors(@identity) - .form-group - = f.label :provider, class: 'control-label' + .form-group.row + = f.label :provider, class: 'col-form-label col-sm-2' .col-sm-10 - values = Gitlab::Auth::OAuth::Provider.providers.map { |name| ["#{Gitlab::Auth::OAuth::Provider.label_for(name)} (#{name})", name] } = f.select :provider, values, { allow_blank: false }, class: 'form-control' - .form-group - = f.label :extern_uid, "Identifier", class: 'control-label' + .form-group.row + = f.label :extern_uid, "Identifier", class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :extern_uid, class: 'form-control', required: true diff --git a/app/views/admin/identities/_identity.html.haml b/app/views/admin/identities/_identity.html.haml index ef5a3f1d969..50fe9478a78 100644 --- a/app/views/admin/identities/_identity.html.haml +++ b/app/views/admin/identities/_identity.html.haml @@ -4,9 +4,9 @@ %td = identity.extern_uid %td - = link_to edit_admin_user_identity_path(@user, identity), class: 'btn btn-xs btn-grouped' do + = link_to edit_admin_user_identity_path(@user, identity), class: 'btn btn-sm btn-grouped' do Edit = link_to [:admin, @user, identity], method: :delete, - class: 'btn btn-xs btn-danger', + class: 'btn btn-sm btn-danger', data: { confirm: "Are you sure you want to remove this identity?" } do Delete diff --git a/app/views/admin/identities/index.html.haml b/app/views/admin/identities/index.html.haml index ff67e59cdac..ee51fb3fda1 100644 --- a/app/views/admin/identities/index.html.haml +++ b/app/views/admin/identities/index.html.haml @@ -1,7 +1,7 @@ - page_title "Identities", @user.name, "Users" = render 'admin/users/head' -= link_to 'New identity', new_admin_user_identity_path, class: 'pull-right btn btn-new' += link_to 'New identity', new_admin_user_identity_path, class: 'float-right btn btn-new' - if @identities.present? .table-holder %table.table diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml index d5e6bede36a..ee51b44e83e 100644 --- a/app/views/admin/labels/_form.html.haml +++ b/app/views/admin/labels/_form.html.haml @@ -1,21 +1,22 @@ -= form_for [:admin, @label], html: { class: 'form-horizontal label-form js-requires-input' } do |f| += form_for [:admin, @label], html: { class: 'label-form js-requires-input' } do |f| = form_errors(@label) - .form-group - = f.label :title, class: 'control-label' + .form-group.row + = f.label :title, class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :title, class: "form-control", required: true - .form-group - = f.label :description, class: 'control-label' + .form-group.row + = f.label :description, class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :description, class: "form-control js-quick-submit" - .form-group - = f.label :color, "Background color", class: 'control-label' + .form-group.row + = f.label :color, "Background color", class: 'col-form-label col-sm-2' .col-sm-10 .input-group - .input-group-addon.label-color-preview + .input-group-prepend + .input-group-text.label-color-preview = f.text_field :color, class: "form-control" - .help-block + .form-text.text-muted Choose any color. %br Or you can choose one of suggested colors below diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml index 77b174fbb27..009a47dd517 100644 --- a/app/views/admin/labels/_label.html.haml +++ b/app/views/admin/labels/_label.html.haml @@ -2,6 +2,6 @@ .label-row = render_colored_label(label, tooltip: false) = markdown_field(label, :description) - .pull-right + .float-right = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm' = link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"} diff --git a/app/views/admin/labels/destroy.js.haml b/app/views/admin/labels/destroy.js.haml index 9d51762890f..7a0dcbdd1c6 100644 --- a/app/views/admin/labels/destroy.js.haml +++ b/app/views/admin/labels/destroy.js.haml @@ -1,2 +1,2 @@ - if @labels.size == 0 - $('.labels').load(document.URL + ' .light-well').hide().fadeIn(1000) + $('.labels').load(document.URL + ' .card.bg-light').hide().fadeIn(1000) diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml index 05d6b9ed238..add38fb333e 100644 --- a/app/views/admin/labels/index.html.haml +++ b/app/views/admin/labels/index.html.haml @@ -1,7 +1,7 @@ - page_title "Labels" %div - = link_to new_admin_label_path, class: "pull-right btn btn-nr btn-new" do + = link_to new_admin_label_path, class: "float-right btn btn-nr btn-new" do New label %h3.page-title Labels @@ -13,6 +13,6 @@ = render @labels = paginate @labels, theme: 'gitlab' - else - .light-well + .card.bg-light .nothing-here-block There are no labels yet diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index 78757b6384f..a6c436cd1f4 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -2,7 +2,7 @@ - page_title "Logs" %div{ class: container_class } - %ul.nav-links.log-tabs + %ul.nav-links.log-tabs.nav.nav-tabs - @loggers.each do |klass| %li{ class: active_when(klass == @loggers.first) }> = link_to klass.file_name, "##{klass.file_name_noext}", data: { toggle: 'tab' } @@ -15,7 +15,7 @@ .js-file-title.file-title %i.fa.fa-file = klass.file_name - .pull-right + .float-right = link_to '#', class: 'log-bottom' do %i.fa.fa-arrow-down Scroll down diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml index b5d7b889504..00933d726d9 100644 --- a/app/views/admin/projects/_projects.html.haml +++ b/app/views/admin/projects/_projects.html.haml @@ -12,10 +12,10 @@ = s_('AdminProjects|Delete') .stats - %span.badge + %span.badge.badge-pill = storage_counter(project.statistics.storage_size) - if project.archived - %span.label.label-warning archived + %span.badge.badge-warning archived .title = link_to [:admin, project.namespace.becomes(Namespace), project] do .dash-project-avatar diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index c37d8ac45b9..57de792f92d 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -15,7 +15,7 @@ - namespace = Namespace.find(params[:namespace_id]) - toggle_text = "#{namespace.kind}: #{namespace.full_path}" = dropdown_toggle(toggle_text, { toggle: 'dropdown', is_filter: 'true' }, { toggle_class: 'js-namespace-select large' }) - .dropdown-menu.dropdown-select.dropdown-menu-align-right + .dropdown-menu.dropdown-select.dropdown-menu-right = dropdown_title('Namespaces') = dropdown_filter("Search for Namespace") = dropdown_content @@ -25,7 +25,7 @@ New Project = button_tag "Search", class: "btn btn-primary btn-search hide" - %ul.nav-links + %ul.nav-links.nav.nav-tabs - opts = params[:visibility_level].present? ? {} : { page: admin_projects_path } = nav_link(opts) do = link_to admin_projects_path do diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 75ca5106fd5..29ec712b6b7 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -3,15 +3,15 @@ - page_title @project.full_name, "Projects" %h3.page-title Project: #{@project.full_name} - = link_to edit_project_path(@project), class: "btn btn-nr pull-right" do + = link_to edit_project_path(@project), class: "btn btn-nr float-right" do %i.fa.fa-pencil-square-o Edit %hr - if @project.last_repository_check_failed? .row .col-md-12 - .panel - .panel-heading.alert.alert-danger + .card + .card-header.alert.alert-danger Last repository check = "(#{time_ago_with_tooltip(@project.last_repository_check_at)})" failed. See @@ -19,8 +19,8 @@ for error messages. .row .col-md-6 - .panel.panel-default - .panel-heading + .card + .card-header Project info: %ul.well-list %li @@ -110,13 +110,13 @@ = visibility_level_icon(@project.visibility_level) = visibility_level_label(@project.visibility_level) - .panel.panel-default - .panel-heading + .card + .card-header Transfer project - .panel-body - = form_for @project, url: transfer_admin_project_path(@project), method: :put, html: { class: 'form-horizontal' } do |f| - .form-group - = f.label :new_namespace_id, "Namespace", class: 'control-label' + .card-body + = form_for @project, url: transfer_admin_project_path(@project), method: :put do |f| + .form-group.row + = f.label :new_namespace_id, "Namespace", class: 'col-form-label col-sm-2' .col-sm-10 .dropdown = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id' }, { toggle_class: 'js-namespace-select large' }) @@ -126,14 +126,14 @@ = dropdown_content = dropdown_loading - .form-group - .col-sm-offset-2.col-sm-10 + .form-group.row + .offset-sm-2.col-sm-10 = f.submit 'Transfer', class: 'btn btn-primary' - .panel.panel-default.repository-check - .panel-heading + .card.repository-check + .card-header Repository check - .panel-body + .card-body = form_for @project, url: repository_check_admin_project_path(@project), method: :post do |f| .form-group - if @project.last_repository_check_at.nil? @@ -158,29 +158,29 @@ .col-md-6 - if @group - .panel.panel-default - .panel-heading + .card + .card-header %strong= @group.name group members - %span.badge= @group_members.size - .pull-right - = link_to admin_group_path(@group), class: 'btn btn-xs' do + %span.badge.badge-pill= @group_members.size + .float-right + = link_to admin_group_path(@group), class: 'btn btn-sm' do = icon('pencil-square-o', text: 'Manage access') %ul.well-list.content-list.members-list = render partial: 'shared/members/member', collection: @group_members, as: :member, locals: { show_controls: false } - .panel-footer + .card-footer = paginate @group_members, param_name: 'group_members_page', theme: 'gitlab' = render 'shared/members/requests', membership_source: @project, requesters: @requesters, force_mobile_view: true - .panel.panel-default - .panel-heading + .card + .card-header %strong= @project.name project members - %span.badge= @project.users.size - .pull-right - = link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@project, :members]), class: "btn btn-xs" + %span.badge.badge-pill= @project.users.size + .float-right + = link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@project, :members]), class: "btn btn-sm" %ul.well-list.project_members.content-list.members-list = render partial: 'shared/members/member', collection: @project_members, as: :member, locals: { show_controls: false } - .panel-footer + .card-footer = paginate @project_members, param_name: 'project_members_page', theme: 'gitlab' diff --git a/app/views/admin/requests_profiles/index.html.haml b/app/views/admin/requests_profiles/index.html.haml index cb02a750490..adfc67d66d0 100644 --- a/app/views/admin/requests_profiles/index.html.haml +++ b/app/views/admin/requests_profiles/index.html.haml @@ -13,8 +13,8 @@ - if @profiles.present? .prepend-top-default - @profiles.each do |path, profiles| - .panel.panel-default.panel-small - .panel-heading + .card.card-small + .card-header %code= path %ul.content-list - profiles.each do |profile| diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml index 6e76e7c2768..a6cd39edcf0 100644 --- a/app/views/admin/runners/_runner.html.haml +++ b/app/views/admin/runners/_runner.html.haml @@ -1,15 +1,15 @@ %tr{ id: dom_id(runner) } %td - if runner.shared? - %span.label.label-success shared + %span.badge.badge-success shared - elsif runner.group_type? - %span.label.label-success group + %span.badge.badge-success group - else - %span.label.label-info specific + %span.badge.badge-info specific - if runner.locked? - %span.label.label-warning locked + %span.badge.badge-warning locked - unless runner.active? - %span.label.label-danger paused + %span.badge.badge-danger paused %td = link_to admin_runner_path(runner) do @@ -29,15 +29,15 @@ #{runner.builds.count(:all)} %td - runner.tag_list.sort.each do |tag| - %span.label.label-primary + %span.badge.badge-primary = tag %td - if runner.contacted_at - #{time_ago_in_words(runner.contacted_at)} ago + = time_ago_with_tooltip runner.contacted_at - else Never %td.admin-runner-btn-group-cell - .pull-right.btn-group + .float-right.btn-group = link_to admin_runner_path(runner), class: 'btn btn-sm btn-default has-tooltip', title: 'Edit', ref: 'tooltip', aria: { label: 'Edit' }, data: { placement: 'top', container: 'body'} do = icon('pencil') diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 1a3b5e58ed5..f38aeb151df 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -14,23 +14,23 @@ %span Each Runner can be in one of the following states: %ul %li - %span.label.label-success shared + %span.badge.badge-success shared \- Runner runs jobs from all unassigned projects %li - %span.label.label-success group + %span.badge.badge-success group \- Runner runs jobs from all unassigned projects in its group %li - %span.label.label-info specific + %span.badge.badge-info specific \- Runner runs jobs from assigned projects %li - %span.label.label-warning locked + %span.badge.badge-warning locked \- Runner cannot be assigned to other projects %li - %span.label.label-danger paused + %span.badge.badge-danger paused \- Runner will not receive any new jobs .bs-callout.clearfix - .pull-left + .float-left %p You can reset runners registration token by pressing a button below. .prepend-top-10 @@ -42,13 +42,13 @@ locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token } .append-bottom-20.clearfix - .pull-left + .float-left = form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do .form-group - = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token', spellcheck: false + = search_field_tag :search, params[:search], class: 'form-control input-short', placeholder: 'Runner description or token', spellcheck: false = submit_tag 'Search', class: 'btn' - .pull-right.light + .float-right.light Runners with last contact more than a minute ago: #{@active_runners_cnt} %br diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 73fadc042b1..8a0c2bf4c5f 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -1,7 +1,7 @@ = content_for :title do %h3.project-title Runner ##{@runner.id} - .pull-right + .float-right - if @runner.shared? %span.runner-state.runner-state-shared Shared @@ -48,8 +48,8 @@ %strong = project.full_name %td - .pull-right - = link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-xs' + .float-right + = link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-sm' %table.table.unassigned-projects %thead @@ -70,10 +70,10 @@ %td = project.full_name %td - .pull-right + .float-right = form_for [:admin, project.namespace.becomes(Namespace), project, project.runner_projects.new] do |f| = f.hidden_field :runner_id, value: @runner.id - = f.submit 'Enable', class: 'btn btn-xs' + = f.submit 'Enable', class: 'btn btn-sm' = paginate @projects, theme: "gitlab" .col-md-6 diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml index e5b8ebdf613..144dceacbdd 100644 --- a/app/views/admin/services/_form.html.haml +++ b/app/views/admin/services/_form.html.haml @@ -3,7 +3,7 @@ %p #{@service.description} template -= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'form-horizontal fieldset-form' } do |form| += form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'fieldset-form' } do |form| = render 'shared/service_settings', form: form, subject: @service .footer-block.row-content-block diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml index ea6a0c4fb77..9d47dc1cce5 100644 --- a/app/views/admin/spam_logs/_spam_log.html.haml +++ b/app/views/admin/spam_logs/_spam_log.html.haml @@ -24,16 +24,16 @@ %td - if user = link_to 'Remove user', admin_spam_log_path(spam_log, remove_user: true), - data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove" + data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-sm btn-remove" %td - if spam_log.submitted_as_ham? - .btn.btn-xs.disabled + .btn.btn-sm.disabled Submitted as ham - else - = link_to 'Submit as ham', mark_as_ham_admin_spam_log_path(spam_log), method: :post, class: 'btn btn-xs btn-warning' + = link_to 'Submit as ham', mark_as_ham_admin_spam_log_path(spam_log), method: :post, class: 'btn btn-sm btn-warning' - if user && !user.blocked? - = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs" + = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-sm" - else - .btn.btn-xs.disabled + .btn.btn-sm.disabled Already blocked - = link_to 'Remove log', [:admin, spam_log], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr" + = link_to 'Remove log', [:admin, spam_log], remote: true, method: :delete, class: "btn btn-sm btn-close js-remove-tr" diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml index 23f9927cfee..b19934e028d 100644 --- a/app/views/admin/system_info/show.html.haml +++ b/app/views/admin/system_info/show.html.haml @@ -5,7 +5,7 @@ .prepend-top-default .row .col-sm-4 - .light-well + .card.bg-light.light-well %h4 CPU .data - if @cpus @@ -14,7 +14,7 @@ = icon('warning', class: 'text-warning') Unable to collect CPU info .col-sm-4 - .light-well + .card.bg-light.light-well %h4 Memory Usage .data - if @memory @@ -23,7 +23,7 @@ = icon('warning', class: 'text-warning') Unable to collect memory info .col-sm-4 - .light-well + .card.bg-light.light-well %h4 Disk Usage .data - @disks.each do |disk| @@ -31,7 +31,7 @@ %p= disk[:disk_name] %p= disk[:mount_path] .col-sm-4 - .light-well + .card.bg-light.light-well %h4 Uptime .data %h1= distance_of_time_in_words_to_now(Rails.application.config.booted_at) diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml index 794aaec89bd..35a331283ab 100644 --- a/app/views/admin/users/_access_levels.html.haml +++ b/app/views/admin/users/_access_levels.html.haml @@ -1,15 +1,15 @@ %fieldset %legend Access .form-group - = f.label :projects_limit, class: 'control-label' + = f.label :projects_limit, class: 'col-form-label' .col-sm-10= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control' .form-group - = f.label :can_create_group, class: 'control-label' + = f.label :can_create_group, class: 'col-form-label' .col-sm-10= f.check_box :can_create_group .form-group - = f.label :access_level, class: 'control-label' + = f.label :access_level, class: 'col-form-label' .col-sm-10 - editing_current_user = (current_user == @user) @@ -29,7 +29,7 @@ You cannot remove your own admin rights. .form-group - = f.label :external, class: 'control-label' + = f.label :external, class: 'col-form-label' .col-sm-10 = f.check_box :external do External diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index e911af3f6f9..010cb2ac354 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -1,21 +1,21 @@ .user_new - = form_for [:admin, @user], html: { class: 'form-horizontal fieldset-form' } do |f| + = form_for [:admin, @user], html: { class: 'fieldset-form' } do |f| = form_errors(@user) %fieldset %legend Account - .form-group - = f.label :name, class: 'control-label' + .form-group.row + = f.label :name, class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :name, required: true, autocomplete: 'off', class: 'form-control' %span.help-inline * required - .form-group - = f.label :username, class: 'control-label' + .form-group.row + = f.label :username, class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control' %span.help-inline * required - .form-group - = f.label :email, class: 'control-label' + .form-group.row + = f.label :email, class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :email, required: true, autocomplete: 'off', class: 'form-control' %span.help-inline * required @@ -23,8 +23,8 @@ - if @user.new_record? %fieldset %legend Password - .form-group - = f.label :password, class: 'control-label' + .form-group.row + = f.label :password, class: 'col-form-label col-sm-2' .col-sm-10 %strong Reset link will be generated and sent to the user. @@ -33,33 +33,33 @@ - else %fieldset %legend Password - .form-group - = f.label :password, class: 'control-label' + .form-group.row + = f.label :password, class: 'col-form-label col-sm-2' .col-sm-10= f.password_field :password, disabled: f.object.force_random_password, class: 'form-control' - .form-group - = f.label :password_confirmation, class: 'control-label' + .form-group.row + = f.label :password_confirmation, class: 'col-form-label col-sm-2' .col-sm-10= f.password_field :password_confirmation, disabled: f.object.force_random_password, class: 'form-control' = render partial: 'access_levels', locals: { f: f } %fieldset %legend Profile - .form-group - = f.label :avatar, class: 'control-label' + .form-group.row + = f.label :avatar, class: 'col-form-label col-sm-2' .col-sm-10 = f.file_field :avatar - .form-group - = f.label :skype, class: 'control-label' + .form-group.row + = f.label :skype, class: 'col-form-label col-sm-2' .col-sm-10= f.text_field :skype, class: 'form-control' - .form-group - = f.label :linkedin, class: 'control-label' + .form-group.row + = f.label :linkedin, class: 'col-form-label col-sm-2' .col-sm-10= f.text_field :linkedin, class: 'form-control' - .form-group - = f.label :twitter, class: 'control-label' + .form-group.row + = f.label :twitter, class: 'col-form-label' .col-sm-10= f.text_field :twitter, class: 'form-control' - .form-group - = f.label :website_url, 'Website', class: 'control-label' + .form-group.row + = f.label :website_url, 'Website', class: 'col-form-label col-sm-2' .col-sm-10= f.text_field :website_url, class: 'form-control' .form-actions diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml index be41c33b853..bfbc16d37a0 100644 --- a/app/views/admin/users/_head.html.haml +++ b/app/views/admin/users/_head.html.haml @@ -7,14 +7,14 @@ - if @user.admin %span.cred (Admin) - .pull-right + .float-right - if @user != current_user && @user.can?(:log_in) = link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-nr btn-grouped btn-info" = link_to edit_admin_user_path(@user), class: "btn btn-nr btn-grouped" do %i.fa.fa-pencil-square-o Edit %hr -%ul.nav-links +%ul.nav-links.nav.nav-tabs = nav_link(path: 'users#show') do = link_to "Account", admin_user_path(@user) = nav_link(path: 'users#projects') do diff --git a/app/views/admin/users/_profile.html.haml b/app/views/admin/users/_profile.html.haml index 6bc217f84cc..af22652e07c 100644 --- a/app/views/admin/users/_profile.html.haml +++ b/app/views/admin/users/_profile.html.haml @@ -1,5 +1,5 @@ -.panel.panel-default - .panel-heading +.card + .card-header Profile %ul.well-list %li diff --git a/app/views/admin/users/_projects.html.haml b/app/views/admin/users/_projects.html.haml index a126a858ea8..81cfb71af16 100644 --- a/app/views/admin/users/_projects.html.haml +++ b/app/views/admin/users/_projects.html.haml @@ -1,13 +1,13 @@ - if local_assigns.has_key?(:contributed_projects) && contributed_projects.present? - .panel.panel-default.contributed-projects - .panel-heading Projects contributed to + .card.contributed-projects + .card-header Projects contributed to = render 'shared/projects/list', projects: contributed_projects.sort_by(&:star_count).reverse, projects_limit: 5, stars: true, avatar: false - if local_assigns.has_key?(:projects) && projects.present? - .panel.panel-default - .panel-heading Personal projects + .card + .card-header Personal projects = render 'shared/projects/list', projects: projects.sort_by(&:star_count).reverse, projects_limit: 10, stars: true, avatar: false diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml index badf3dd74b3..5e176b61d68 100644 --- a/app/views/admin/users/_user.html.haml +++ b/app/views/admin/users/_user.html.haml @@ -5,11 +5,11 @@ .user-name.row-title.str-truncated-100 = link_to user.name, [:admin, user] - if user.blocked? - %span.label.label-danger blocked + %span.badge.badge-danger blocked - if user.admin? - %span.label.label-success Admin + %span.badge.badge-success Admin - if user.external? - %span.label.label-default External + %span.badge.badge-secondary External - if user == current_user %span It's you! .row-second-line.str-truncated-100 @@ -21,7 +21,7 @@ %a.dropdown-new.btn.btn-default#project-settings-button{ href: '#', data: { toggle: 'dropdown' } } = icon('cog') = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right + %ul.dropdown-menu.dropdown-menu-right %li.dropdown-header Settings %li @@ -38,7 +38,7 @@ %li.divider - if user.can_be_removed? %li - %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal', + %button.delete-user-button.btn.btn-danger{ data: { toggle: 'modal', target: '#delete-user-modal', delete_user_url: admin_user_path(user), block_user_url: block_admin_user_path(user), @@ -47,7 +47,7 @@ = s_('AdminUsers|Delete user') %li - %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal', + %button.delete-user-button.btn.btn-danger{ data: { toggle: 'modal', target: '#delete-user-modal', delete_user_url: admin_user_path(user, hard_delete: true), block_user_url: block_admin_user_path(user), diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 10b8bf5d565..faeb82656ba 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -13,7 +13,7 @@ .dropdown - toggle_text = if @sort.present? then sort_options_hash[@sort] else sort_title_name end = dropdown_toggle(toggle_text, { toggle: 'dropdown' }) - %ul.dropdown-menu.dropdown-menu-align-right + %ul.dropdown-menu.dropdown-menu-right %li.dropdown-header Sort by %li @@ -38,35 +38,35 @@ = icon('angle-left') .fade-right = icon('angle-right') - %ul.nav-links.scrolling-tabs + %ul.nav-links.nav.nav-tabs.scrolling-tabs = nav_link(html_options: { class: active_when(params[:filter].nil?) }) do = link_to admin_users_path do Active - %small.badge= limited_counter_with_delimiter(User.active) + %small.badge.badge-pill= limited_counter_with_delimiter(User.active) = nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do = link_to admin_users_path(filter: "admins") do Admins - %small.badge= limited_counter_with_delimiter(User.admins) + %small.badge.badge-pill= limited_counter_with_delimiter(User.admins) = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_enabled')} filter-two-factor-enabled" }) do = link_to admin_users_path(filter: 'two_factor_enabled') do 2FA Enabled - %small.badge= limited_counter_with_delimiter(User.with_two_factor) + %small.badge.badge-pill= limited_counter_with_delimiter(User.with_two_factor) = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_disabled')} filter-two-factor-disabled" }) do = link_to admin_users_path(filter: 'two_factor_disabled') do 2FA Disabled - %small.badge= limited_counter_with_delimiter(User.without_two_factor) + %small.badge.badge-pill= limited_counter_with_delimiter(User.without_two_factor) = nav_link(html_options: { class: active_when(params[:filter] == 'external') }) do = link_to admin_users_path(filter: 'external') do External - %small.badge= limited_counter_with_delimiter(User.external) + %small.badge.badge-pill= limited_counter_with_delimiter(User.external) = nav_link(html_options: { class: active_when(params[:filter] == 'blocked') }) do = link_to admin_users_path(filter: "blocked") do Blocked - %small.badge= limited_counter_with_delimiter(User.blocked) + %small.badge.badge-pill= limited_counter_with_delimiter(User.blocked) = nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do = link_to admin_users_path(filter: "wop") do Without projects - %small.badge= limited_counter_with_delimiter(User.without_projects) + %small.badge.badge-pill= limited_counter_with_delimiter(User.without_projects) %ul.flex-list.content-list - if @users.empty? diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml index 96835ee9af5..469a7bd9715 100644 --- a/app/views/admin/users/projects.html.haml +++ b/app/views/admin/users/projects.html.haml @@ -2,19 +2,19 @@ = render 'admin/users/head' - if @user.groups.any? - .panel.panel-default - .panel-heading Group projects - %ul.well-list + .card + .card-header Group projects + %ul.card-body-list - @user.group_members.includes(:source).each do |group_member| - group = group_member.group %li.group_member %strong= link_to group.name, admin_group_path(group) – access to #{pluralize(group.projects.count, 'project')} - .pull-right + .float-right %span.light.vertical-align-middle= group_member.human_access - unless group_member.owner? - = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove prepend-left-10", title: 'Remove user from group' do + = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member) }, method: :delete, remote: true, class: "btn-sm btn btn-remove prepend-left-10", title: 'Remove user from group' do %i.fa.fa-times.fa-inverse .row @@ -26,9 +26,9 @@ .col-md-6 - .panel.panel-default - .panel-heading Joined projects (#{@joined_projects.count}) - %ul.well-list + .card + .card-header Joined projects (#{@joined_projects.count}) + %ul.card-body-list - @joined_projects.sort_by(&:full_name).each do |project| - member = project.team.find_member(@user.id) %li.project_member @@ -37,12 +37,12 @@ = project.full_name - if member - .pull-right + .float-right - if member.owner? %span.light Owner - else %span.light.vertical-align-middle= member.human_access - if member.respond_to? :project - = link_to project_project_member_path(project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove prepend-left-10", title: 'Remove user from project' do + = link_to project_project_member_path(project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-sm btn btn-remove prepend-left-10", title: 'Remove user from project' do %i.fa.fa-times diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 814ccdae8f3..a74fcea65d8 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -5,8 +5,8 @@ .row .col-md-6 - .panel.panel-default - .panel-heading + .card + .card-header = @user.name %ul.well-list %li @@ -18,8 +18,8 @@ = @user.username = render 'admin/users/profile', user: @user - .panel.panel-default - .panel-heading + .card + .card-header Account: %ul.well-list %li @@ -37,7 +37,7 @@ %li %span.light Secondary email: %strong= email.email - = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-xs btn btn-remove pull-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do + = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-sm btn btn-remove float-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do %i.fa.fa-times %li.two-factor-status @@ -45,7 +45,7 @@ %strong{ class: @user.two_factor_enabled? ? 'cgreen' : 'cred' } - if @user.two_factor_enabled? Enabled - = link_to 'Disable', disable_two_factor_admin_user_path(@user), data: {confirm: 'Are you sure?'}, method: :patch, class: 'btn btn-xs btn-remove pull-right', title: 'Disable Two-factor Authentication' + = link_to 'Disable', disable_two_factor_admin_user_path(@user), data: {confirm: 'Are you sure?'}, method: :patch, class: 'btn btn-sm btn-remove float-right', title: 'Disable Two-factor Authentication' - else Disabled @@ -128,20 +128,20 @@ .col-md-6 - unless @user == current_user - unless @user.confirmed? - .panel.panel-info - .panel-heading + .card.bg-info + .card-header Confirm user - .panel-body + .card-body - if @user.unconfirmed_email.present? - email = " (#{@user.unconfirmed_email})" %p This user has an unconfirmed email address#{email}. You may force a confirmation. %br = link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } - if @user.blocked? - .panel.panel-info - .panel-heading + .card.bg-info + .card-header This user is blocked - .panel-body + .card-body %p A blocked user cannot: %ul %li Log in @@ -149,10 +149,10 @@ %br = link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } - else - .panel.panel-warning - .panel-heading + .card.bg-warning + .card-header Block this user - .panel-body + .card-body %p Blocking user has the following effects: %ul %li User will not be able to login @@ -162,23 +162,23 @@ %br = link_to 'Block user', block_admin_user_path(@user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put, class: "btn btn-warning" - if @user.access_locked? - .panel.panel-info - .panel-heading + .card.bg-info + .card-header This account has been locked - .panel-body + .card-body %p This user has been temporarily locked due to excessive number of failed logins. You may manually unlock the account. %br = link_to 'Unlock user', unlock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } - .panel.panel-danger - .panel-heading + .card.bg-danger + .card-header = s_('AdminUsers|Delete user') - .panel-body + .card-body - if @user.can_be_removed? && can?(current_user, :destroy_user, @user) %p Deleting a user has the following effects: = render 'users/deletion_guidance', user: @user %br - %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal', + %button.delete-user-button.btn.btn-danger{ data: { toggle: 'modal', target: '#delete-user-modal', delete_user_url: admin_user_path(@user), block_user_url: block_admin_user_path(@user), @@ -196,10 +196,10 @@ %p You don't have access to delete this user. - .panel.panel-danger - .panel-heading + .card.bg-danger + .card-header = s_('AdminUsers|Delete user and contributions') - .panel-body + .card-body - if can?(current_user, :destroy_user, @user) %p This option deletes the user and any contributions that @@ -210,7 +210,7 @@ the user, and projects in them, will also be removed. Commits to other projects are unaffected. %br - %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal', + %button.delete-user-button.btn.btn-danger{ data: { toggle: 'modal', target: '#delete-user-modal', delete_user_url: admin_user_path(@user, hard_delete: true), block_user_url: block_admin_user_path(@user), diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml index 22f149d1caa..d4455749803 100644 --- a/app/views/ci/lints/show.html.haml +++ b/app/views/ci/lints/show.html.haml @@ -1,8 +1,8 @@ .row.empty-state - .col-xs-12 + .col-12 .svg-content = image_tag 'illustrations/feature_moved.svg' - .col-xs-12 + .col-12 .text-content.text-center %h4= _("GitLab CI Linter has been moved") %p diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml index d828f6f971d..8b0463db000 100644 --- a/app/views/ci/status/_dropdown_graph_badge.html.haml +++ b/app/views/ci/status/_dropdown_graph_badge.html.haml @@ -6,12 +6,12 @@ - tooltip = "#{subject.name} - #{status.status_tooltip}" - if status.has_details? - = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, html: true, container: 'body' } do + = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, html: 'true', container: 'body' } do %span{ class: klass }= sprite_icon(status.icon) %span.ci-build-text= subject.name - else - .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', html: true, title: tooltip, container: 'body' } } + .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', html: 'true', title: tooltip, container: 'body' } } %span{ class: klass }= sprite_icon(status.icon) %span.ci-build-text= subject.name diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml index 7a3f3667ac1..7503548fa3d 100644 --- a/app/views/dashboard/_activity_head.html.haml +++ b/app/views/dashboard/_activity_head.html.haml @@ -1,5 +1,5 @@ .top-area - %ul.nav-links + %ul.nav-links.nav.nav-tabs %li{ class: active_when(params[:filter].nil?) }> = link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do Your projects diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml index 617c20b9635..d8f1e50544c 100644 --- a/app/views/dashboard/_groups_head.html.haml +++ b/app/views/dashboard/_groups_head.html.haml @@ -1,5 +1,5 @@ .top-area - %ul.nav-links.mobile-separator + %ul.nav-links.mobile-separator.nav.nav-tabs = nav_link(page: dashboard_groups_path) do = link_to dashboard_groups_path, title: _("Your groups") do Your groups diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 449a2ce625e..9b1d9b659f9 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -4,7 +4,7 @@ .top-area.scrolling-tabs-container.inner-page-scroll-tabs .fade-left= icon('angle-left') .fade-right= icon('angle-right') - %ul.nav-links.scrolling-tabs.mobile-separator + %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs = nav_link(page: [dashboard_projects_path, root_path]) do = link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do Your projects diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml index a9488df07bd..e7e323a8683 100644 --- a/app/views/dashboard/_snippets_head.html.haml +++ b/app/views/dashboard/_snippets_head.html.haml @@ -1,5 +1,5 @@ .top-area - %ul.nav-links + %ul.nav-links.nav.nav-tabs = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do Your snippets @@ -8,5 +8,5 @@ Explore snippets - if current_user - .nav-controls.hidden-xs + .nav-controls.d-none.d-sm-block = link_to "New snippet", new_snippet_path, class: "btn btn-new", title: "New snippet" diff --git a/app/views/dashboard/projects/_nav.html.haml b/app/views/dashboard/projects/_nav.html.haml index 97f854cc5f0..da3cf5807b0 100644 --- a/app/views/dashboard/projects/_nav.html.haml +++ b/app/views/dashboard/projects/_nav.html.haml @@ -1,5 +1,5 @@ .nav-block - %ul.nav-links.mobile-separator + %ul.nav-links.mobile-separator.nav.nav-tabs = nav_link(html_options: { class: ("active" unless params[:personal].present?) }) do = link_to s_('DashboardProjects|All'), dashboard_projects_path = nav_link(html_options: { class: ("active" if params[:personal].present?) }) do diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml index b1efe59aadc..8933d9e31ff 100644 --- a/app/views/dashboard/projects/starred.html.haml +++ b/app/views/dashboard/projects/starred.html.haml @@ -11,5 +11,5 @@ - if params[:filter_projects] || any_projects?(@projects) = render 'projects' - else - %h3 You don't have starred projects yet + %h3.page-title You don't have starred projects yet %p.slead Visit project page and press on star icon and it will appear on this page. diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index e86b1ab3116..4391624196b 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -5,7 +5,7 @@ = render 'dashboard/snippets_head' = render partial: 'snippets/snippets_scope_menu', locals: { include_private: true } -.visible-xs +.d-block.d-sm-none = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do New snippet diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 664966989db..d5a9cc646a6 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -4,18 +4,18 @@ - if current_user.todos.any? .top-area - %ul.nav-links.mobile-separator + %ul.nav-links.mobile-separator.nav.nav-tabs %li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }> = link_to todos_filter_path(state: 'pending') do %span Todos - %span.badge + %span.badge.badge-pill = number_with_delimiter(todos_pending_count) %li.todos-done{ class: active_when(params[:state] == 'done') }> = link_to todos_filter_path(state: 'done') do %span Done - %span.badge + %span.badge.badge-pill = number_with_delimiter(todos_done_count) .nav-controls @@ -35,7 +35,7 @@ - if params[:project_id].present? = hidden_field_tag(:project_id, params[:project_id]) = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', - placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project' } }) + placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project', display: 'static' } }) .filter-item.inline - if params[:author_id].present? = hidden_field_tag(:author_id, params[:author_id]) @@ -60,7 +60,7 @@ - else = sort_title_recently_created = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-sort + %ul.dropdown-menu.dropdown-menu-sort.dropdown-menu-right %li = link_to todos_filter_path(sort: sort_value_label_priority) do = sort_title_label_priority @@ -73,7 +73,7 @@ - if @todos.any? .js-todos-list-container .js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } } - .panel.panel-default.panel-without-border.panel-without-margin + .card.card-without-border.card-without-margin %ul.content-list.todos-list = render @todos = paginate @todos, theme: "gitlab" diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 41462f503cb..c45d2214592 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -6,11 +6,11 @@ = f.label :password = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required." - if devise_mapping.rememberable? - .remember-me.checkbox + .remember-me %label{ for: "user_remember_me" } = f.check_box :remember_me, class: 'remember-me-checkbox' %span Remember me - .pull-right.forgot-password + .float-right.forgot-password = link_to "Forgot your password?", new_password_path(:user) .submit-container.move-submit-down = f.submit "Sign in", class: "btn btn-save" diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml index 2556cb6f59b..36ff42090be 100644 --- a/app/views/devise/sessions/_new_crowd.html.haml +++ b/app/views/devise/sessions/_new_crowd.html.haml @@ -6,7 +6,7 @@ = label_tag :password = password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true } - if devise_mapping.rememberable? - .remember-me.checkbox + .remember-me %label{ for: "remember_me" } = check_box_tag :remember_me, '1', false, id: 'remember_me' %span Remember me diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index 3159d21598a..6bf7349f602 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -6,7 +6,7 @@ = label_tag :password = password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true } - if devise_mapping.rememberable? - .remember-me.checkbox + .remember-me %label{ for: "remember_me" } = check_box_tag :remember_me, '1', false, id: 'remember_me' %span Remember me diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index 6e54b9b5645..ba168c4eab8 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -9,7 +9,7 @@ %div = f.label 'Two-Factor Authentication code', name: :otp_attempt = f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.' - %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. + %p.form-text.text-muted.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. .prepend-top-20 = f.submit "Verify code", class: "btn btn-save" diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 546cec4d565..3723814debe 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -7,7 +7,7 @@ %span.light - has_icon = provider_has_icon?(provider) = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: 'oauth-login' + (has_icon ? ' oauth-image-link' : ' btn'), id: "oauth-login-#{provider}" - %fieldset.prepend-top-10.checkbox.remember-me + %fieldset.prepend-top-10.remember-me %label = check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox' %span diff --git a/app/views/devise/shared/_tab_single.html.haml b/app/views/devise/shared/_tab_single.html.haml index 7bd414d64c3..5683b4207b4 100644 --- a/app/views/devise/shared/_tab_single.html.haml +++ b/app/views/devise/shared/_tab_single.html.haml @@ -1,3 +1,3 @@ -%ul.nav-links.new-session-tabs.single-tab - %li.active - %a= tab_title +%ul.nav-links.new-session-tabs.single-tab.nav-tabs.nav + %li.nav-item + %a.nav-link.active= tab_title diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml index f50e0724e09..087af61235b 100644 --- a/app/views/devise/shared/_tabs_ldap.html.haml +++ b/app/views/devise/shared/_tabs_ldap.html.haml @@ -1,13 +1,13 @@ -%ul.nav-links.new-session-tabs{ class: ('custom-provider-tabs' if form_based_providers.any?) } +%ul.nav-links.new-session-tabs.nav-tabs.nav{ class: ('custom-provider-tabs' if form_based_providers.any?) } - if crowd_enabled? - %li.active - = link_to "Crowd", "#crowd", 'data-toggle' => 'tab' + %li.nav-item + = link_to "Crowd", "#crowd", class: 'nav-link active', 'data-toggle' => 'tab' - @ldap_servers.each_with_index do |server, i| - %li{ class: active_when(i.zero? && !crowd_enabled?) } - = link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab' + %li.nav-item{ class: active_when(i.zero? && !crowd_enabled?) } + = link_to server['label'], "##{server['provider_name']}", class: 'nav-link', 'data-toggle' => 'tab' - if password_authentication_enabled_for_web? - %li - = link_to 'Standard', '#login-pane', 'data-toggle' => 'tab' + %li.nav-item + = link_to 'Standard', '#login-pane', class: 'nav-link', 'data-toggle' => 'tab' - if allow_signup? - %li - = link_to 'Register', '#register-pane', 'data-toggle' => 'tab' + %li.nav-item + = link_to 'Register', '#register-pane', class: 'nav-link', 'data-toggle' => 'tab' diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml index fa3c3df7f60..284d4fa1b89 100644 --- a/app/views/devise/shared/_tabs_normal.html.haml +++ b/app/views/devise/shared/_tabs_normal.html.haml @@ -1,6 +1,6 @@ -%ul.nav-links.new-session-tabs{ role: 'tablist' } - %li.active{ role: 'presentation' } - %a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in +%ul.nav-links.new-session-tabs.nav-tabs.nav{ role: 'tablist' } + %li.nav-item{ role: 'presentation' } + %a.nav-link.active{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in - if allow_signup? - %li{ role: 'presentation' } - %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register + %li.nav-item{ role: 'presentation' } + %a.nav-link{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml index e9589213f80..1765251c93d 100644 --- a/app/views/discussions/_discussion.html.haml +++ b/app/views/discussions/_discussion.html.haml @@ -13,7 +13,7 @@ = icon("chevron-up") - else = icon("chevron-down") - Toggle discussion + = _('Toggle discussion') = link_to_member(@project, discussion.author, avatar: false) .inline.discussion-headline-light @@ -51,5 +51,5 @@ - if discussion.diff_discussion? && discussion.diff_file = render "discussions/diff_with_notes", discussion: discussion - else - .panel.panel-default + .card = render partial: "discussions/notes", locals: { discussion: discussion, disable_collapse_class: true } diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml index 1cc227428e9..30b00ca86b3 100644 --- a/app/views/discussions/_notes.html.haml +++ b/app/views/discussions/_notes.html.haml @@ -11,7 +11,7 @@ - if discussion.try(:on_image?) && show_toggle %button.diff-notes-collapse.js-diff-notes-toggle{ type: 'button' } = sprite_icon('collapse', css_class: 'collapse-icon') - %button.btn-transparent.badge.js-diff-notes-toggle{ type: 'button' } + %button.btn-transparent.badge.badge-pill.js-diff-notes-toggle{ type: 'button' } = badge_counter = render partial: "shared/notes/note", collection: discussion.notes, as: :note, locals: { badge_counter: badge_counter, show_image_comment_badge: show_image_comment_badge } @@ -22,7 +22,7 @@ - if discussion.potentially_resolvable? - line_type = local_assigns.fetch(:line_type, nil) - .btn-group-justified.discussion-with-resolve-btn{ role: "group" } + .btn-group.discussion-with-resolve-btn{ role: "group" } .btn-group{ role: "group" } = link_to_reply_discussion(discussion, line_type) diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml index cf0e0de1ca4..be0935b8313 100644 --- a/app/views/doorkeeper/applications/_form.html.haml +++ b/app/views/doorkeeper/applications/_form.html.haml @@ -9,10 +9,10 @@ = f.label :redirect_uri, class: 'label-light' = f.text_area :redirect_uri, class: 'form-control', required: true - %span.help-block + %span.form-text.text-muted Use one line per URI - if Doorkeeper.configuration.native_redirect_uri - %span.help-block + %span.form-text.text-muted Use %code= Doorkeeper.configuration.native_redirect_uri for local tests diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml index d1237d7bf6f..cdf3ff81bd9 100644 --- a/app/views/doorkeeper/applications/index.html.haml +++ b/app/views/doorkeeper/applications/index.html.haml @@ -73,7 +73,7 @@ %tr %td Anonymous - .help-block + .form-text.text-muted %em Authorization was granted by entering your username and password in the application. %td= token.created_at %td= token.scopes diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml index 6364f0be4a3..89ad626f73f 100644 --- a/app/views/doorkeeper/applications/show.html.haml +++ b/app/views/doorkeeper/applications/show.html.haml @@ -30,5 +30,5 @@ = render "shared/tokens/scopes_list", token: @application .form-actions - = link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide pull-left' + = link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide float-left' = render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10' diff --git a/app/views/doorkeeper/authorized_applications/index.html.haml b/app/views/doorkeeper/authorized_applications/index.html.haml index c8a585560a2..30c9d02b72e 100644 --- a/app/views/doorkeeper/authorized_applications/index.html.haml +++ b/app/views/doorkeeper/authorized_applications/index.html.haml @@ -1,4 +1,4 @@ -%header.page-header +%header %h1 Your authorized applications %main{ :role => "main" } .table-holder diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml index e3c5fd55f08..bc1d32607e4 100644 --- a/app/views/events/_event_push.atom.haml +++ b/app/views/events/_event_push.atom.haml @@ -6,7 +6,7 @@ at = event.created_at.to_s(:short) - unless event.rm_ref? - %blockquote= markdown(escape_once(event.commit_title), pipeline: :atom, project: event.project, author: event.author) + .blockquote= markdown(escape_once(event.commit_title), pipeline: :atom, project: event.project, author: event.author) - if event.commits_count > 1 %p %i diff --git a/app/views/explore/groups/_nav.html.haml b/app/views/explore/groups/_nav.html.haml index c8d95b52156..ab4787c6d05 100644 --- a/app/views/explore/groups/_nav.html.haml +++ b/app/views/explore/groups/_nav.html.haml @@ -1,5 +1,5 @@ .top-area - %ul.nav-links + %ul.nav-links.nav.nav-tabs = nav_link(page: explore_groups_path) do = link_to explore_groups_path do Explore Groups diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index efa8b2706da..0643b9cfbc5 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -9,7 +9,7 @@ = render 'nav' - if cookies[:explore_groups_landing_dismissed] != 'true' - .explore-groups.landing.content-block.js-explore-groups-landing.hidden + .explore-groups.landing.content-block.js-explore-groups-landing.hide %button.dismiss-button{ type: 'button', 'aria-label' => 'Dismiss' }= icon('times') .svg-container = custom_icon('icon_explore_groups_splash') diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index f630f1effdc..845f4046d0d 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -8,7 +8,7 @@ - else Any = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-align-right + %ul.dropdown-menu.dropdown-menu-right %li = link_to filter_projects_path(visibility_level: nil) do Any diff --git a/app/views/explore/projects/_nav.html.haml b/app/views/explore/projects/_nav.html.haml index e0a2a1e9c96..558cd26f1e0 100644 --- a/app/views/explore/projects/_nav.html.haml +++ b/app/views/explore/projects/_nav.html.haml @@ -1,5 +1,5 @@ .top-area - %ul.nav-links + %ul.nav-links.nav.nav-tabs = nav_link(page: [trending_explore_projects_path, explore_root_path]) do = link_to trending_explore_projects_path do Trending diff --git a/app/views/groups/_create_chat_team.html.haml b/app/views/groups/_create_chat_team.html.haml index 20de1b4c973..9a3ff0313b5 100644 --- a/app/views/groups/_create_chat_team.html.haml +++ b/app/views/groups/_create_chat_team.html.haml @@ -1,10 +1,10 @@ .form-group - = f.label :create_chat_team, class: 'control-label' do + = f.label :create_chat_team, class: 'col-form-label' do %span.mattermost-icon = custom_icon('icon_mattermost') Mattermost .col-sm-10 - .checkbox.js-toggle-container + .form-check.js-toggle-container = f.label :create_chat_team do .js-toggle-button= f.check_box(:create_chat_team, { checked: true }, true, false) Create a Mattermost team for this group diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml index 65e95f3aeef..3cd3fb32b9c 100644 --- a/app/views/groups/_group_admin_settings.html.haml +++ b/app/views/groups/_group_admin_settings.html.haml @@ -1,7 +1,7 @@ -.form-group - = f.label :lfs_enabled, 'Large File Storage', class: 'control-label' +.form-group.row + = f.label :lfs_enabled, 'Large File Storage', class: 'col-form-label col-sm-2' .col-sm-10 - .checkbox + .form-check = f.label :lfs_enabled do = f.check_box :lfs_enabled, checked: @group.lfs_enabled? %strong @@ -10,17 +10,17 @@ %br/ %span.descr This setting can be overridden in each project. -.form-group - = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' +.form-group.row + = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2' .col-sm-10 - .checkbox + .form-check = f.label :require_two_factor_authentication do = f.check_box :require_two_factor_authentication %strong Require all users in this group to setup Two-factor authentication = link_to icon('question-circle'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group') -.form-group - .col-sm-offset-2.col-sm-10 - .checkbox +.form-group.row + .offset-sm-2.col-sm-10 + .form-check = f.text_field :two_factor_grace_period, class: 'form-control' - .help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication + .form-text.text-muted Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 3375e01b3a1..96ed63937fa 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -1,16 +1,16 @@ - breadcrumb_title "General Settings" - @content_class = "limit-container-width" unless fluid_layout -.panel.panel-default.prepend-top-default - .panel-heading +.card.prepend-top-default + .card-header Group settings - .panel-body - = form_for @group, html: { multipart: true, class: "form-horizontal gl-show-field-errors" }, authenticity_token: true do |f| + .card-body + = form_for @group, html: { multipart: true, class: "gl-show-field-errors" }, authenticity_token: true do |f| = form_errors(@group) = render 'shared/group_form', f: f - .form-group - .col-sm-offset-2.col-sm-10 + .form-group.row + .offset-sm-2.col-sm-10 .avatar-container.s160 = group_icon(@group, alt: '', class: 'avatar group-avatar s160') %p.light @@ -25,15 +25,15 @@ = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group - .form-group - .col-sm-offset-2.col-sm-10 + .form-group.row + .offset-sm-2.col-sm-10 = render 'shared/allow_request_access', form: f - .form-group - %label.control-label + .form-group.row + %label.col-form-label.col-sm-2 = s_("GroupSettings|Share with group lock") .col-sm-10 - .checkbox + .form-check = f.label :share_with_group_lock do = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group) %strong @@ -47,9 +47,9 @@ .form-actions = f.submit 'Save group', class: "btn btn-save" -.panel.panel-danger - .panel-heading Remove group - .panel-body +.card.bg-danger + .card-header Remove group + .card-body = form_tag(@group, method: :delete) do %p Removing group will cause all child projects and resources to be removed. @@ -60,9 +60,9 @@ = button_to 'Remove group', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_group_message(@group) } - if supports_nested_groups? - .panel.panel-warning - .panel-heading Transfer group - .panel-body + .card.bg-warning + .card-header Transfer group + .card-body = form_for @group, url: transfer_group_path(@group), method: :put do |f| .form-group = dropdown_tag('Select parent group', options: { toggle_class: 'js-groups-dropdown', title: 'Parent Group', filter: true, dropdown_class: 'dropdown-open-top dropdown-group-transfer', placeholder: "Search groups", data: { data: parent_group_options(@group) } }) diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml index 5b1a4630c56..aa03f8365f9 100644 --- a/app/views/groups/group_members/_new_group_member.html.haml +++ b/app/views/groups/group_members/_new_group_member.html.haml @@ -2,12 +2,12 @@ .row .col-md-4.col-lg-6 = users_select_tag(:user_ids, multiple: true, class: 'input-clamp', scope: :all, email_user: true) - .help-block.append-bottom-10 + .form-text.text-muted.append-bottom-10 Search for members by name, username, or email, or invite new ones using their email address. .col-md-3.col-lg-2 = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select" - .help-block.append-bottom-10 + .form-text.text-muted.append-bottom-10 = link_to "Read more", help_page_path("user/permissions"), class: "vlink" about role permissions @@ -15,7 +15,7 @@ .clearable-input = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' %i.clear-icon.js-clear-input - .help-block.append-bottom-10 + .form-text.text-muted.append-bottom-10 On this date, the member(s) will automatically lose access to this group and all of its projects. .col-md-2 diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index c8addc49117..6a0321bcd2b 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -17,8 +17,8 @@ .clearfix %h5.member.existing-title Existing members - .panel.panel-default - .panel-heading.flex-project-members-panel + .card + .card-header.flex-project-members-panel %span.flex-project-title Members with access to %strong= @group.name diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml index a1be0d3220a..6d35457a0ec 100644 --- a/app/views/groups/milestones/_form.html.haml +++ b/app/views/groups/milestones/_form.html.haml @@ -1,14 +1,14 @@ -= form_for [@group, @milestone], html: { class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input' } do |f| += form_for [@group, @milestone], html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' } do |f| .row = form_errors(@milestone) .col-md-6 - .form-group - = f.label :title, "Title", class: "control-label" + .form-group.row + = f.label :title, "Title", class: "col-form-label col-sm-2" .col-sm-10 = f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true - .form-group.milestone-description - = f.label :description, "Description", class: "control-label" + .form-group.row.milestone-description + = f.label :description, "Description", class: "col-form-label col-sm-2" .col-sm-10 = render layout: 'projects/md_preview', locals: { url: group_preview_markdown_path } do = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...', supports_autocomplete: false diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index e9daac95ca1..1e72d88db1e 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -8,12 +8,12 @@ New Group %hr -= form_for @group, html: { class: 'group-form form-horizontal gl-show-field-errors' } do |f| += form_for @group, html: { class: 'group-form gl-show-field-errors' } do |f| = form_errors(@group) = render 'shared/group_form', f: f, autofocus: true - .form-group.group-description-holder - = f.label :avatar, "Group avatar", class: 'control-label' + .form-group.row.group-description-holder + = f.label :avatar, "Group avatar", class: 'col-form-label col-sm-2' .col-sm-10 = render 'shared/choose_group_avatar_button', f: f @@ -21,8 +21,8 @@ = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled - .form-group - .col-sm-offset-2.col-sm-10 + .form-group.row + .offset-sm-2.col-sm-10 = render 'shared/group_tips' .form-actions diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index ef181b425bc..cd07b95155c 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -1,7 +1,7 @@ - breadcrumb_title "Projects" -.panel.panel-default.prepend-top-default - .panel-heading +.card.prepend-top-default + .card-header %strong= @group.name projects: - if can? current_user, :admin_group, @group @@ -15,10 +15,10 @@ %span{ class: visibility_level_color(project.visibility_level) } = visibility_level_icon(project.visibility_level) %strong= link_to project.full_name, project - .pull-right + .float-right - if project.archived - %span.label.label-warning archived - %span.badge + %span.badge.badge-warning archived + %span.badge.badge-pill = storage_counter(project.statistics.storage_size) = link_to 'Members', project_project_members_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" = link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 8e1dea4afc1..8b0ef3cd87a 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -20,7 +20,7 @@ %input.btn.btn-success.dropdown-primary.js-new-group-child{ type: "button", value: new_project_label, data: { action: "new-project" } } %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown" } } = icon("caret-down", class: "dropdown-btn-icon") - %ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-align-right{ data: { dropdown: true } } + %ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-right{ data: { dropdown: true } } %li.droplab-item-selected{ role: "button", data: { value: "new-project", text: new_project_label } } .menu-item .icon-container diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 1c5b4aecabb..9a3a03a7671 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -121,7 +121,7 @@ %tr %td.shortcut .key g - .key e + .key v %td Go to the project's activity feed %tr @@ -175,6 +175,18 @@ %tr %td.shortcut .key g + .key e + %td + Go to environments + %tr + %td.shortcut + .key g + .key k + %td + Go to kubernetes + %tr + %td.shortcut + .key g .key s %td Go to snippets @@ -219,6 +231,17 @@ %td.shortcut .key y %td Go to file permalink + %tbody + %tr + %th + %th Web IDE + %tr + %td.shortcut + - if browser.platform.mac? + .key ⌘ p + - else + .key ctrl p + %td Go to file .col-lg-4 %table.shortcut-mappings %tbody.hidden-shortcut.network{ style: 'display:none' } diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index bf2725dc328..6391a13dd25 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -8,7 +8,7 @@ Community Edition - if user_signed_in? %span= Gitlab::VERSION - %small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION) + %small= link_to Gitlab.revision, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab.revision) = version_status_badge %hr @@ -33,8 +33,8 @@ .documentation-index.wiki = markdown(@help_index) .col-md-4 - .panel.panel-default - .panel-heading + .card + .card-header Quick help %ul.well-list %li= link_to 'See our website for getting help', support_url diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 7908a04c2eb..94b012d39a3 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -129,8 +129,8 @@ .lead List inside panel .example - .panel.panel-default - .panel-heading Your list + .card + .card-header Your list %ul.well-list %li One item @@ -174,7 +174,7 @@ .example .top-area - %ul.nav-links + %ul.nav-links.nav.nav-tabs %li.active %a Open %li @@ -205,7 +205,7 @@ %h2#buttons Buttons .example - %button.btn.btn-default{ :type => "button" } Default + %button.btn.btn-default{ :type => "button" } Secondary %button.btn.btn-primary{ :type => "button" } Primary %button.btn.btn-success{ :type => "button" } Success %button.btn.btn-info{ :type => "button" } Info @@ -217,7 +217,7 @@ .example .clearfix - .dropdown.inline.pull-left + .dropdown.inline.float-left %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } Dropdown = icon('chevron-down') @@ -225,11 +225,11 @@ %li %a{ href: "#" } Dropdown option - .dropdown.inline.pull-right + .dropdown.inline.float-right %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } Dropdown = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-align-right + %ul.dropdown-menu.dropdown-menu-right %li %a{ href: "#" } Dropdown option @@ -415,26 +415,26 @@ .row .col-md-6 - .panel.panel-success - .panel-heading Success - .panel-body + .card.bg-success + .card-header Success + .card-body = lorem - .panel.panel-primary - .panel-heading Primary - .panel-body + .card.bg-primary + .card-header Primary + .card-body = lorem - .panel.panel-info - .panel-heading Info - .panel-body + .card.bg-info + .card-header Info + .card-body = lorem .col-md-6 - .panel.panel-warning - .panel-heading Warning - .panel-body + .card.bg-warning + .card-header Warning + .card-body = lorem - .panel.panel-danger - .panel-heading Danger - .panel-body + .card.bg-danger + .card-header Danger + .card-body = lorem %h2#alerts Alerts @@ -460,23 +460,23 @@ %code form.horizontal-form .example - %form.form-horizontal - .form-group - %label.col-sm-2.control-label{ :for => "inputEmail3" } Email + %form + .form-group.row + %label.col-sm-2.col-form-label{ :for => "inputEmail3" } Email .col-sm-10 %input#inputEmail3.form-control{ :placeholder => "Email", :type => "email" }/ - .form-group - %label.col-sm-2.control-label{ :for => "inputPassword3" } Password + .form-group.row + %label.col-sm-2.col-form-label{ :for => "inputPassword3" } Password .col-sm-10 %input#inputPassword3.form-control{ :placeholder => "Password", :type => "password" }/ - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check %label %input{ :type => "checkbox" }/ Remember me - .form-group - .col-sm-offset-2.col-sm-10 + .form-group.row + .offset-sm-2.col-sm-10 %button.btn.btn-default{ :type => "submit" } Sign in .lead @@ -491,7 +491,7 @@ .form-group %label{ :for => "exampleInputPassword1" } Password %input#exampleInputPassword1.form-control{ :placeholder => "Password", :type => "password" }/ - .checkbox + .form-check %label %input{ :type => "checkbox" }/ Remember me diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml index 638c8b5a672..f7094375023 100644 --- a/app/views/import/_githubish_status.html.haml +++ b/app/views/import/_githubish_status.html.haml @@ -46,14 +46,15 @@ %td.import-target %fieldset.row .input-group - .project-path.input-group-btn + .project-path.input-group-prepend - if current_user.can_select_namespace? - selected = params[:namespace_id] || :current_user - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {} - = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } + = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'input-group-text select2 js-select-namespace', tabindex: 1 } - else - = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true - %span.input-group-addon / + = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true + %span.input-group-prepend + .input-group-text / = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true %td.import-actions.job-status = button_tag class: "btn btn-import js-add-to-import" do diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index 9589e0956f4..4e8f715db4f 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -54,14 +54,15 @@ %td.import-target %fieldset.row .input-group - .project-path.input-group-btn + .project-path.input-group-prepend - if current_user.can_select_namespace? - selected = params[:namespace_id] || :current_user - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner, path: repo.owner) } : {} - = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } + = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'input-group-text select2 js-select-namespace', tabindex: 1 } - else - = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true - %span.input-group-addon / + = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true + %span.input-group-prepend + .input-group-text / = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true %td.import-actions.job-status = button_tag class: 'btn btn-import js-add-to-import' do @@ -73,7 +74,7 @@ = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer' %td.import-target %td.import-actions-job-status - = label_tag 'Incompatible Project', nil, class: 'label label-danger' + = label_tag 'Incompatible Project', nil, class: 'label badge-danger' - if @incompatible_repos.any? %p diff --git a/app/views/import/fogbugz/new.html.haml b/app/views/import/fogbugz/new.html.haml index 5515fad6f48..d5a37095365 100644 --- a/app/views/import/fogbugz/new.html.haml +++ b/app/views/import/fogbugz/new.html.haml @@ -5,21 +5,21 @@ Import projects from FogBugz %hr -= form_tag callback_import_fogbugz_path, class: 'form-horizontal' do += form_tag callback_import_fogbugz_path do %p To get started you enter your FogBugz URL and login information below. In the next steps, you'll be able to map users and select the projects you want to import. - .form-group - = label_tag :uri, 'FogBugz URL', class: 'control-label' + .form-group.row + = label_tag :uri, 'FogBugz URL', class: 'col-form-label col-sm-8' .col-sm-4 = text_field_tag :uri, nil, placeholder: 'https://mycompany.fogbugz.com', class: 'form-control' - .form-group - = label_tag :email, 'FogBugz Email', class: 'control-label' + .form-group.row + = label_tag :email, 'FogBugz Email', class: 'col-form-label col-sm-8' .col-sm-4 = text_field_tag :email, nil, class: 'form-control' - .form-group - = label_tag :password, 'FogBugz Password', class: 'control-label' + .form-group.row + = label_tag :password, 'FogBugz Password', class: 'col-form-label col-sm-8' .col-sm-4 = password_field_tag :password, nil, class: 'form-control' .form-actions diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml index 84e0009487f..d27c5d3c36d 100644 --- a/app/views/import/fogbugz/new_user_map.html.haml +++ b/app/views/import/fogbugz/new_user_map.html.haml @@ -5,7 +5,7 @@ Import projects from FogBugz %hr -= form_tag create_user_map_import_fogbugz_path, class: 'form-horizontal' do += form_tag create_user_map_import_fogbugz_path do %p Customize how FogBugz email addresses and usernames are imported into GitLab. In the next step, you'll be able to select the projects you want to import. diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml index 02a116f996b..eb9790c7903 100644 --- a/app/views/import/gitea/new.html.haml +++ b/app/views/import/gitea/new.html.haml @@ -10,13 +10,13 @@ = succeed '.' do = link_to 'Personal Access Token', 'https://github.com/gogits/go-gogs-client/wiki#access-token' -= form_tag personal_access_token_import_gitea_path, class: 'form-horizontal' do - .form-group - = label_tag :gitea_host_url, 'Gitea Host URL', class: 'control-label' += form_tag personal_access_token_import_gitea_path do + .form-group.row + = label_tag :gitea_host_url, 'Gitea Host URL', class: 'col-form-label col-sm-8' .col-sm-4 = text_field_tag :gitea_host_url, nil, placeholder: 'https://try.gitea.io', class: 'form-control' - .form-group - = label_tag :personal_access_token, 'Personal Access Token', class: 'control-label' + .form-group.row + = label_tag :personal_access_token, 'Personal Access Token', class: 'col-form-label col-sm-8' .col-sm-4 = text_field_tag :personal_access_token, nil, class: 'form-control' .form-actions diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml index dec85368d10..2d059e78490 100644 --- a/app/views/import/gitlab_projects/new.html.haml +++ b/app/views/import/gitlab_projects/new.html.haml @@ -8,20 +8,22 @@ = form_tag import_gitlab_project_path, class: 'new_project', multipart: true do .row - .form-group.col-xs-12.col-sm-6 + .form-group.col-12.col-sm-6 = label_tag :namespace_id, 'Project path', class: 'label-light' .form-group .input-group - if current_user.can_select_namespace? - .input-group-addon.has-tooltip{ title: root_url } - = root_url + .input-group-prepend.has-tooltip{ title: root_url } + .input-group-text + = root_url = select_tag :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), class: 'select2 js-select-namespace', tabindex: 1 - else - .input-group-addon.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' } - #{user_url(current_user.username)}/ + .input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' } + .input-group-text + #{user_url(current_user.username)}/ = hidden_field_tag :namespace_id, value: current_user.namespace_id - .form-group.col-xs-12.col-sm-6.project-path + .form-group.col-12.col-sm-6.project-path = label_tag :path, 'Project name', class: 'label-light' = text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control", tabindex: 2, autofocus: true, required: true diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml index c5800a1cca0..2f1fb8d9c56 100644 --- a/app/views/import/google_code/new.html.haml +++ b/app/views/import/google_code/new.html.haml @@ -5,7 +5,7 @@ Import projects from Google Code %hr -= form_tag callback_import_google_code_path, class: 'form-horizontal', multipart: true do += form_tag callback_import_google_code_path, multipart: true do %p Follow the steps below to export your Google Code project data. In the next step, you'll be able to select the projects you want to import. diff --git a/app/views/import/google_code/new_user_map.html.haml b/app/views/import/google_code/new_user_map.html.haml index 0738b3db1eb..91c774f575c 100644 --- a/app/views/import/google_code/new_user_map.html.haml +++ b/app/views/import/google_code/new_user_map.html.haml @@ -5,7 +5,7 @@ Import projects from Google Code %hr -= form_tag create_user_map_import_google_code_path, class: 'form-horizontal' do += form_tag create_user_map_import_google_code_path do %p Customize how Google Code email addresses and usernames are imported into GitLab. In the next step, you'll be able to select the projects you want to import. @@ -36,7 +36,7 @@ will add "By <a href="#">johnsmith@example.com</a>" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address. - .form-group + .form-group.row .col-sm-12 = text_area_tag :user_map, JSON.pretty_generate(@user_map), class: 'form-control', rows: 15 diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml index bc61aeece72..acf7a108cb0 100644 --- a/app/views/import/google_code/status.html.haml +++ b/app/views/import/google_code/status.html.haml @@ -66,7 +66,7 @@ = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank", rel: 'noopener noreferrer' %td.import-target %td.import-actions-job-status - = label_tag "Incompatible Project", nil, class: "label label-danger" + = label_tag "Incompatible Project", nil, class: "label badge-danger" - if @incompatible_repos.any? %p diff --git a/app/views/kaminari/gitlab/_first_page.html.haml b/app/views/kaminari/gitlab/_first_page.html.haml index e7a70e3bb28..369165da02a 100644 --- a/app/views/kaminari/gitlab/_first_page.html.haml +++ b/app/views/kaminari/gitlab/_first_page.html.haml @@ -5,5 +5,5 @@ -# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li.first - = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, remote: remote +%li.first.page-item + = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, remote: remote, class: 'page-link' diff --git a/app/views/kaminari/gitlab/_gap.html.haml b/app/views/kaminari/gitlab/_gap.html.haml index 889514c4755..6eec30212d1 100644 --- a/app/views/kaminari/gitlab/_gap.html.haml +++ b/app/views/kaminari/gitlab/_gap.html.haml @@ -4,6 +4,5 @@ -# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li - %span.gap - = raw(t 'views.pagination.truncate') +%li.page-item.disabled + = link_to raw(t 'views.pagination.truncate'), '#', class: 'page-link' diff --git a/app/views/kaminari/gitlab/_last_page.html.haml b/app/views/kaminari/gitlab/_last_page.html.haml index 53f780d1d1b..8b49db58281 100644 --- a/app/views/kaminari/gitlab/_last_page.html.haml +++ b/app/views/kaminari/gitlab/_last_page.html.haml @@ -5,5 +5,5 @@ -# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li.last - = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {remote: remote} +%li.last.page-item + = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {remote: remote, class: 'page-link'} diff --git a/app/views/kaminari/gitlab/_next_page.html.haml b/app/views/kaminari/gitlab/_next_page.html.haml index c93dc7a50e8..05f151555ad 100644 --- a/app/views/kaminari/gitlab/_next_page.html.haml +++ b/app/views/kaminari/gitlab/_next_page.html.haml @@ -5,9 +5,8 @@ -# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -- if current_page.last? - %li.next.disabled - %span= raw(t 'views.pagination.next') -- else - %li.next - = link_to raw(t 'views.pagination.next'), url, rel: 'next', remote: remote + +- page_url = current_page.last? ? '#' : url + +%li.page-item{ class: ('disabled' if current_page.last?) } + = link_to raw(t 'views.pagination.next'), page_url, rel: 'next', remote: remote, class: 'page-link' diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml index 5c5be03a7cd..8a40e13a537 100644 --- a/app/views/kaminari/gitlab/_page.html.haml +++ b/app/views/kaminari/gitlab/_page.html.haml @@ -6,5 +6,5 @@ -# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li.page{ class: [active_when(page.current?), ('sibling' if page.next? || page.prev?)] } - = link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil } +%li.page-item.js-pagination-page{ class: [active_when(page.current?), ('sibling' if page.next? || page.prev?)] } + = link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil, class: 'page-link' } diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml index 8fe6bd653ae..a6435deb4bf 100644 --- a/app/views/kaminari/gitlab/_paginator.html.haml +++ b/app/views/kaminari/gitlab/_paginator.html.haml @@ -7,7 +7,7 @@ -# paginator: the paginator that renders the pagination tags inside = paginator.render do .gl-pagination - %ul.pagination.clearfix + %ul.pagination.justify-content-center - unless current_page.first? = first_page_tag unless total_pages < 5 # As kaminari will always show the first 5 pages = prev_page_tag diff --git a/app/views/kaminari/gitlab/_prev_page.html.haml b/app/views/kaminari/gitlab/_prev_page.html.haml index b7c6caf7ff4..f4a11a449b7 100644 --- a/app/views/kaminari/gitlab/_prev_page.html.haml +++ b/app/views/kaminari/gitlab/_prev_page.html.haml @@ -5,9 +5,8 @@ -# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -- if current_page.first? - %li.prev.disabled - %span= raw(t 'views.pagination.previous') -- else - %li.prev - = link_to raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote + +- page_url = current_page.first? ? '#' : url + +%li.page-item{ class: ('disabled' if current_page.first?) } + = link_to raw(t 'views.pagination.previous'), page_url, rel: 'prev', remote: remote, class: 'page-link' diff --git a/app/views/kaminari/gitlab/_without_count.html.haml b/app/views/kaminari/gitlab/_without_count.html.haml index 250029c4475..1425a809052 100644 --- a/app/views/kaminari/gitlab/_without_count.html.haml +++ b/app/views/kaminari/gitlab/_without_count.html.haml @@ -1,8 +1,8 @@ .gl-pagination %ul.pagination.clearfix - if previous_path - %li.prev - = link_to(t('views.pagination.previous'), previous_path, rel: 'prev') + %li.page-item.prev + = link_to(t('views.pagination.previous'), previous_path, rel: 'prev', class: 'page-link') - if next_path - %li.next - = link_to(t('views.pagination.next'), next_path, rel: 'next') + %li.page-item.next + = link_to(t('views.pagination.next'), next_path, rel: 'next', class: 'page-link') diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 52587760ba4..91f796adb2e 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -7,7 +7,7 @@ - if @project && @project.persisted? - project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: project_issues_path(@project), mr_path: project_merge_requests_path(@project), issues_disabled: !@project.issues_enabled? } .search.search-form{ class: "#{'has-location-badge' if label.present?}" } - = form_tag search_path, method: :get, class: 'navbar-form' do |f| + = form_tag search_path, method: :get, class: 'form-inline' do |f| .search-input-container - if label.present? .location-badge= label diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 6513b719199..82ca7252424 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -10,9 +10,7 @@ .content = render "layouts/flash" .row - .col-sm-5.pull-right.new-session-forms-container - = yield - .col-sm-7.brand-holder.pull-left + .col-sm-7.brand-holder %h1 = brand_title = brand_image @@ -28,6 +26,8 @@ - if Gitlab::CurrentSettings.sign_in_text.present? = markdown_field(Gitlab::CurrentSettings.current_application_settings, :sign_in_text) + .col-sm-5.new-session-forms-container + = yield %hr.footer-fixed .container.footer-container diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index dc121812406..1bca837a311 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -1,4 +1,4 @@ -%header.navbar.navbar-gitlab.qa-navbar +%header.navbar.navbar-gitlab.qa-navbar.navbar-expand-sm %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content .container-fluid .header-content @@ -8,7 +8,7 @@ = brand_header_logo - logo_text = brand_header_logo_type - if logo_text.present? - %span.logo-text.hidden-xs + %span.logo-text.d-none.d-sm-block = logo_text - if current_user @@ -21,9 +21,9 @@ - if current_user = render 'layouts/header/new_dropdown' - if header_link?(:search) - %li.hidden-sm.hidden-xs + %li.nav-item.d-none.d-sm-none.d-md-block = render 'layouts/search' unless current_controller?(:search) - %li.visible-sm-inline-block.visible-xs-inline-block + %li.nav-item.d-inline-block.d-sm-none.d-md-none = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('search', size: 16) @@ -32,38 +32,38 @@ = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('issues', size: 16) - issues_count = assigned_issuables_count(:issues) - %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) } + %span.badge.badge-pill.issues-count{ class: ('hidden' if issues_count.zero?) } = number_with_delimiter(issues_count) - if header_link?(:merge_requests) = nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do = link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('git-merge', size: 16) - merge_requests_count = assigned_issuables_count(:merge_requests) - %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) } + %span.badge.badge-pill.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) } = number_with_delimiter(merge_requests_count) - if header_link?(:todos) = nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('todo-done', size: 16) - %span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) } + %span.badge.badge-pill.todos-count{ class: ('hidden' if todos_pending_count.zero?) } = todos_count_format(todos_pending_count) - if header_link?(:user_dropdown) - %li.header-user.dropdown + %li.nav-item.header-user.dropdown = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar" = sprite_icon('angle-down', css_class: 'caret-down') - .dropdown-menu-nav.dropdown-menu-align-right + .dropdown-menu.dropdown-menu-right = render 'layouts/header/current_user_dropdown' - if header_link?(:admin_impersonation) - %li.impersonation - = link_to admin_impersonation_path, class: 'impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do + %li.nav-item.impersonation + = link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do = icon('user-secret') - if header_link?(:sign_in) - %li + %li.nav-item %div - = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in' + = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'nav-link btn btn-sign-in' - %button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' } + %button.navbar-toggler.d-block.d-sm-none{ type: 'button' } %span.sr-only Toggle navigation = sprite_icon('more', size: 12, css_class: 'more-icon js-navbar-toggle-right') = sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left') diff --git a/app/views/layouts/header/_empty.html.haml b/app/views/layouts/header/_empty.html.haml index 2ed4edb1136..2dfc787b7a8 100644 --- a/app/views/layouts/header/_empty.html.haml +++ b/app/views/layouts/header/_empty.html.haml @@ -1,4 +1,4 @@ -%header.navbar.navbar-fixed-top.navbar-empty +%header.navbar.fixed-top.navbar-empty .container - .center-logo + .mx-auto = brand_header_logo diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index 6f53f5ac1ae..db8137cc248 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -2,7 +2,7 @@ = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do = sprite_icon('plus-square', size: 16) = sprite_icon('angle-down', css_class: 'caret-down') - .dropdown-menu-nav.dropdown-menu-align-right + .dropdown-menu.dropdown-menu-right %ul - if @group&.persisted? - create_group_project = can?(current_user, :create_projects, @group) diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index f773bd0832d..7647e25e804 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -8,34 +8,34 @@ = render "layouts/nav/projects_dropdown/show" - if dashboard_nav_link?(:groups) - = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "hidden-xs" }) do + = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "d-none d-sm-block" }) do = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups qa-groups-link', title: 'Groups' do Groups - if dashboard_nav_link?(:activity) - = nav_link(path: 'dashboard#activity', html_options: { class: "visible-lg" }) do + = nav_link(path: 'dashboard#activity', html_options: { class: "d-none d-lg-block d-xl-block" }) do = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do Activity - if dashboard_nav_link?(:milestones) - = nav_link(controller: 'dashboard/milestones', html_options: { class: "visible-lg" }) do + = nav_link(controller: 'dashboard/milestones', html_options: { class: "d-none d-lg-block d-xl-block" }) do = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do Milestones - if dashboard_nav_link?(:snippets) - = nav_link(controller: 'dashboard/snippets', html_options: { class: "visible-lg" }) do + = nav_link(controller: 'dashboard/snippets', html_options: { class: "d-none d-lg-block d-xl-block" }) do = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do Snippets - if any_dashboard_nav_link?([:groups, :milestones, :activity, :snippets]) - %li.header-more.dropdown.hidden-lg + %li.header-more.dropdown.d-lg-none.d-xl-none %a{ href: "#", data: { toggle: "dropdown" } } More = sprite_icon('angle-down', css_class: 'caret-down') .dropdown-menu %ul - if dashboard_nav_link?(:groups) - = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "visible-xs" }) do + = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "d-block d-sm-none" }) do = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do Groups @@ -61,13 +61,13 @@ Projects - if current_controller?('ide') - %li.line-separator.hidden-xs + %li.line-separator.d-none.d-sm-block = nav_link(controller: 'ide') do = link_to '#', class: 'dashboard-shortcuts-web-ide', title: 'Web IDE' do Web IDE - if current_user.admin? || Gitlab::Sherlock.enabled? - %li.line-separator.hidden-xs + %li.line-separator.d-none.d-sm-block - if current_user.admin? = nav_link(controller: 'admin/dashboard') do = link_to admin_root_path, class: 'admin-icon qa-admin-area-link', title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index dd086f70641..62402c32e08 100644 --- a/app/views/layouts/nav/sidebar/_admin.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml @@ -127,13 +127,13 @@ = sprite_icon('slight-frown') %span.nav-item-name Abuse Reports - %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) + %span.badge.badge-pill.count= number_with_delimiter(AbuseReport.count(:all)) %ul.sidebar-sub-level-items.is-fly-out-only = nav_link(controller: :abuse_reports, html_options: { class: "fly-out-top-item" } ) do = link_to admin_abuse_reports_path do %strong.fly-out-top-item-name #{ _('Abuse Reports') } - %span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(AbuseReport.count(:all)) + %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(AbuseReport.count(:all)) - if akismet_enabled? = nav_link(controller: :spam_logs) do diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index 517d9aa3d99..39a033337ff 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -43,14 +43,14 @@ = sprite_icon('issues') %span.nav-item-name Issues - %span.badge.count= number_with_delimiter(issues_count) + %span.badge.badge-pill.count= number_with_delimiter(issues_count) %ul.sidebar-sub-level-items = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index'], html_options: { class: "fly-out-top-item" } ) do = link_to issues_group_path(@group) do %strong.fly-out-top-item-name #{ _('Issues') } - %span.badge.count.issue_counter.fly-out-badge= number_with_delimiter(issues_count) + %span.badge.badge-pill.count.issue_counter.fly-out-badge= number_with_delimiter(issues_count) %li.divider.fly-out-top-item = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do @@ -83,13 +83,13 @@ = sprite_icon('git-merge') %span.nav-item-name Merge Requests - %span.badge.count= number_with_delimiter(merge_requests_count) + %span.badge.badge-pill.count= number_with_delimiter(merge_requests_count) %ul.sidebar-sub-level-items.is-fly-out-only = nav_link(path: 'groups#merge_requests', html_options: { class: "fly-out-top-item" } ) do = link_to merge_requests_group_path(@group) do %strong.fly-out-top-item-name #{ _('Merge Requests') } - %span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests_count) + %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests_count) - if group_sidebar_link?(:group_members) = nav_link(path: 'group_members#index') do diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 196db08cebd..fdb07ce6fc5 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -13,13 +13,13 @@ .nav-icon-container = sprite_icon('project') %span.nav-item-name - Project + = _('Project') %ul.sidebar-sub-level-items = nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do = link_to project_path(@project) do %strong.fly-out-top-item-name - #{ _('Overview') } + = _('Project') %li.divider.fly-out-top-item = nav_link(path: 'projects#show') do = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do @@ -40,45 +40,45 @@ .nav-icon-container = sprite_icon('doc_text') %span.nav-item-name - Repository + = _('Repository') %ul.sidebar-sub-level-items = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network), html_options: { class: "fly-out-top-item" } ) do = link_to project_tree_path(@project) do %strong.fly-out-top-item-name - #{ _('Repository') } + = _('Repository') %li.divider.fly-out-top-item = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do = link_to project_tree_path(@project) do - #{ _('Files') } + = _('Files') = nav_link(controller: [:commit, :commits]) do = link_to project_commits_path(@project, current_ref) do - #{ _('Commits') } + = _('Commits') = nav_link(html_options: {class: branches_tab_class}) do = link_to project_branches_path(@project) do - #{ _('Branches') } + = _('Branches') = nav_link(controller: [:tags, :releases]) do = link_to project_tags_path(@project) do - #{ _('Tags') } + = _('Tags') = nav_link(path: 'graphs#show') do = link_to project_graph_path(@project, current_ref) do - #{ _('Contributors') } + = _('Contributors') = nav_link(controller: %w(network)) do = link_to project_network_path(@project, current_ref) do - #{ s_('ProjectNetworkGraph|Graph') } + = _('Graph') = nav_link(controller: :compare) do = link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do - #{ _('Compare') } + = _('Compare') = nav_link(path: 'graphs#charts') do = link_to charts_project_graph_path(@project, current_ref) do - #{ _('Charts') } + = _('Charts') - if project_nav_tab? :issues = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do @@ -86,24 +86,24 @@ .nav-icon-container = sprite_icon('issues') %span.nav-item-name - Issues + = _('Issues') - if @project.issues_enabled? - %span.badge.count.issue_counter - = number_with_delimiter(@project.open_issues_count) + %span.badge.badge-pill.count.issue_counter + = number_with_delimiter(@project.open_issues_count(current_user)) %ul.sidebar-sub-level-items = nav_link(controller: :issues, html_options: { class: "fly-out-top-item" } ) do = link_to project_issues_path(@project) do %strong.fly-out-top-item-name - #{ _('Issues') } + = _('Issues') - if @project.issues_enabled? - %span.badge.count.issue_counter.fly-out-badge - = number_with_delimiter(@project.open_issues_count) + %span.badge.badge-pill.count.issue_counter.fly-out-badge + = number_with_delimiter(@project.open_issues_count(current_user)) %li.divider.fly-out-top-item = nav_link(controller: :issues, action: :index) do = link_to project_issues_path(@project), title: 'Issues' do %span - List + = _('List') = nav_link(controller: :boards) do = link_to project_boards_path(@project), title: boards_link_text do @@ -113,12 +113,12 @@ = nav_link(controller: :labels) do = link_to project_labels_path(@project), title: 'Labels' do %span - Labels + = _('Labels') = nav_link(controller: :milestones) do = link_to project_milestones_path(@project), title: 'Milestones' do %span - Milestones + = _('Milestones') - if project_nav_tab? :external_issue_tracker = nav_link do - issue_tracker = @project.external_issue_tracker @@ -139,59 +139,80 @@ .nav-icon-container = sprite_icon('git-merge') %span.nav-item-name - Merge Requests - %span.badge.count.merge_counter.js-merge-counter + = _('Merge Requests') + %span.badge.badge-pill.count.merge_counter.js-merge-counter = number_with_delimiter(@project.open_merge_requests_count) %ul.sidebar-sub-level-items.is-fly-out-only = nav_link(controller: :merge_requests, html_options: { class: "fly-out-top-item" } ) do = link_to project_merge_requests_path(@project) do %strong.fly-out-top-item-name - #{ _('Merge Requests') } - %span.badge.count.merge_counter.js-merge-counter.fly-out-badge + = _('Merge Requests') + %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge = number_with_delimiter(@project.open_merge_requests_count) - if project_nav_tab? :pipelines - = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts, :clusters, :user, :gcp]) do + = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts]) do = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do .nav-icon-container - = sprite_icon('pipeline') + = sprite_icon('rocket') %span.nav-item-name - CI / CD + = _('CI / CD') %ul.sidebar-sub-level-items - = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts, :clusters, :user, :gcp], html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts], html_options: { class: "fly-out-top-item" } ) do = link_to project_pipelines_path(@project) do %strong.fly-out-top-item-name - #{ _('CI / CD') } + = _('CI / CD') %li.divider.fly-out-top-item - if project_nav_tab? :pipelines = nav_link(path: ['pipelines#index', 'pipelines#show']) do = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do %span - Pipelines + = _('Pipelines') - if project_nav_tab? :builds = nav_link(controller: [:jobs, :artifacts]) do = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do %span - Jobs + = _('Jobs') - if project_nav_tab? :pipelines = nav_link(controller: :pipeline_schedules) do = link_to pipeline_schedules_path(@project), title: 'Schedules', class: 'shortcuts-builds' do %span - Schedules + = _('Schedules') + + - if @project.feature_available?(:builds, current_user) && !@project.empty_repo? + = nav_link(path: 'pipelines#charts') do + = link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do + %span + = _('Charts') + + - if project_nav_tab? :operations + = nav_link(controller: [:environments, :clusters, :user, :gcp]) do + = link_to project_environments_path(@project), class: 'shortcuts-operations' do + .nav-icon-container + = sprite_icon('cloud-gear') + %span.nav-item-name + = _('Operations') + + %ul.sidebar-sub-level-items + = nav_link(controller: [:environments, :clusters, :user, :gcp], html_options: { class: "fly-out-top-item" } ) do + = link_to project_environments_path(@project) do + %strong.fly-out-top-item-name + = _('Operations') + %li.divider.fly-out-top-item - if project_nav_tab? :environments = nav_link(controller: :environments) do = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do %span - Environments + = _('Environments') - if project_nav_tab? :clusters - show_cluster_hint = show_gke_cluster_integration_callout?(@project) = nav_link(controller: [:clusters, :user, :gcp]) do - = link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-cluster' do + = link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do %span = _('Kubernetes') - if show_cluster_hint @@ -213,23 +234,22 @@ = link_to 'Auto DevOps', help_page_path('topics/autodevops/index.md') %span= _('uses Kubernetes clusters to deploy your code!') %hr - %button.btn.btn-create.btn-xs.dismiss-feature-highlight{ type: 'button' } + %button.btn.btn-create.btn-sm.dismiss-feature-highlight{ type: 'button' } %span= _("Got it!") = sprite_icon('thumb-up') - - if @project.feature_available?(:builds, current_user) && !@project.empty_repo? - = nav_link(path: 'pipelines#charts') do - = link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do - %span - Charts - - if project_nav_tab? :container_registry = nav_link(controller: %w[projects/registry/repositories]) do = link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do .nav-icon-container = sprite_icon('disk') %span.nav-item-name - Registry + = _('Registry') + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: %w[projects/registry/repositories], html_options: { class: "fly-out-top-item" } ) do + = link_to project_container_registry_index_path(@project) do + %strong.fly-out-top-item-name + = _('Registry') - if project_nav_tab? :wiki = nav_link(controller: :wikis) do @@ -237,12 +257,12 @@ .nav-icon-container = sprite_icon('book') %span.nav-item-name - Wiki + = _('Wiki') %ul.sidebar-sub-level-items.is-fly-out-only = nav_link(controller: :wikis, html_options: { class: "fly-out-top-item" } ) do = link_to get_project_wiki_path(@project) do %strong.fly-out-top-item-name - #{ _('Wiki') } + = _('Wiki') - if project_nav_tab? :snippets = nav_link(controller: :snippets) do @@ -250,12 +270,12 @@ .nav-icon-container = sprite_icon('snippet') %span.nav-item-name - Snippets + = _('Snippets') %ul.sidebar-sub-level-items.is-fly-out-only = nav_link(controller: :snippets, html_options: { class: "fly-out-top-item" } ) do = link_to project_snippets_path(@project) do %strong.fly-out-top-item-name - #{ _('Snippets') } + = _('Snippets') - if project_nav_tab? :settings = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show badges#index pages#show]) do @@ -263,7 +283,7 @@ .nav-icon-container = sprite_icon('settings') %span.nav-item-name.qa-settings-item - Settings + = _('Settings') %ul.sidebar-sub-level-items - can_edit = can?(current_user, :admin_project, @project) @@ -271,16 +291,16 @@ = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show badges#index pages#show], html_options: { class: "fly-out-top-item" } ) do = link_to edit_project_path(@project) do %strong.fly-out-top-item-name - #{ _('Settings') } + = _('Settings') %li.divider.fly-out-top-item = nav_link(path: %w[projects#edit]) do = link_to edit_project_path(@project), title: 'General' do %span - General + = _('General') = nav_link(controller: :project_members) do = link_to project_project_members_path(@project), title: 'Members' do %span - Members + = _('Members') - if can_edit = nav_link(controller: :badges) do = link_to project_settings_badges_path(@project), title: _('Badges') do @@ -290,21 +310,21 @@ = nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do = link_to project_settings_integrations_path(@project), title: 'Integrations' do %span - Integrations + = _('Integrations') = nav_link(controller: :repository) do = link_to project_settings_repository_path(@project), title: 'Repository' do %span - Repository + = _('Repository') - if @project.feature_available?(:builds, current_user) = nav_link(controller: :ci_cd) do = link_to project_settings_ci_cd_path(@project), title: 'CI / CD' do %span - CI / CD + = _('CI / CD') - if @project.pages_available? = nav_link(controller: :pages) do = link_to project_pages_path(@project), title: 'Pages' do %span - Pages + = _('Pages') - else = nav_link(controller: :project_members) do @@ -312,12 +332,12 @@ .nav-icon-container = sprite_icon('users') %span.nav-item-name - Members + = _('Members') %ul.sidebar-sub-level-items.is-fly-out-only = nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do = link_to project_project_members_path(@project) do %strong.fly-out-top-item-name - #{ _('Members') } + = _('Members') = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml index 87f4151f241..a8964b19ba1 100644 --- a/app/views/layouts/terms.html.haml +++ b/app/views/layouts/terms.html.haml @@ -29,6 +29,6 @@ = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar" = sprite_icon('angle-down', css_class: 'caret-down') - .dropdown-menu-nav.dropdown-menu-align-right + .dropdown-menu.dropdown-menu-right = render 'layouts/header/current_user_dropdown' = yield diff --git a/app/views/notify/member_invited_email.html.haml b/app/views/notify/member_invited_email.html.haml index b8b75da3f2f..6730172242b 100644 --- a/app/views/notify/member_invited_email.html.haml +++ b/app/views/notify/member_invited_email.html.haml @@ -4,7 +4,7 @@ by = link_to member.created_by.name, user_url(member.created_by) to join the - = link_to member_source.human_name, member_source.web_url + = link_to member_source.human_name, member_source.public? ? member_source.web_url : invite_url(@token) #{member_source.model_name.singular} as #{member.human_access}. %p diff --git a/app/views/notify/merge_request_unmergeable_email.html.haml b/app/views/notify/merge_request_unmergeable_email.html.haml new file mode 100644 index 00000000000..e84905a5fad --- /dev/null +++ b/app/views/notify/merge_request_unmergeable_email.html.haml @@ -0,0 +1,6 @@ +%p + Merge Request #{link_to @merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request)} can no longer be merged due to the following #{pluralize(@reasons, 'reason')}: + + %ul + - @reasons.each do |reason| + %li= reason diff --git a/app/views/notify/merge_request_unmergeable_email.text.haml b/app/views/notify/merge_request_unmergeable_email.text.haml new file mode 100644 index 00000000000..4f0901c761a --- /dev/null +++ b/app/views/notify/merge_request_unmergeable_email.text.haml @@ -0,0 +1,11 @@ +Merge Request #{@merge_request.to_reference} can no longer be merged due to the following #{pluralize(@reasons, 'reason')}: + +- @reasons.each do |reason| + * #{reason} + +Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)} + += merge_path_description(@merge_request, 'to') + +Author: #{@merge_request.author_name} +Assignee: #{@merge_request.assignee_name} diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml index 38dab104eb5..baafaa6e3a0 100644 --- a/app/views/notify/pipeline_failed_email.html.haml +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -12,7 +12,7 @@ %tr.section %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } - %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } + %table.table-info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project @@ -114,7 +114,7 @@ = failed.size failed #{'build'.pluralize(failed.size)}. -%tr.warning +%tr.table-warning %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" } Logs may contain sensitive data. Please consider before forwarding this email. %tr.section diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml index 7b06e8afa0b..4fe3c4c8269 100644 --- a/app/views/notify/pipeline_success_email.html.haml +++ b/app/views/notify/pipeline_success_email.html.haml @@ -1,4 +1,4 @@ -%tr.success +%tr.table-success %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" } %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } %tbody @@ -12,7 +12,7 @@ %tr.section %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" } - %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } + %table.table-info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" } %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project diff --git a/app/views/profiles/_event_table.html.haml b/app/views/profiles/_event_table.html.haml index d0ad90ac6cc..37466f7c821 100644 --- a/app/views/profiles/_event_table.html.haml +++ b/app/views/profiles/_event_table.html.haml @@ -9,6 +9,6 @@ Signed in with = event.details[:with] authentication - %span.pull-right= time_ago_with_tooltip(event.created_at) + %span.float-right= time_ago_with_tooltip(event.created_at) = paginate events, theme: "gitlab" diff --git a/app/views/profiles/active_sessions/_active_session.html.haml b/app/views/profiles/active_sessions/_active_session.html.haml index d40b771f48b..d198bfc80db 100644 --- a/app/views/profiles/active_sessions/_active_session.html.haml +++ b/app/views/profiles/active_sessions/_active_session.html.haml @@ -1,6 +1,6 @@ - is_current_session = active_session.current?(session) -%li +%li.list-group-item .pull-left.append-right-10{ data: { toggle: 'tooltip' }, title: active_session.human_device_type } = active_session_device_type_icon(active_session) diff --git a/app/views/profiles/active_sessions/index.html.haml b/app/views/profiles/active_sessions/index.html.haml index d0250bb4eab..8688a52843d 100644 --- a/app/views/profiles/active_sessions/index.html.haml +++ b/app/views/profiles/active_sessions/index.html.haml @@ -10,5 +10,6 @@ .col-lg-8 .append-bottom-default - %ul.well-list - = render partial: 'profiles/active_sessions/active_session', collection: @sessions + .card.border-0 + %ul.list-group.list-group-flush + = render partial: 'profiles/active_sessions/active_session', collection: @sessions diff --git a/app/views/profiles/chat_names/_chat_name.html.haml b/app/views/profiles/chat_names/_chat_name.html.haml index c7094800fb2..9e82e47c1e1 100644 --- a/app/views/profiles/chat_names/_chat_name.html.haml +++ b/app/views/profiles/chat_names/_chat_name.html.haml @@ -24,4 +24,4 @@ Never %td - = link_to 'Remove', profile_chat_name_path(chat_name), method: :delete, class: 'btn btn-danger pull-right', data: { confirm: 'Are you sure you want to revoke this nickname?' } + = link_to 'Remove', profile_chat_name_path(chat_name), method: :delete, class: 'btn btn-danger float-right', data: { confirm: 'Are you sure you want to revoke this nickname?' } diff --git a/app/views/profiles/chat_names/new.html.haml b/app/views/profiles/chat_names/new.html.haml index 5bf22aa94f1..d86941b7a29 100644 --- a/app/views/profiles/chat_names/new.html.haml +++ b/app/views/profiles/chat_names/new.html.haml @@ -9,7 +9,7 @@ .actions = form_tag profile_chat_names_path, method: :post do = hidden_field_tag :token, @chat_name_token.token - = submit_tag "Authorize", class: "btn btn-success wide pull-left" + = submit_tag "Authorize", class: "btn btn-success wide float-left" = form_tag deny_profile_chat_names_path, method: :delete do = hidden_field_tag :token, @chat_name_token.token = submit_tag "Deny", class: "btn btn-danger prepend-left-10" diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index e3c2bd1150e..914bd4eb57c 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -32,20 +32,20 @@ %ul.well-list %li = render partial: 'shared/email_with_badge', locals: { email: @primary_email, verified: current_user.confirmed? } - %span.pull-right - %span.label.label-success Primary email + %span.float-right + %span.badge.badge-success Primary email - if @primary_email === current_user.public_email - %span.label.label-info Public email + %span.badge.badge-info Public email - if @primary_email === current_user.notification_email - %span.label.label-info Notification email + %span.badge.badge-info Notification email - @emails.each do |email| %li = render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? } - %span.pull-right + %span.float-right - if email.email === current_user.public_email - %span.label.label-info Public email + %span.badge.badge-info Public email - if email.email === current_user.notification_email - %span.label.label-info Notification email + %span.badge.badge-info Notification email - unless email.confirmed? - confirm_title = "#{email.confirmation_sent_at ? 'Resend' : 'Send'} confirmation email" = link_to confirm_title, resend_confirmation_instructions_profile_email_path(email), method: :put, class: 'btn btn-sm btn-warning prepend-left-10' diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml index 5ed517c1ef6..d1fd7bc8e71 100644 --- a/app/views/profiles/gpg_keys/_key.html.haml +++ b/app/views/profiles/gpg_keys/_key.html.haml @@ -1,6 +1,6 @@ %li.key-list-item - .pull-left.append-right-10 - = icon 'key', class: "settings-list-icon hidden-xs" + .float-left.append-right-10 + = icon 'key', class: "settings-list-icon d-none d-sm-block" .key-list-item-info - key.emails_with_verified_status.map do |email, verified| = render partial: 'shared/email_with_badge', locals: { email: email, verified: verified } @@ -14,7 +14,7 @@ - key.subkeys.each do |subkey| %li %code= subkey.fingerprint - .pull-right + .float-right %span.key-created-at created #{time_ago_with_tooltip(key.created_at)} = link_to profile_gpg_key_path(key), data: { confirm: 'Are you sure? Removing this GPG key does not affect already signed commits.' }, method: :delete, class: "btn btn-danger prepend-left-10" do diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml index 103446243e5..ce20994b0f4 100644 --- a/app/views/profiles/keys/_key.html.haml +++ b/app/views/profiles/keys/_key.html.haml @@ -1,9 +1,9 @@ %li.key-list-item - .pull-left.append-right-10 + .float-left.append-right-10 - if key.valid? - = icon 'key', class: 'settings-list-icon hidden-xs' + = icon 'key', class: 'settings-list-icon d-none d-sm-block' - else - = icon 'exclamation-triangle', class: 'settings-list-icon hidden-xs has-tooltip', + = icon 'exclamation-triangle', class: 'settings-list-icon d-none d-sm-block has-tooltip', title: key.errors.full_messages.join(', ') @@ -15,7 +15,7 @@ .last-used-at last used: = key.last_used_at ? time_ago_with_tooltip(key.last_used_at) : 'n/a' - .pull-right + .float-right %span.key-created-at created #{time_ago_with_tooltip(key.created_at)} = link_to path_to_key(key, is_admin), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-transparent prepend-left-10" do diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml index 77521417f47..02d5b08f7a3 100644 --- a/app/views/profiles/keys/_key_details.html.haml +++ b/app/views/profiles/keys/_key_details.html.haml @@ -1,8 +1,8 @@ - is_admin = defined?(admin) ? true : false .row.prepend-top-default .col-md-4 - .panel.panel-default - .panel-heading + .card + .card-header SSH Key %ul.well-list %li @@ -23,5 +23,5 @@ %pre.well-pre = @key.key .col-md-12 - .pull-right + .float-right = link_to 'Remove', path_to_key(@key, is_admin), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key" diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml index 537bba21f4a..a12246bcdcc 100644 --- a/app/views/profiles/notifications/_group_settings.html.haml +++ b/app/views/profiles/notifications/_group_settings.html.haml @@ -8,5 +8,5 @@ %span.str-truncated = link_to group.name, group_path(group) - .pull-right + .float-right = render 'shared/notifications/button', notification_setting: setting diff --git a/app/views/profiles/notifications/_project_settings.html.haml b/app/views/profiles/notifications/_project_settings.html.haml index 5b2a69b8891..823fec3fab4 100644 --- a/app/views/profiles/notifications/_project_settings.html.haml +++ b/app/views/profiles/notifications/_project_settings.html.haml @@ -8,5 +8,5 @@ %span.str-truncated = link_to_project(project) - .pull-right + .float-right = render 'shared/notifications/button', notification_setting: setting diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 8f099aa6dd7..71ea625e5d5 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -29,7 +29,7 @@ = label_tag :global_notification_level, "Global notification level", class: "label-light" %br .clearfix - .form-group.pull-left.global-notification-setting + .form-group.float-left.global-notification-setting = render 'shared/notifications/button', notification_setting: @global_notification_setting .clearfix diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index c606b5a1e6c..8a51a30191a 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -20,7 +20,7 @@ .form-group = f.label :current_password, class: 'label-light' = f.password_field :current_password, required: true, class: 'form-control' - %p.help-block + %p.form-text.text-muted You must provide your current password in order to change it. .form-group = f.label :password, 'New password', class: 'label-light' diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml index 2eb9fac57c3..2176d7f8a31 100644 --- a/app/views/profiles/passwords/new.html.haml +++ b/app/views/profiles/passwords/new.html.haml @@ -2,7 +2,7 @@ - header_title "New Password" %h3.page-title Setup new password %hr -= form_for @user, url: profile_password_path, method: :post, html: { class: 'form-horizontal '} do |f| += form_for @user, url: profile_password_path, method: :post do |f| %p.slead Please set a new password before proceeding. %br @@ -11,14 +11,14 @@ = form_errors(@user) - unless @user.password_automatically_set? - .form-group - = f.label :current_password, class: 'control-label' + .form-group.row + = f.label :current_password, class: 'col-form-label col-sm-2' .col-sm-10= f.password_field :current_password, required: true, class: 'form-control' - .form-group - = f.label :password, class: 'control-label' + .form-group.row + = f.label :password, class: 'col-form-label col-sm-2' .col-sm-10= f.password_field :password, required: true, class: 'form-control' - .form-group - = f.label :password_confirmation, class: 'control-label' + .form-group.row + = f.label :password_confirmation, class: 'col-form-label col-sm-2' .col-sm-10 = f.password_field :password_confirmation, required: true, class: 'form-control' .form-actions diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index 9b87a7aaca8..d253e8e456e 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -20,9 +20,9 @@ .form-group .input-group = text_field_tag 'created-personal-access-token', @new_personal_access_token, readonly: true, class: "form-control js-select-on-focus", 'aria-describedby' => "created-personal-access-token-help-block" - %span.input-group-btn - = clipboard_button(text: @new_personal_access_token, title: "Copy personal access token to clipboard", placement: "left", class: "btn-default btn-clipboard") - %span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again. + %span.input-group-append + = clipboard_button(text: @new_personal_access_token, title: "Copy personal access token to clipboard", placement: "left", class: "input-group-text btn-default btn-clipboard") + %span#created-personal-access-token-help-block.form-text.text-muted.text-danger Make sure you save it - you won't be able to access it again. %hr @@ -42,7 +42,7 @@ .col-lg-8.rss-token-reset = label_tag :rss_token, 'RSS token', class: "label-light" = text_field_tag :rss_token, current_user.rss_token, class: 'form-control', readonly: true, onclick: 'this.select()' - %p.help-block + %p.form-text.text-muted Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds as if they were you. You should = link_to 'reset it', [:reset, :rss_token, :profile], method: :put, data: { confirm: 'Are you sure? Any RSS URLs currently in use will stop working.' } @@ -61,7 +61,7 @@ .col-lg-8.incoming-email-token-reset = label_tag :incoming_email_token, 'Incoming email token', class: "label-light" = text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control', readonly: true, onclick: 'this.select()' - %p.help-block + %p.form-text.text-muted Keep this token secret. Anyone who gets ahold of it can create issues as if they were you. You should = link_to 'reset it', [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: 'Are you sure? Any issue email addresses currently in use will stop working.' } diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 6aefd97bb96..ab5565cfdaf 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -51,7 +51,7 @@ = f.label :layout, class: 'label-light' do Layout width = f.select :layout, layout_choices, {}, class: 'form-control' - .help-block + .form-text.text-muted Choose between fixed (max. 1200px) and fluid (100%) application layout. .form-group = f.label :dashboard, class: 'label-light' do @@ -61,7 +61,7 @@ = f.label :project_view, class: 'label-light' do Project overview content = f.select :project_view, project_view_choices, {}, class: 'form-control' - .help-block + .form-text.text-muted Choose what content you want to see on a project’s overview page .form-group = f.submit 'Save changes', class: 'btn btn-save' diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index e497eab32e0..fbb29e7a0d9 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -26,7 +26,7 @@ %button.btn.js-choose-user-avatar-button{ type: 'button' }= _("Choose file...") %span.avatar-file-name.prepend-left-default.js-avatar-filename= _("No file chosen") = f.file_field_without_bootstrap :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*' - .help-block= _("The maximum file size allowed is 200KB.") + .form-text.text-muted= _("The maximum file size allowed is 200KB.") - if @user.avatar? %hr = link_to _('Remove avatar'), profile_avatar_path, data: { confirm: _('Avatar will be removed. Are you sure?') }, method: :delete, class: 'btn btn-danger btn-inverted' diff --git a/app/views/profiles/two_factor_auths/_codes.html.haml b/app/views/profiles/two_factor_auths/_codes.html.haml index 43b58be7f98..93722d7b034 100644 --- a/app/views/profiles/two_factor_auths/_codes.html.haml +++ b/app/views/profiles/two_factor_auths/_codes.html.haml @@ -4,7 +4,7 @@ %b will lose access to your account. -.codes.well +.codes.card %ul - @codes.each do |code| %li diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index d1eae05c46c..e35ebdea435 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -86,7 +86,7 @@ %tr %td= registration.name.presence || "<no name set>" %td= registration.created_at.to_date.to_s(:medium) - %td= link_to "Delete", profile_u2f_registration_path(registration), method: :delete, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to delete this device? This action cannot be undone." } + %td= link_to "Delete", profile_u2f_registration_path(registration), method: :delete, class: "btn btn-danger float-right", data: { confirm: "Are you sure you want to delete this device? This action cannot be undone." } - else .settings-message.text-center diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 043057e79ee..075badb9e56 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -37,7 +37,7 @@ = render 'projects/buttons/star' = render 'projects/buttons/fork' - %span.hidden-xs + %span.d-none.d-sm-inline - if can?(current_user, :download_code, @project) .project-clone-holder = render "shared/clone_panel" diff --git a/app/views/projects/_issuable_by_email.html.haml b/app/views/projects/_issuable_by_email.html.haml index c137e38ed50..e3dc0677bd6 100644 --- a/app/views/projects/_issuable_by_email.html.haml +++ b/app/views/projects/_issuable_by_email.html.haml @@ -17,8 +17,8 @@ You can create a new #{name} inside this project by sending an email to the following email address: .email-modal-input-group.input-group = text_field_tag :issuable_email, email, class: "monospace js-select-on-focus form-control", readonly: true - .input-group-btn - = clipboard_button(target: '#issuable_email', class: 'btn btn-clipboard btn-transparent hidden-xs') + .input-group-append + = clipboard_button(target: '#issuable_email', class: 'btn btn-clipboard input-group-text btn-transparent d-none d-sm-block') = mail_to email, class: 'btn btn-clipboard btn-transparent', subject: _("Enter the #{name} title"), body: _("Enter the #{name} description"), diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml index f6d396c8127..3b66fdbdf1a 100644 --- a/app/views/projects/_last_push.html.haml +++ b/app/views/projects/_last_push.html.haml @@ -1,6 +1,6 @@ - event = last_push_event - if event && show_last_push_widget?(event) - .row-content-block.top-block.hidden-xs.white + .row-content-block.top-block.d-none.d-sm-block.white .event-last-push .event-last-push-text %span= s_("LastPushEvent|You pushed to") diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 8212ab9a31e..8fb6aa55436 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -9,7 +9,7 @@ .md-area .md-header - %ul.nav-links.clearfix + %ul.nav.nav-tabs.nav-links.clearfix %li.md-header-tab.active %a.js-md-write-button{ href: "#md-write-holder", tabindex: -1 } Write diff --git a/app/views/projects/_merge_request_fast_forward_settings.html.haml b/app/views/projects/_merge_request_fast_forward_settings.html.haml index f455522d17c..2f08a28e26e 100644 --- a/app/views/projects/_merge_request_fast_forward_settings.html.haml +++ b/app/views/projects/_merge_request_fast_forward_settings.html.haml @@ -1,7 +1,7 @@ - form = local_assigns.fetch(:form) - project = local_assigns.fetch(:project) -.radio +.form-check = label_tag :project_merge_method_ff do = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff" %strong Fast-forward merge diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml index f6e5712ce81..762a263656d 100644 --- a/app/views/projects/_merge_request_merge_settings.html.haml +++ b/app/views/projects/_merge_request_merge_settings.html.haml @@ -1,7 +1,7 @@ - form = local_assigns.fetch(:form) .form-group - .checkbox.builds-feature{ class: ("hidden" if @project && @project.project_feature.send(:builds_access_level) == 0) } + .form-check.builds-feature{ class: ("hidden" if @project && @project.project_feature.send(:builds_access_level) == 0) } = form.label :only_allow_merge_if_pipeline_succeeds do = form.check_box :only_allow_merge_if_pipeline_succeeds %strong Only allow merge requests to be merged if the pipeline succeeds @@ -9,15 +9,15 @@ %span.descr Pipelines need to be configured to enable this feature. = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds'), target: '_blank' - .checkbox + .form-check = 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 - .checkbox + .form-check = form.label :resolve_outdated_diff_discussions do = form.check_box :resolve_outdated_diff_discussions %strong Automatically resolve merge request diff discussions when they become outdated - .checkbox + .form-check = form.label :printing_merge_request_link_enabled do = form.check_box :printing_merge_request_link_enabled %strong Show link to create/view merge request when pushing from the command line diff --git a/app/views/projects/_merge_request_rebase_settings.html.haml b/app/views/projects/_merge_request_rebase_settings.html.haml index 54e0b73d24c..93895a55435 100644 --- a/app/views/projects/_merge_request_rebase_settings.html.haml +++ b/app/views/projects/_merge_request_rebase_settings.html.haml @@ -1,6 +1,6 @@ - form = local_assigns.fetch(:form) -.radio +.form-check = label_tag :project_merge_method_rebase_merge do = form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio" %strong Merge commit with semi-linear history diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml index fd0c419cdac..a9ddcd94865 100644 --- a/app/views/projects/_merge_request_settings.html.haml +++ b/app/views/projects/_merge_request_settings.html.haml @@ -3,7 +3,7 @@ .form-group = label_tag :merge_method_merge, class: 'label-light' do Merge method - .radio + .form-check = label_tag :project_merge_method_merge do = form.radio_button :merge_method, :merge, class: "js-merge-method-radio" %strong Merge commit diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index 241bc3dbca0..6366a2f729a 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -9,13 +9,15 @@ Project path .input-group - if current_user.can_select_namespace? - .input-group-addon.has-tooltip{ title: root_url } - = root_url + .input-group-prepend.has-tooltip{ title: root_url } + .input-group-text + = root_url = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), {}, { class: 'select2 js-select-namespace qa-project-namespace-select', tabindex: 1} - else - .input-group-addon.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' } - #{user_url(current_user.username)}/ + .input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' } + .input-group-text + #{user_url(current_user.username)}/ = f.hidden_field :namespace_id, value: current_user.namespace_id .form-group.project-path.col-sm-6 = f.label :path, class: 'label-light' do @@ -23,7 +25,7 @@ Project name = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true - if current_user.can_create_group? - .help-block + .form-text.text-muted Want to house several dependent projects under the same namespace? = link_to "Create a group", new_group_path diff --git a/app/views/projects/_new_project_push_tip.html.haml b/app/views/projects/_new_project_push_tip.html.haml index 9bc69211d12..22e9522c0e7 100644 --- a/app/views/projects/_new_project_push_tip.html.haml +++ b/app/views/projects/_new_project_push_tip.html.haml @@ -3,9 +3,9 @@ = label_tag(:push_to_create_tip, _("Private projects can be created in your personal namespace with:"), class: "weight-normal") %p.input-group.project-tip-command - %span.input-group-btn + %span = text_field_tag :push_to_create_tip, push_to_create_project_command, class: "js-select-on-focus form-control monospace", readonly: true, aria: { label: _("Push project from command line") } - %span.input-group-btn - = clipboard_button(text: push_to_create_project_command, title: _("Copy command to clipboard"), placement: "right") + %span.input-group-append + = clipboard_button(text: push_to_create_project_command, title: _("Copy command to clipboard"), class: 'input-group-text', placement: "right") %p = link_to("What does this command do?", help_page_path("gitlab-basics/create-project", anchor: "push-to-create-a-new-project"), target: "_blank") diff --git a/app/views/projects/_project_templates.html.haml b/app/views/projects/_project_templates.html.haml index d50175727be..9d27f51926e 100644 --- a/app/views/projects/_project_templates.html.haml +++ b/app/views/projects/_project_templates.html.haml @@ -14,11 +14,12 @@ %label.label-light Template .input-group.template-input-group - .input-group-addon - .selected-icon - - Gitlab::ProjectTemplate.all.each do |template| - = custom_icon(template.logo) - .selected-template + .input-group-prepend + .input-group-text + .selected-icon + - Gitlab::ProjectTemplate.all.each do |template| + = custom_icon(template.logo) + .selected-template %button.btn.btn-default.change-template{ type: "button" } Change template = render 'new_project_fields', f: f, project_name_id: "template-project-name" diff --git a/app/views/projects/_stat_anchor_list.html.haml b/app/views/projects/_stat_anchor_list.html.haml index a115b65938b..8bffd1396ae 100644 --- a/app/views/projects/_stat_anchor_list.html.haml +++ b/app/views/projects/_stat_anchor_list.html.haml @@ -1,8 +1,8 @@ - anchors = local_assigns.fetch(:anchors, []) - return unless anchors.any? -%ul.nav +%ul.nav.justify-content-center - anchors.each do |anchor| - %li - = link_to_if anchor.link, anchor.label, anchor.link, class: anchor.enabled ? 'stat-link' : "btn btn-#{anchor.class_modifier || 'missing'}" do + %li.nav-item + = link_to_if anchor.link, anchor.label, anchor.link, class: anchor.enabled ? 'nav-link stat-link' : "nav-link btn btn-#{anchor.class_modifier || 'missing'}" do %span.stat-text= anchor.label diff --git a/app/views/projects/artifacts/file.html.haml b/app/views/projects/artifacts/file.html.haml index 2942d618a42..aac7a1870df 100644 --- a/app/views/projects/artifacts/file.html.haml +++ b/app/views/projects/artifacts/file.html.haml @@ -5,11 +5,11 @@ .tree-holder .nav-block %ul.breadcrumb.repo-breadcrumb - %li + %li.breadcrumb-item = link_to 'Artifacts', browse_project_job_artifacts_path(@project, @build) - path_breadcrumbs do |title, path| - title = truncate(title, length: 40) - %li + %li.breadcrumb-item - if path == @path = link_to file_project_job_artifacts_path(@project, @build, path) do %strong= title @@ -22,7 +22,7 @@ .js-file-title.file-title-flex-parent = render 'projects/blob/header_content', blob: blob - .file-actions.hidden-xs + .file-actions.d-none.d-sm-block = render 'projects/blob/viewer_switcher', blob: blob .btn-group{ role: "group" }< diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index e45861ac08d..e90916e340d 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -22,7 +22,7 @@ .commit-row-title %span.item-title.str-truncated-100 = link_to_markdown commit.title, project_commit_path(@project, commit.id), class: "cdark", title: commit.title - .pull-right + .float-right = link_to commit.short_id, project_commit_path(@project, commit), class: "commit-sha" .light diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 849716a679b..a4b1b496b69 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -1,6 +1,6 @@ = render "projects/blob/breadcrumb", blob: blob -.info-well.hidden-xs +.info-well.d-none.d-sm-block .well-segment %ul.blob-commit-info = render 'projects/commits/commit', commit: @last_commit, project: @project, ref: @ref diff --git a/app/views/projects/blob/_breadcrumb.html.haml b/app/views/projects/blob/_breadcrumb.html.haml index 1c148de9678..a4fb5f6ba88 100644 --- a/app/views/projects/blob/_breadcrumb.html.haml +++ b/app/views/projects/blob/_breadcrumb.html.haml @@ -5,12 +5,12 @@ = render 'shared/ref_switcher', destination: 'blob', path: @path %ul.breadcrumb.repo-breadcrumb - %li + %li.breadcrumb-item = link_to project_tree_path(@project, @ref) do = @project.path - path_breadcrumbs do |title, path| - title = truncate(title, length: 40) - %li + %li.breadcrumb-item - if path == @path = link_to project_blob_path(@project, tree_join(@ref, path)) do %strong= title diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index c9fa90acd11..8560b72fe85 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -16,7 +16,7 @@ = text_field_tag 'file_name', params[:file_name], placeholder: "File name", required: true, class: 'form-control new-file-name js-file-path-name-input' - .pull-right.file-buttons + .float-right.file-buttons = button_tag class: 'soft-wrap-toggle btn', type: 'button', tabindex: '-1' do %span.no-wrap = custom_icon('icon_no_wrap') diff --git a/app/views/projects/blob/_header_content.html.haml b/app/views/projects/blob/_header_content.html.haml index 5d457a50c49..4bef45932d0 100644 --- a/app/views/projects/blob/_header_content.html.haml +++ b/app/views/projects/blob/_header_content.html.haml @@ -10,4 +10,4 @@ = number_to_human_size(blob.raw_size) - if blob.stored_externally? && blob.external_storage == :lfs - %span.label.label-lfs.append-right-5 LFS + %span.badge.label-lfs.append-right-5 LFS diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml index 48ff66900be..e7a4e3d67cb 100644 --- a/app/views/projects/blob/_new_dir.html.haml +++ b/app/views/projects/blob/_new_dir.html.haml @@ -5,9 +5,9 @@ %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title= _('Create New Directory') .modal-body - = form_tag project_create_dir_path(@project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-quick-submit js-requires-input' do - .form-group - = label_tag :dir_name, _('Directory name'), class: 'control-label' + = form_tag project_create_dir_path(@project, @id), method: :post, remote: false, class: 'js-create-dir-form js-quick-submit js-requires-input' do + .form-group.row + = label_tag :dir_name, _('Directory name'), class: 'col-form-label col-sm-2' .col-sm-10 = text_field_tag :dir_name, params[:dir_name], required: true, class: 'form-control' diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml index 750bdef3308..4628ecff3d6 100644 --- a/app/views/projects/blob/_remove.html.haml +++ b/app/views/projects/blob/_remove.html.haml @@ -6,10 +6,10 @@ %h3.page-title Delete #{@blob.name} .modal-body - = form_tag project_blob_path(@project, @id), method: :delete, class: 'form-horizontal js-delete-blob-form js-quick-submit js-requires-input' do + = form_tag project_blob_path(@project, @id), method: :delete, class: 'js-delete-blob-form js-quick-submit js-requires-input' do = render 'shared/new_commit_form', placeholder: "Delete #{@blob.name}" - .form-group - .col-sm-offset-2.col-sm-10 + .form-group.row + .offset-sm-2.col-sm-10 = button_tag 'Delete file', class: 'btn btn-remove btn-remove-file' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index 182d02376bf..60a49441ce8 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -5,7 +5,7 @@ %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title= title .modal-body - = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal', data: { method: method } do + = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form', data: { method: method } do .dropzone .dropzone-previews.blob-upload-dropzone-previews %p.dz-message.light diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 9d90251ab66..27cf040da7c 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -15,7 +15,7 @@ Edit file = render 'template_selectors' .file-editor - %ul.nav-links.no-bottom.js-edit-mode + %ul.nav-links.no-bottom.js-edit-mode.nav.nav-tabs %li.active = link_to '#editor' do Write @@ -24,7 +24,7 @@ = link_to '#preview', 'data-preview-url' => project_preview_blob_path(@project, @id) do = editing_preview_title(@blob.name) - = form_tag(project_update_blob_path(@project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do + = form_tag(project_update_blob_path(@project, @id), method: :put, class: 'js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}" = hidden_field_tag 'last_commit_sha', @last_commit_sha diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index fa091d8f6ef..39442564a2b 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -7,7 +7,7 @@ New file = render 'template_selectors' .file-editor - = form_tag(project_create_blob_path(@project, @id), method: :post, class: 'form-horizontal js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do + = form_tag(project_create_blob_path(@project, @id), method: :post, class: 'js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do = render 'projects/blob/editor', ref: @ref = render 'shared/new_commit_form', placeholder: "Add new file" diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 0e012b5a216..5b1f2d8953b 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -12,13 +12,13 @@ = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name prepend-left-8' do = branch.name - if branch.name == @repository.root_ref - %span.label.label-primary.prepend-left-5 default + %span.badge.badge-primary.prepend-left-5 default - elsif merged - %span.label.label-info.has-tooltip.prepend-left-5{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } } + %span.badge.badge-info.has-tooltip.prepend-left-5{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } } = s_('Branches|merged') - if protected_branch?(@project, branch) - %span.label.label-success.prepend-left-5 + %span.badge.badge-success.prepend-left-5 = s_('Branches|protected') .block-truncated @@ -28,7 +28,7 @@ = s_('Branches|Cant find HEAD commit for this branch') - if branch.name != @repository.root_ref - .divergence-graph.hidden-xs{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind), + .divergence-graph.d-none.d-sm-block{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind), default_branch: @repository.root_ref, number_commits_ahead: diverging_count_label(number_commits_ahead) } } .graph-side @@ -39,7 +39,7 @@ .bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" } %span.count.count-ahead= diverging_count_label(number_commits_ahead) - .controls.hidden-xs< + .controls.d-none.d-sm-block< - if merge_project && create_mr_button?(@repository.root_ref, branch.name) = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do = _('Merge request') diff --git a/app/views/projects/branches/_panel.html.haml b/app/views/projects/branches/_panel.html.haml index 12e5a8e8d69..398f76d379a 100644 --- a/app/views/projects/branches/_panel.html.haml +++ b/app/views/projects/branches/_panel.html.haml @@ -7,13 +7,13 @@ - return unless branches.any? -.panel.panel-default.prepend-top-10 - .panel-heading - %h4.panel-title +.card.prepend-top-10 + .card-header + %h4.card-title = panel_title %ul.content-list.all-branches - branches.first(overview_max_branches).each do |branch| = render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch) - if branches.size > overview_max_branches - .panel-footer.text-center + .card-footer.text-center = link_to show_more_text, project_branches_filtered_path(project, state: state), id: "state-#{state}", data: { state: state } diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 5dcc72d8263..d6568c9f64a 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -3,7 +3,7 @@ %div{ class: container_class } .top-area.adjust - %ul.nav-links.issues-state-filters + %ul.nav-links.issues-state-filters.nav.nav-tabs %li{ class: active_when(@mode == 'overview') }> = link_to s_('Branches|Overview'), project_branches_path(@project), title: s_('Branches|Show overview of the branches') @@ -26,7 +26,7 @@ %span.light = branches_sort_options_hash[@sort] = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable + %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable %li.dropdown-header = s_('Branches|Sort by') - branches_sort_options_hash.each do |value, title| diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index c7fc5a98ca8..65b414c8af2 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -9,14 +9,14 @@ New Branch %hr -= form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "form-horizontal js-create-branch-form js-requires-input" do - .form-group - = label_tag :branch_name, nil, class: 'control-label' += form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "js-create-branch-form js-requires-input" do + .form-group.row + = label_tag :branch_name, nil, class: 'col-form-label col-sm-2' .col-sm-10 = text_field_tag :branch_name, params[:branch_name], required: true, autofocus: true, class: 'form-control js-branch-name' - .help-block.text-danger.js-branch-name-error - .form-group - = label_tag :ref, 'Create from', class: 'control-label' + .form-text.text-muted.text-danger.js-branch-name-error + .form-group.row + = label_tag :ref, 'Create from', class: 'col-form-label col-sm-2' .col-sm-10.create-from .dropdown = hidden_field_tag :ref, default_ref @@ -24,7 +24,7 @@ .text-left.dropdown-toggle-text= default_ref = icon('chevron-down') = render 'shared/ref_dropdown', dropdown_class: 'wide' - .help-block Existing branch name, tag, or commit SHA + .form-text.text-muted Existing branch name, tag, or commit SHA .form-actions = button_tag 'Create branch', class: 'btn btn-create', tabindex: 3 = link_to 'Cancel', project_branches_path(@project), class: 'btn btn-cancel' diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index f49f6e630d2..c75093c4c24 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -7,7 +7,7 @@ = sprite_icon('download') = icon("caret-down") %span.sr-only= _('Select Archive Format') - %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } + %ul.dropdown-menu.dropdown-menu-right{ role: 'menu' } %li.dropdown-header #{ _('Source code') } %li diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 2e86a7d36d7..84245d72f4a 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -11,7 +11,7 @@ %a.btn.dropdown-toggle.has-tooltip{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...') } = icon('plus') = icon("caret-down") - %ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown + %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown - if can_create_issue || merge_project || can_create_project_snippet %li.dropdown-header= _('This project') diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 9126476e79e..44c1453e239 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -41,14 +41,14 @@ .label-container - if job.tags.any? - job.tags.each do |tag| - %span.label.label-primary + %span.badge.badge-primary = tag - if job.try(:trigger_request) - %span.label.label-info triggered + %span.badge.badge-info triggered - if job.try(:allow_failure) - %span.label.label-danger allowed to fail + %span.badge.badge-danger allowed to fail - if job.action? - %span.label.label-info manual + %span.badge.badge-info manual - if pipeline_link %td @@ -93,7 +93,7 @@ #{job.coverage}% %td - .pull-right + .float-right - if can?(current_user, :read_build, job) && job.artifacts? = link_to download_project_job_artifacts_path(job.project, job), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do = sprite_icon('download') diff --git a/app/views/projects/ci/lints/show.html.haml b/app/views/projects/ci/lints/show.html.haml index 6ca8152183d..cbda6bf2107 100644 --- a/app/views/projects/ci/lints/show.html.haml +++ b/app/views/projects/ci/lints/show.html.haml @@ -3,22 +3,21 @@ - content_for :library_javascripts do = page_specific_javascript_tag('lib/ace.js') -%h2 Check your .gitlab-ci.yml +%h2.pt-3.pb-3 Check your .gitlab-ci.yml .project-ci-linter - .row - = form_tag project_ci_lint_path(@project), method: :post do - .form-group - .col-sm-12 - .file-holder - .js-file-title.file-title.clearfix - Content of .gitlab-ci.yml - #ci-editor.ci-editor= @content - = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true) + = form_tag project_ci_lint_path(@project), method: :post do + .row .col-sm-12 - .pull-left.prepend-top-10 + .file-holder + .js-file-title.file-title.clearfix + Content of .gitlab-ci.yml + #ci-editor.ci-editor= @content + = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true) + .col-sm-12 + .float-left.prepend-top-10 = submit_tag('Validate', class: 'btn btn-success submit-yml') - .pull-right.prepend-top-10 + .float-right.prepend-top-10 = button_tag('Clear', type: 'button', class: 'btn btn-default clear-yml') .row.prepend-top-20 diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/projects/clusters/_advanced_settings.html.haml index 14979bee714..e9bdc54364b 100644 --- a/app/views/projects/clusters/_advanced_settings.html.haml +++ b/app/views/projects/clusters/_advanced_settings.html.haml @@ -7,7 +7,7 @@ - link_gke = link_to(s_('ClusterIntegration|Google Kubernetes Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer') = s_('ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke } - .well.form-group + .card.form-group %label.text-danger = s_('ClusterIntegration|Remove Kubernetes cluster integration') %p diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/projects/clusters/_empty_state.html.haml index 5f49d03b1bb..b8a3556a206 100644 --- a/app/views/projects/clusters/_empty_state.html.haml +++ b/app/views/projects/clusters/_empty_state.html.haml @@ -1,7 +1,7 @@ .row.empty-state - .col-xs-12 + .col-12 .svg-content= image_tag 'illustrations/clusters_empty.svg' - .col-xs-12 + .col-12 .text-content %h4.text-center= s_('ClusterIntegration|Integrate Kubernetes cluster automation') - link_to_help_page = link_to(_('Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') diff --git a/app/views/projects/clusters/gcp/_show.html.haml b/app/views/projects/clusters/gcp/_show.html.haml index 78cd687ef93..877e0cc876c 100644 --- a/app/views/projects/clusters/gcp/_show.html.haml +++ b/app/views/projects/clusters/gcp/_show.html.haml @@ -3,8 +3,8 @@ = s_('ClusterIntegration|Kubernetes cluster name') .input-group %input.form-control.cluster-name.js-select-on-focus{ value: @cluster.name, readonly: true } - %span.input-group-btn - = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'btn-default') + %span.input-group-append + = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'input-group-text btn-default') = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| = form_errors(@cluster) @@ -14,22 +14,22 @@ = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL') .input-group = platform_kubernetes_field.text_field :api_url, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|API URL'), readonly: true - %span.input-group-btn - = clipboard_button(text: @cluster.platform_kubernetes.api_url, title: s_('ClusterIntegration|Copy API URL'), class: 'btn-default') + %span.input-group-append + = clipboard_button(text: @cluster.platform_kubernetes.api_url, title: s_('ClusterIntegration|Copy API URL'), class: 'input-group-text btn-default') .form-group = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate') .input-group = platform_kubernetes_field.text_area :ca_cert, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), readonly: true - %span.input-group-addon.clipboard-addon - = clipboard_button(text: @cluster.platform_kubernetes.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), class: 'btn-blank') + %span.input-group-append.clipboard-addon + = clipboard_button(text: @cluster.platform_kubernetes.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), class: 'input-group-text btn-blank') .form-group = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token') .input-group = platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token js-select-on-focus', type: 'password', placeholder: s_('ClusterIntegration|Token'), readonly: true - %span.input-group-btn - %button.btn.btn-default.js-show-cluster-token{ type: 'button' } + %span.input-group-append + %button.btn.btn-default.input-group-text.js-show-cluster-token{ type: 'button' } = s_('ClusterIntegration|Show') = clipboard_button(text: @cluster.platform_kubernetes.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default') diff --git a/app/views/projects/clusters/gcp/login.html.haml b/app/views/projects/clusters/gcp/login.html.haml index ff046c59a7a..f1771349a53 100644 --- a/app/views/projects/clusters/gcp/login.html.haml +++ b/app/views/projects/clusters/gcp/login.html.haml @@ -10,7 +10,7 @@ = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine') = render 'header' .row - .col-sm-8.col-sm-offset-4.signin-with-google + .col-sm-8.offset-sm-4.signin-with-google - if @authorize_url = link_to @authorize_url do = image_tag('auth_buttons/signin_with_google.png', width: '191px') diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml index ebbf7e775c7..77d7a055474 100644 --- a/app/views/projects/clusters/user/_show.html.haml +++ b/app/views/projects/clusters/user/_show.html.haml @@ -17,9 +17,10 @@ = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token') .input-group = platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token', type: 'password', placeholder: s_('ClusterIntegration|Token'), autocomplete: 'off' - %span.input-group-addon.clipboard-addon - %button.js-show-cluster-token.btn-blank{ type: 'button' } - = s_('ClusterIntegration|Show') + %span.input-group-append.clipboard-addon + .input-group-text + %button.js-show-cluster-token.btn-blank{ type: 'button' } + = s_('ClusterIntegration|Show') .form-group = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') diff --git a/app/views/projects/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml index 36b28c731a1..eb677cff5f0 100644 --- a/app/views/projects/commit/_ajax_signature.html.haml +++ b/app/views/projects/commit/_ajax_signature.html.haml @@ -1,2 +1,2 @@ - if commit.has_signature? - %a{ href: 'javascript:void(0)', tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } } + %a{ href: 'javascript:void(0)', tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } } diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index 21e4664d4e4..430bc8f59f9 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -20,9 +20,9 @@ .modal-body - if description %p.append-bottom-20= description - = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do - .form-group.branch - = label_tag 'start_branch', branch_label, class: 'control-label' + = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "js-#{type}-form js-requires-input" do + .form-group.row.branch + = label_tag 'start_branch', branch_label, class: 'col-form-label col-sm-2' .col-sm-10 = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch' = dropdown_tag(@project.default_branch, options: { title: s_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: s_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: project_branches_path(@project), submit_form_on_click: false } }) diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml index 7338468967f..f6666921a25 100644 --- a/app/views/projects/commit/_ci_menu.html.haml +++ b/app/views/projects/commit/_ci_menu.html.haml @@ -1,10 +1,10 @@ -%ul.nav-links.no-top.no-bottom.commit-ci-menu +%ul.nav-links.no-top.no-bottom.commit-ci-menu.nav.nav-tabs = nav_link(path: 'commit#show') do = link_to project_commit_path(@project, @commit.id) do Changes - %span.badge= @diffs.size + %span.badge.badge-pill= @diffs.size - if can?(current_user, :read_pipeline, @project) = nav_link(path: 'commit#pipelines') do = link_to pipelines_project_commit_path(@project, @commit.id) do Pipelines - %span.badge.js-pipelines-mr-count= @commit.pipelines.size + %span.badge.badge-pill.js-pipelines-mr-count= @commit.pipelines.size diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 1bffb3e8bf0..886dd73c33b 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -7,7 +7,7 @@ #{ s_('CommitBoxTitle|Commit') } %span.commit-sha= @commit.short_id = clipboard_button(text: @commit.id, title: _("Copy commit SHA to clipboard")) - %span.hidden-xs authored + %span.d-none.d-sm-inline authored #{time_ago_with_tooltip(@commit.authored_date)} %span= s_('ByAuthor|by') = author_avatar(@commit, size: 24) @@ -21,17 +21,17 @@ .header-action-buttons - if defined?(@notes_count) && @notes_count > 0 - %span.btn.disabled.btn-grouped.hidden-xs.append-right-10 + %span.btn.disabled.btn-grouped.d-none.d-sm-block.append-right-10 = icon('comment') = @notes_count - = link_to project_tree_path(@project, @commit), class: "btn btn-default append-right-10 hidden-xs hidden-sm" do + = link_to project_tree_path(@project, @commit), class: "btn btn-default append-right-10 d-none d-sm-none d-md-inline" do #{ _('Browse files') } .dropdown.inline %a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } } %span= _('Options') = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right - %li.visible-xs-block.visible-sm-block + %ul.dropdown-menu.dropdown-menu-right + %li.d-block.d-sm-none.d-md-none = link_to project_tree_path(@project, @commit) do #{ _('Browse Files') } - if can_collaborate && !@commit.has_been_reverted?(current_user) diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml index aac020b42c5..c4d986ef742 100644 --- a/app/views/projects/commit/_signature_badge.html.haml +++ b/app/views/projects/commit/_signature_badge.html.haml @@ -24,5 +24,5 @@ = link_to('Learn more about signing commits', help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link') -%a{ href: 'javascript:void(0)', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } } +%a{ href: 'javascript:void(0)', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } } = label diff --git a/app/views/projects/commit/branches.html.haml b/app/views/projects/commit/branches.html.haml index 8611129b356..a91e31afc2b 100644 --- a/app/views/projects/commit/branches.html.haml +++ b/app/views/projects/commit/branches.html.haml @@ -6,7 +6,8 @@ - if @branches.any? || @tags.any? || @tags_limit_exceeded %span - = link_to "…", "#", class: "js-details-expand label label-gray" + = link_to "#", class: "js-details-expand label label-gray ref-name" do + = sprite_icon('ellipsis_h', size: 12, css_class: 'vertical-align-middle') %span.js-details-content.hide = commit_branches_links(@project, @branches) - if @tags_limit_exceeded diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index c390c9c4469..12b27eb9b66 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -18,7 +18,7 @@ = cache(cache_key, expires_in: 1.day) do %li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" } - .avatar-cell.hidden-xs + .avatar-cell.d-none.d-sm-block = author_avatar(commit, size: 36) .commit-detail.flex-list @@ -27,14 +27,14 @@ = link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "commit-row-message item-title" - else = link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title") - %span.commit-row-message.visible-xs-inline + %span.commit-row-message.d-block.d-sm-none · = commit.short_id - if commit.status(ref) - .visible-xs-inline + .d-block.d-sm-none = render_commit_status(commit, ref: ref) - if commit.description? - %button.text-expander.hidden-xs.js-toggle-button{ type: "button" } ... + %button.text-expander.d-none.d-sm-inline-block.js-toggle-button{ type: "button" } ... .commiter - commit_author_link = commit_author_link(commit, avatar: false, size: 24) @@ -46,7 +46,7 @@ %pre.commit-row-description.js-toggle-content.prepend-top-8.append-bottom-8 = preserve(markdown_field(commit, :description)) - .commit-actions.flex-row.hidden-xs + .commit-actions.flex-row.d-none.d-sm-flex - if request.xhr? = render partial: 'projects/commit/signature', object: commit.signature - else diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml index 6f5835cb9be..8f8eb2c3d5a 100644 --- a/app/views/projects/commits/_commit_list.html.haml +++ b/app/views/projects/commits/_commit_list.html.haml @@ -1,8 +1,8 @@ - commits, hidden = limited_commits(@commits) - commits = Commit.decorate(commits, @project) -.panel.panel-default - .panel-heading +.card + .card-header Commits (#{@commits.count}) - if hidden > 0 %ul.content-list diff --git a/app/views/projects/commits/_inline_commit.html.haml b/app/views/projects/commits/_inline_commit.html.haml index 26385d2f534..caaff082cc3 100644 --- a/app/views/projects/commits/_inline_commit.html.haml +++ b/app/views/projects/commits/_inline_commit.html.haml @@ -4,5 +4,5 @@ %span.str-truncated = link_to_markdown_field(commit, :title, project_commit_path(project, commit.id), class: "commit-row-message") - .pull-right + .float-right #{time_ago_with_tooltip(commit.committed_date)} diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 483cca11df9..9d254463fb6 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -15,7 +15,7 @@ %ul.breadcrumb.repo-breadcrumb = commits_breadcrumbs - .tree-controls.hidden-xs.hidden-sm + .tree-controls.d-none.d-sm-none.d-md-block - if @merge_request.present? .control = link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'btn' diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index 40cdf96e76d..07112c98804 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -1,28 +1,29 @@ = form_tag project_compare_index_path(@project), method: :post, class: 'form-inline js-requires-input js-signature-container', data: { 'signatures-path' => signatures_namespace_project_compare_index_path } do - .clearfix - - if params[:to] && params[:from] - .compare-switch-container - = link_to icon('exchange'), { from: params[:to], to: params[:from] }, class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Swap revisions' - .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown - .input-group.inline-input-group - %span.input-group-addon + - if params[:to] && params[:from] + .compare-switch-container + = link_to icon('exchange'), { from: params[:to], to: params[:from] }, class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Swap revisions' + .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown + .input-group.inline-input-group + %span.input-group-prepend + .input-group-text = s_("CompareBranches|Source") - = hidden_field_tag :to, params[:to] - = button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do - .dropdown-toggle-text.str-truncated= params[:to] || _("Select branch/tag") - = render 'shared/ref_dropdown' - .compare-ellipsis.inline ... - .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown - .input-group.inline-input-group - %span.input-group-addon + = hidden_field_tag :to, params[:to] + = button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do + .dropdown-toggle-text.str-truncated= params[:to] || _("Select branch/tag") + = render 'shared/ref_dropdown' + .compare-ellipsis.inline ... + .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown + .input-group.inline-input-group + %span.input-group-prepend + .input-group-text = s_("CompareBranches|Target") - = hidden_field_tag :from, params[:from] - = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do - .dropdown-toggle-text.str-truncated= params[:from] || _("Select branch/tag") - = render 'shared/ref_dropdown' - - = button_tag s_("CompareBranches|Compare"), class: "btn btn-create commits-compare-btn" - - if @merge_request.present? - = link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'prepend-left-10 btn' - - elsif create_mr_button? - = link_to _("Create merge request"), create_mr_path, class: 'prepend-left-10 btn' + = hidden_field_tag :from, params[:from] + = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do + .dropdown-toggle-text.str-truncated= params[:from] || _("Select branch/tag") + = render 'shared/ref_dropdown' + + = button_tag s_("CompareBranches|Compare"), class: "btn btn-create commits-compare-btn" + - if @merge_request.present? + = link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'prepend-left-10 btn' + - elsif create_mr_button? + = link_to _("Create merge request"), create_mr_path, class: 'prepend-left-10 btn' diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index 8da55664878..b6bebbabed0 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -10,7 +10,7 @@ = render "projects/commits/commit_list" = render "projects/diffs/diffs", diffs: @diffs, environment: @environment - else - .light-well + .card.bg-light .center %h4 = s_("CompareBranches|There isn't anything to compare.") diff --git a/app/views/projects/cycle_analytics/_overview.html.haml b/app/views/projects/cycle_analytics/_overview.html.haml index 9007f2c24ba..5b0d73b8c68 100644 --- a/app/views/projects/cycle_analytics/_overview.html.haml +++ b/app/views/projects/cycle_analytics/_overview.html.haml @@ -1,7 +1,7 @@ .cycle-analytics-overview .container .row - .col-md-10.col-md-offset-1 + .col-md-10.offset-md-1 .row.overview-details .col-md-6.overview-text %h4 Introducing Cycle Analytics diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 5041f322612..bdf021fd87f 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -8,21 +8,21 @@ "v-on:dismiss-overview-dialog" => "dismissOverviewDialog()" } = icon("spinner spin", "v-show" => "isLoading") .wrapper{ "v-show" => "!isLoading && !hasError" } - .panel.panel-default - .panel-heading + .card + .card-header {{ __('Pipeline Health') }} .content-block .container-fluid .row - .col-sm-3.col-xs-12.column{ "v-for" => "item in state.summary" } + .col-sm-3.col-12.column{ "v-for" => "item in state.summary" } %h3.header {{ item.value }} %p.text {{ item.title }} - .col-sm-3.col-xs-12.column + .col-sm-3.col-12.column .dropdown.inline.js-ca-dropdown %button.dropdown-menu-toggle{ "data-toggle" => "dropdown", :type => "button" } %span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }} %i.fa.fa-chevron-down - %ul.dropdown-menu.dropdown-menu-align-right + %ul.dropdown-menu.dropdown-menu-right %li %a{ "href" => "#", "data-value" => "7" } {{ n__('Last %d day', 'Last %d days', 7) }} @@ -33,8 +33,8 @@ %a{ "href" => "#", "data-value" => "90" } {{ n__('Last %d day', 'Last %d days', 90) }} .stage-panel-container - .panel.panel-default.stage-panel - .panel-heading + .card.stage-panel + .card-header %nav.col-headers %ul %li.stage-header diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml index c363180d0db..5ad8091a02b 100644 --- a/app/views/projects/deploy_keys/_form.html.haml +++ b/app/views/projects/deploy_keys/_form.html.haml @@ -1,24 +1,24 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input" } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input container" } do |f| = form_errors(@deploy_keys.new_key) - .form-group + .form-group.row = f.label :title, class: "label-light" = f.text_field :title, class: 'form-control', required: true - .form-group + .form-group.row = f.label :key, class: "label-light" = f.text_area :key, class: "form-control", rows: 5, required: true - .form-group + .form-group.row %p.light.append-bottom-0 Paste a machine public key here. Read more about how to generate it = link_to "here", help_page_path("ssh/README") = f.fields_for :deploy_keys_projects do |deploy_keys_project_form| - .form-group - .checkbox - = deploy_keys_project_form.label :can_push do - = deploy_keys_project_form.check_box :can_push - %strong Write access allowed - .form-group + .form-group.row + = deploy_keys_project_form.label :can_push do + = deploy_keys_project_form.check_box :can_push + %strong Write access allowed + .form-group.row %p.light.append-bottom-0 Allow this key to push to repository as well? (Default only allows pull access.) - = f.submit "Add key", class: "btn-create btn" + .form-group.row + = f.submit "Add key", class: "btn-create btn" diff --git a/app/views/projects/deploy_keys/edit.html.haml b/app/views/projects/deploy_keys/edit.html.haml index cd910b82b57..e009b6fef0e 100644 --- a/app/views/projects/deploy_keys/edit.html.haml +++ b/app/views/projects/deploy_keys/edit.html.haml @@ -3,7 +3,7 @@ %hr %div - = form_for [@project.namespace.becomes(Namespace), @project, @deploy_key], html: { class: 'form-horizontal js-requires-input' } do |f| + = form_for [@project.namespace.becomes(Namespace), @project, @deploy_key], html: { class: 'js-requires-input' } do |f| = render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key } .form-actions = f.submit 'Save changes', class: 'btn-save btn' diff --git a/app/views/projects/deploy_tokens/_revoke_modal.html.haml b/app/views/projects/deploy_tokens/_revoke_modal.html.haml index 085964fe22e..ace3480c815 100644 --- a/app/views/projects/deploy_tokens/_revoke_modal.html.haml +++ b/app/views/projects/deploy_tokens/_revoke_modal.html.haml @@ -2,7 +2,7 @@ .modal-dialog .modal-content .modal-header - %h4.modal-title.pull-left + %h4.modal-title.float-left = s_('DeployTokens|Revoke') %b #{token.name}? %button.close{ 'aria-label' => _('Close'), 'data-dismiss' => 'modal', type: 'button' } diff --git a/app/views/projects/deploy_tokens/_table.html.haml b/app/views/projects/deploy_tokens/_table.html.haml index 5013a9b250d..91466a6736b 100644 --- a/app/views/projects/deploy_tokens/_table.html.haml +++ b/app/views/projects/deploy_tokens/_table.html.haml @@ -24,7 +24,7 @@ - else %span.token-never-expires-label Never %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>" - %td= link_to s_('DeployTokens|Revoke'), "#", class: "btn btn-danger pull-right", data: { toggle: "modal", target: "#revoke-modal-#{token.id}"} + %td= link_to s_('DeployTokens|Revoke'), "#", class: "btn btn-danger float-right", data: { toggle: "modal", target: "#revoke-modal-#{token.id}"} = render 'projects/deploy_tokens/revoke_modal', token: token, project: project - else .settings-message.text-center diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index e2baaa625ae..e0ecf56525a 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -6,7 +6,7 @@ %button.dropdown.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' } = custom_icon('icon_play') = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right + %ul.dropdown-menu.dropdown-menu-right - actions.each do |action| - next unless can?(current_user, :update_build, action) %li diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 9f420ee86f7..077c6c68f7e 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -6,16 +6,16 @@ .content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed{ class: ("diff-files-changed-merge-request" if merge_request) } .files-changed-inner - .inline-parallel-buttons.hidden-xs.hidden-sm + .inline-parallel-buttons.d-none.d-sm-none.d-md-block - if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? } = link_to 'Expand all', url_for(safe_params.merge(expanded: 1, format: nil)), class: 'btn btn-default' - if show_whitespace_toggle - if current_controller?(:commit) - = commit_diff_whitespace_link(diffs.project, @commit, class: 'hidden-xs') + = commit_diff_whitespace_link(diffs.project, @commit, class: 'd-none d-sm-inline-block') - elsif current_controller?('projects/merge_requests/diffs') - = diff_merge_request_whitespace_link(diffs.project, @merge_request, class: 'hidden-xs') + = diff_merge_request_whitespace_link(diffs.project, @merge_request, class: 'd-none d-sm-inline-block') - elsif current_controller?(:compare) - = diff_compare_whitespace_link(diffs.project, params[:from], params[:to], class: 'hidden-xs') + = diff_compare_whitespace_link(diffs.project, params[:from], params[:to], class: 'd-none d-sm-inline-block') .btn-group = inline_diff_btn = parallel_diff_btn diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 47bfcb21cf4..b4df654c839 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -10,7 +10,7 @@ - unless diff_file.submodule? - blob = diff_file.blob - .file-actions.hidden-xs + .file-actions.d-none.d-sm-block - if blob&.readable_text? = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file", disabled: @diff_notes_disabled do = icon('comment') diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml index dbeddf6689a..4cb04d744dc 100644 --- a/app/views/projects/diffs/_file_header.html.haml +++ b/app/views/projects/diffs/_file_header.html.haml @@ -37,4 +37,4 @@ #{diff_file.a_mode} → #{diff_file.b_mode} - if diff_file.stored_externally? && diff_file.external_storage == :lfs - %span.label.label-lfs.append-right-5 LFS + %span.badge.label-lfs.append-right-5 LFS diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 6fd6018dea3..aa1112c3313 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -2,7 +2,7 @@ - sum_removed_lines = diff_files.sum(&:removed_lines) .commit-stat-summary.dropdown Showing - %button.diff-stats-summary-toggler.js-diff-stats-dropdown{ type: "button", data: { toggle: "dropdown" } }< + %button.diff-stats-summary-toggler.js-diff-stats-dropdown{ type: "button", data: { toggle: "dropdown", display: "static" } }< = pluralize(diff_files.size, "changed file") = icon("caret-down", class: "prepend-left-5") %span.diff-stats-additions-deletions-expanded#diff-stats @@ -10,7 +10,7 @@ %strong.cgreen= pluralize(sum_added_lines, 'addition') and %strong.cred= pluralize(sum_removed_lines, 'deletion') - .diff-stats-additions-deletions-collapsed.pull-right.hidden-xs.hidden-sm{ "aria-hidden": "true", "aria-describedby": "diff-stats" } + .diff-stats-additions-deletions-collapsed.float-right.d-none.d-sm-none{ "aria-hidden": "true", "aria-describedby": "diff-stats" } %strong.cgreen< +#{sum_added_lines} %strong.cred< diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml index da34a83d8e0..abe494f2974 100644 --- a/app/views/projects/diffs/_warning.html.haml +++ b/app/views/projects/diffs/_warning.html.haml @@ -1,7 +1,7 @@ .alert.alert-warning %h4 Too many changes to show. - .pull-right + .float-right - if current_controller?(:commit) = link_to "Plain diff", project_commit_path(@project, @commit, format: :diff), class: "btn btn-sm" = link_to "Email patch", project_commit_path(@project, @commit, format: :patch), class: "btn btn-sm" diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 0994498c6be..79b22766bc7 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -40,7 +40,7 @@ .form-group = f.label :tag_list, "Tags", class: 'label-light' = f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control" - %p.help-block Separate tags with commas. + %p.form-text.text-muted Separate tags with commas. %fieldset.features %h5.prepend-top-0= _("Project avatar") .form-group @@ -54,7 +54,7 @@ %button.btn.js-choose-project-avatar-button{ type: 'button' }= _("Choose file...") %span.file_name.prepend-left-default.js-avatar-filename= _("No file chosen") = f.file_field :avatar, class: "js-project-avatar-input hidden" - .help-block= _("The maximum file size allowed is 200KB.") + .form-text.text-muted= _("The maximum file size allowed is 200KB.") - if @project.avatar? %hr = link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _("Avatar will be removed. Are you sure?") }, method: :delete, class: "btn btn-danger btn-inverted" @@ -142,8 +142,9 @@ %span Path .form-group .input-group - .input-group-addon - #{URI.join(root_url, @project.namespace.full_path)}/ + .input-group-prepend + .input-group-text + #{URI.join(root_url, @project.namespace.full_path)}/ = f.text_field :path, class: 'form-control' %ul %li Be careful. Renaming a project's repository can have unintended side effects. diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index a066f9f4cca..bf6fc8af12d 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -22,7 +22,7 @@ %hr %p - - link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings')) + - link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings')) - link_to_add_kubernetes_cluster = link_to(s_('AutoDevOps|add a Kubernetes cluster'), new_project_cluster_path(@project)) = s_('AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}.').html_safe % { link_to_auto_devops_settings: link_to_auto_devops_settings, link_to_add_kubernetes_cluster: link_to_add_kubernetes_cluster } @@ -44,14 +44,14 @@ .git-empty %fieldset %h5 Git global setup - %pre.light-well + %pre.card.bg-light :preserve git config --global user.name "#{h git_user_name}" git config --global user.email "#{h git_user_email}" %fieldset %h5 Create a new repository - %pre.light-well + %pre.card.bg-light :preserve git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')} cd #{h @project.path} @@ -64,7 +64,7 @@ %fieldset %h5 Existing folder - %pre.light-well + %pre.card.bg-light :preserve cd existing_folder git init @@ -77,7 +77,7 @@ %fieldset %h5 Existing Git repository - %pre.light-well + %pre.card.bg-light :preserve cd existing_repo git remote rename origin old-origin @@ -89,4 +89,4 @@ - if can? current_user, :remove_project, @project .prepend-top-20 - = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove pull-right" + = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove float-right" diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml index a3467eb6f05..a966bfb2dd9 100644 --- a/app/views/projects/find_file/show.html.haml +++ b/app/views/projects/find_file/show.html.haml @@ -5,10 +5,10 @@ .tree-ref-holder = render 'shared/ref_switcher', destination: 'find_file', path: @path %ul.breadcrumb.repo-breadcrumb - %li + %li.breadcrumb-item = link_to project_tree_path(@project, @ref) do = @project.path - %li.file-finder + %li.file-finder.breadcrumb-item %input#file_find.form-control.file-finder-input{ type: "text", placeholder: _('Find by path'), autocomplete: 'off' } .tree-content-holder diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index 21a4702a2a9..57afc7ac9c3 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -16,7 +16,7 @@ - else = sort_title_recently_created = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-align-right + %ul.dropdown-menu.dropdown-menu-right %li - excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id] = link_to page_filter_path(sort: sort_value_recently_created, without: excluded_filters) do diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml index a603b1024eb..a603b1024eb 100755..100644 --- a/app/views/projects/forks/new.html.haml +++ b/app/views/projects/forks/new.html.haml diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 620fd1906ba..639efd34a74 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -35,10 +35,10 @@ .label-container - if generic_commit_status.tags.any? - generic_commit_status.tags.each do |tag| - %span.label.label-primary + %span.badge.badge-primary = tag - if retried - %span.label.label-warning retried + %span.badge.badge-warning retried - if pipeline_link %td diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml index 14c47a5d91c..983cb187c2f 100644 --- a/app/views/projects/graphs/charts.html.haml +++ b/app/views/projects/graphs/charts.html.haml @@ -14,7 +14,7 @@ = icon('circle') = language[:label] - .pull-right + .float-right = language[:value] \% .col-md-8 diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index c81ee6874e3..f1b14d4c4d1 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -24,4 +24,4 @@ .graphs.row #contributors-master #contributors.clearfix - %ol.contributors-list.clearfix + %ol.contributors-list.row diff --git a/app/views/projects/hook_logs/_index.html.haml b/app/views/projects/hook_logs/_index.html.haml index 8096d9530c3..3e54c3ca9f8 100644 --- a/app/views/projects/hook_logs/_index.html.haml +++ b/app/views/projects/hook_logs/_index.html.haml @@ -18,8 +18,8 @@ %tr %td = render partial: 'shared/hook_logs/status_label', locals: { hook_log: hook_log } - %td.hidden-xs - %span.label.label-gray.deploy-project-label + %td.d-none.d-sm-block + %span.badge.badge-gray.deploy-project-label = hook_log.trigger.singularize.titleize %td = truncate(hook_log.url, length: 50) diff --git a/app/views/projects/hook_logs/show.html.haml b/app/views/projects/hook_logs/show.html.haml index 1cf4105bd27..e51efa85df0 100644 --- a/app/views/projects/hook_logs/show.html.haml +++ b/app/views/projects/hook_logs/show.html.haml @@ -4,6 +4,6 @@ Request details .col-lg-9 - = link_to 'Resend Request', retry_project_hook_hook_log_path(@project, @hook, @hook_log), class: "btn btn-default pull-right prepend-left-10" + = link_to 'Resend Request', retry_project_hook_hook_log_path(@project, @hook, @hook_log), class: "btn btn-default float-right prepend-left-10" = render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log } diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml index dcc1f0e3fbe..c31aef60453 100644 --- a/app/views/projects/hooks/edit.html.haml +++ b/app/views/projects/hooks/edit.html.haml @@ -13,7 +13,7 @@ = f.submit 'Save changes', class: 'btn btn-create' = render 'shared/web_hooks/test_button', triggers: ProjectHook.triggers, hook: @hook - = link_to 'Remove', project_hook_path(@project, @hook), method: :delete, class: 'btn btn-remove pull-right', data: { confirm: 'Are you sure?' } + = link_to 'Remove', project_hook_path(@project, @hook), method: :delete, class: 'btn btn-remove float-right', data: { confirm: 'Are you sure?' } %hr diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml index 778ff91362d..16c4f21279d 100644 --- a/app/views/projects/imports/new.html.haml +++ b/app/views/projects/imports/new.html.haml @@ -5,14 +5,14 @@ %hr - if @project.import_failed? - .panel.panel-danger - .panel-heading The repository could not be imported. - .panel-body + .card.bg-danger + .card-header The repository could not be imported. + .card-body %pre :preserve #{h(sanitize_repo_path(@project, @project.import_error))} -= form_for @project, url: project_import_path(@project), method: :post, html: { class: 'form-horizontal' } do |f| += form_for @project, url: project_import_path(@project), method: :post do |f| = render "shared/import_form", f: f .form-actions diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index 8b011af78eb..2c5ffd85372 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -1,2 +1,2 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form common-note-form js-quick-submit js-requires-input' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'issue-form common-note-form js-quick-submit js-requires-input' } do |f| = render 'shared/issuable/form', f: f, issuable: @issue diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index e27f5658e87..8a14146cb87 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -12,25 +12,25 @@ = confidential_icon(issue) = link_to issue.title, issue_path(issue) - if issue.tasks? - %span.task-status.hidden-xs + %span.task-status.d-none.d-sm-inline-block = issue.task_status .issuable-info %span.issuable-reference #{issuable_reference(issue)} - %span.issuable-authored.hidden-xs + %span.issuable-authored.d-none.d-sm-inline-block · opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} by #{link_to_member(@project, issue.author, avatar: false)} - if issue.milestone - %span.issuable-milestone.hidden-xs + %span.issuable-milestone.d-none.d-sm-inline-block - = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_due_date(issue.milestone) } do + = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 'true', toggle: 'tooltip', title: milestone_tooltip_due_date(issue.milestone) } do = icon('clock-o') = issue.milestone.title - if issue.due_date - %span.issuable-due-date.hidden-xs.has-tooltip{ class: "#{'cred' if issue.overdue?}", title: _('Due date') } + %span.issuable-due-date.d-none.d-sm-inline-block.has-tooltip{ class: "#{'cred' if issue.overdue?}", title: _('Due date') } = icon('calendar') = issue.due_date.to_s(:medium) @@ -50,5 +50,5 @@ = render 'shared/issuable_meta_data', issuable: issue - .pull-right.issuable-updated-at.hidden-xs + .float-right.issuable-updated-at.d-none.d-sm-inline-block %span updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')} diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index 4b8bf578b28..76438fae663 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -14,7 +14,7 @@ = icon('spinner', class: 'fa-spin') %span.text Checking branch availability… - .btn-group.available.hide + .btn-group.available.hidden %button.btn.js-create-merge-request.btn-success.btn-inverted{ type: 'button', data: { action: data_action } } = value @@ -22,7 +22,7 @@ = icon('caret-down') .droplab-dropdown - %ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } } + %ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-right.gl-show-field-errors{ data: { dropdown: true } } - if can_create_merge_request %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: _('Create merge request') } } .menu-item @@ -40,13 +40,13 @@ %label{ for: 'new-branch-name' } = _('Branch name') %input#new-branch-name.js-branch-name.form-control{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" } - %span.js-branch-message.help-block + %span.js-branch-message.form-text.text-muted .form-group %label{ for: 'source-name' } = _('Source (branch or tag)') %input#source-name.js-ref.ref.form-control{ type: 'text', placeholder: "#{@project.default_branch}", value: "#{@project.default_branch}", data: { value: "#{@project.default_branch}" } } - %span.js-ref-message.help-block + %span.js-ref-message.form-text.text-muted .form-group %button.btn.btn-success.js-create-target{ type: 'button', data: { action: 'create-mr' } } diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index f1fc1c2316d..2d036bd4e3e 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -12,12 +12,12 @@ .detail-page-header .detail-page-header-body .issuable-status-box.status-box.status-box-issue-closed{ class: issue_button_visibility(@issue, false) } - = sprite_icon('mobile-issue-close', size: 16, css_class: 'hidden-sm hidden-md hidden-lg') - %span.hidden-xs + = sprite_icon('mobile-issue-close', size: 16, css_class: 'd-block d-sm-none') + %span.d-none.d-sm-block Closed .issuable-status-box.status-box.status-box-open{ class: issue_button_visibility(@issue, true) } - = sprite_icon('issue-open-m', size: 16, css_class: 'hidden-sm hidden-md hidden-lg') - %span.hidden-xs Open + = sprite_icon('issue-open-m', size: 16, css_class: 'd-block d-sm-none') + %span.d-none.d-sm-block Open .issuable-meta - if @issue.confidential @@ -26,15 +26,15 @@ .issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon') = issuable_meta(@issue, @project, "Issue") - %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" } + %a.btn.btn-default.float-right.d-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" } = icon('angle-double-left') .detail-page-header-actions.js-issuable-actions .clearfix.issue-btn-group.dropdown - %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } + %button.btn.btn-default.float-left.d-md-none.d-lg-none.d-xl-none{ type: "button", data: { toggle: "dropdown" } } Options = icon('caret-down') - .dropdown-menu.dropdown-menu-align-right.hidden-lg + .dropdown-menu.dropdown-menu-right.d-lg-none.d-xl-none %ul - unless current_user == @issue.author %li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue)) @@ -51,9 +51,9 @@ = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue - if can_report_spam - = link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam' + = link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-sm-none d-md-block btn btn-grouped btn-spam', title: 'Submit as spam' - if can_create_issue - = link_to new_project_issue_path(@project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do + = link_to new_project_issue_path(@project), class: 'd-none d-sm-none d-md-block btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do New issue .issue-details.issuable-details diff --git a/app/views/projects/jobs/_empty_state.html.haml b/app/views/projects/jobs/_empty_state.html.haml index 311934d9c33..ea552c73c92 100644 --- a/app/views/projects/jobs/_empty_state.html.haml +++ b/app/views/projects/jobs/_empty_state.html.haml @@ -5,10 +5,10 @@ - action = local_assigns.fetch(:action, nil) .row.empty-state - .col-xs-12 + .col-12 .svg-content{ class: illustration_size } = image_tag illustration - .col-xs-12 + .col-12 .text-content %h4.text-center= title - if content diff --git a/app/views/projects/jobs/_header.html.haml b/app/views/projects/jobs/_header.html.haml index 83a2af1dc74..b83e8dddccb 100644 --- a/app/views/projects/jobs/_header.html.haml +++ b/app/views/projects/jobs/_header.html.haml @@ -27,5 +27,5 @@ = link_to "New issue", new_project_issue_path(@project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted' - if can?(current_user, :update_build, @build) && @build.retryable? = link_to "Retry job", retry_project_job_path(@project, @build), class: 'btn btn-inverted-secondary', method: :post - %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } + %button.btn.btn-default.float-right.d-block.d-sm-none.d-md-none.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml index 826404c2008..9c9e4ef8fce 100644 --- a/app/views/projects/jobs/_sidebar.html.haml +++ b/app/views/projects/jobs/_sidebar.html.haml @@ -18,7 +18,7 @@ %span= time_ago_with_tooltip @build.artifacts_expire_at - if @build.artifacts? - .btn-group.btn-group-justified{ role: :group } + .btn-group.btn-group.d-flex{ role: :group } - if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build) = link_to keep_project_job_artifacts_path(@project, @build), class: 'btn btn-sm btn-default', method: :post do Keep @@ -42,7 +42,7 @@ - if @build.trigger_variables.any? %p - %button.btn.group.btn-group-justified.js-reveal-variables Reveal Variables + %button.btn.group.btn-group.js-reveal-variables Reveal Variables %dl.js-build-variables.trigger-build-variables.hide - @build.trigger_variables.each do |trigger_variable| @@ -83,7 +83,7 @@ - builds.select{|build| build.status == build_status}.each do |build| .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } } - tooltip = build.tooltip_message - = link_to(project_job_path(@project, build), data: { toggle: 'tooltip', html: true, title: tooltip, container: 'body' }) do + = link_to(project_job_path(@project, build), data: { toggle: 'tooltip', html: 'true', title: tooltip, container: 'body' }) do = sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right') %span{ class: "ci-status-icon-#{build.status}" } = ci_icon_for_status(build.status) diff --git a/app/views/projects/jobs/_user.html.haml b/app/views/projects/jobs/_user.html.haml index 461d503f95d..90ce581a903 100644 --- a/app/views/projects/jobs/_user.html.haml +++ b/app/views/projects/jobs/_user.html.haml @@ -1,7 +1,7 @@ by %a{ href: user_path(@build.user) } - %span.hidden-xs + %span.d-none.d-sm-inline = image_tag avatar_icon_for_user(@build.user, 24), class: "avatar s24" %strong{ data: { toggle: 'tooltip', placement: 'top', title: @build.user.to_reference } } = @build.user.name - %strong.visible-xs-inline= @build.user.to_reference + %strong.d-inline.d-sm-none= @build.user.to_reference diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index cbbcc8f1db5..ec9a04c0eab 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -15,7 +15,7 @@ - elsif @build.tags.any? This job is stuck, because you don't have any active runners online with any of these tags assigned to them: - @build.tags.each do |tag| - %span.label.label-primary + %span.badge.badge-primary = tag - else This job is stuck, because you don't have any active runners that can run this job. @@ -58,13 +58,13 @@ - if @build.running? || @build.has_trace? .build-trace-container.prepend-top-default .top-bar.js-top-bar - .js-truncated-info.truncated-info.hidden-xs.pull-left.hidden< + .js-truncated-info.truncated-info.d-none.d-sm-block.float-left.hidden< Showing last %span.js-truncated-info-size.truncated-info-size>< of log - %a.js-raw-link.raw-link{ href: raw_project_job_path(@project, @build) }>< Complete Raw - .controllers.pull-right + .controllers.float-right - if @build.has_trace? = link_to raw_project_job_path(@project, @build), title: 'Show complete raw', diff --git a/app/views/projects/mattermosts/_no_teams.html.haml b/app/views/projects/mattermosts/_no_teams.html.haml index 243dcfdc187..0377cd6586e 100644 --- a/app/views/projects/mattermosts/_no_teams.html.haml +++ b/app/views/projects/mattermosts/_no_teams.html.haml @@ -13,4 +13,4 @@ and try again. %hr .clearfix - = link_to 'Go back', edit_project_service_path(@project, @service), class: 'btn btn-lg pull-right' + = link_to 'Go back', edit_project_service_path(@project, @service), class: 'btn btn-lg float-right' diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml index 20acd476f73..361d3c61d99 100644 --- a/app/views/projects/mattermosts/_team_selection.html.haml +++ b/app/views/projects/mattermosts/_team_selection.html.haml @@ -11,7 +11,7 @@ - options = options_for_select(mattermost_teams_options(@teams), selected_id) = f.select(:team_id, options, { include_blank: 'Select team...'}, { class: 'form-control', disabled: @teams.one?, selected: selected_id, required: true }) = f.hidden_field(:team_id, value: selected_id, required: true) if @teams.one? - .help-block + .form-text.text-muted - if @teams.one? This is the only available team. - else @@ -25,7 +25,7 @@ %h4 Command trigger word %p Choose the word that will trigger commands = f.text_field(:trigger, value: @project.path, class: 'form-control', required: true) - .help-block + .form-text.text-muted %p Trigger word must be unique, and can't begin with a slash or contain any spaces. Use the word that works best for your team. @@ -41,6 +41,6 @@ = icon('external-link') %hr .clearfix - .pull-right + .float-right = link_to 'Cancel', edit_project_service_path(@project, @service), class: 'btn btn-lg' = f.submit 'Install', class: 'btn btn-save btn-lg' diff --git a/app/views/projects/mattermosts/new.html.haml b/app/views/projects/mattermosts/new.html.haml index 15829a3f143..9e293d07cb7 100644 --- a/app/views/projects/mattermosts/new.html.haml +++ b/app/views/projects/mattermosts/new.html.haml @@ -1,7 +1,7 @@ - @body_class = 'card-content' .service-installation - .inline.pull-right + .inline.float-right = custom_icon('mattermost_logo', size: 48) %h3 Install Mattermost Command - if @teams.empty? diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index 9607a7b5d06..179c1fcc684 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -1,2 +1,2 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' } do |f| = render 'shared/issuable/form', f: f, issuable: @merge_request diff --git a/app/views/projects/merge_requests/_how_to_merge.html.haml b/app/views/projects/merge_requests/_how_to_merge.html.haml index 54a661040ea..5353fa8a88f 100644 --- a/app/views/projects/merge_requests/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/_how_to_merge.html.haml @@ -1,9 +1,9 @@ -#modal_merge_info.modal +#modal_merge_info.modal{ tabindex: '-1' } .modal-dialog .modal-content .modal-header - %a.close{ href: "#", "data-dismiss" => "modal" } × %h3 Check out, review, and merge locally + %a.close{ href: "#", "data-dismiss" => "modal" } × .modal-body %p %strong Step 1. diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 027a9ff1416..cd3d896fff2 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -9,21 +9,21 @@ %span.merge-request-title-text = link_to merge_request.title, merge_request_path(merge_request) - if merge_request.tasks? - %span.task-status.hidden-xs + %span.task-status.d-none.d-sm-inline-block = merge_request.task_status .issuable-info %span.issuable-reference #{issuable_reference(merge_request)} - %span.issuable-authored.hidden-xs + %span.issuable-authored.d-none.d-sm-inline-block · opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} by #{link_to_member(@project, merge_request.author, avatar: false)} - if merge_request.milestone - %span.issuable-milestone.hidden-xs + %span.issuable-milestone.d-none.d-sm-inline-block - = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_due_date(merge_request.milestone) } do + = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 'true', toggle: 'tooltip', title: milestone_tooltip_due_date(merge_request.milestone) } do = icon('clock-o') = merge_request.milestone.title - if merge_request.target_project.default_branch != merge_request.target_branch @@ -40,17 +40,17 @@ .issuable-meta %ul.controls - if merge_request.merged? - %li.issuable-status.hidden-xs + %li.issuable-status.d-none.d-sm-inline-block MERGED - elsif merge_request.closed? - %li.issuable-status.hidden-xs + %li.issuable-status.d-none.d-sm-inline-block = icon('ban') CLOSED - if merge_request.head_pipeline - %li.issuable-pipeline-status.hidden-xs + %li.issuable-pipeline-status.d-none.d-sm-inline-block = render_pipeline_status(merge_request.head_pipeline) - if merge_request.open? && merge_request.broken? - %li.issuable-pipeline-broken.hidden-xs + %li.issuable-pipeline-broken.d-none.d-sm-inline-block = link_to merge_request_path(merge_request), class: "has-tooltip", title: _('Cannot be merged automatically') do = icon('exclamation-triangle') - if merge_request.assignee @@ -59,5 +59,5 @@ = render 'shared/issuable_meta_data', issuable: merge_request - .pull-right.issuable-updated-at.hidden-xs + .float-right.issuable-updated-at.d-none.d-sm-inline-block %span updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')} diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index 22c8b6b513d..a58179091ae 100644 --- a/app/views/projects/merge_requests/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml @@ -7,8 +7,8 @@ .detail-page-header .detail-page-header-body .issuable-status-box.status-box{ class: status_box_class(@merge_request) } - = sprite_icon(@merge_request.state_icon_name, size: 16, css_class: 'hidden-sm hidden-md hidden-lg') - %span.hidden-xs + = sprite_icon(@merge_request.state_icon_name, size: 16, css_class: 'd-block d-sm-none') + %span.d-none.d-sm-block = @merge_request.state_human_name .issuable-meta @@ -16,15 +16,15 @@ .issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon') = issuable_meta(@merge_request, @project, "Merge request") - %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" } + %a.btn.btn-default.float-right.d-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" } = icon('angle-double-left') .detail-page-header-actions.js-issuable-actions .clearfix.issue-btn-group.dropdown - %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } } + %button.btn.btn-default.float-left.d-md-none.d-lg-none.d-xl-none{ type: "button", data: { toggle: "dropdown" } } Options = icon('caret-down') - .dropdown-menu.dropdown-menu-align-right.hidden-lg + .dropdown-menu.dropdown-menu-right.d-lg-none.d-xl-none %ul - if can_update_merge_request %li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) @@ -37,6 +37,6 @@ = link_to 'Reopen', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' - if can_update_merge_request - = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped js-issuable-edit" + = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "d-none d-sm-none d-md-block btn btn-grouped js-issuable-edit" = render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request diff --git a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml index 964dc40a213..e6205f24ae6 100644 --- a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml +++ b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml @@ -11,6 +11,6 @@ Showing %strong.cred {{conflictsCountText}} between - %strong {{conflictsData.sourceBranch}} + %strong.ref-name {{conflictsData.sourceBranch}} and - %strong {{conflictsData.targetBranch}} + %strong.ref-name {{conflictsData.targetBranch}} diff --git a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml index 13026b7566a..4d84ee2488b 100644 --- a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml +++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml @@ -1,16 +1,24 @@ -.form-horizontal.resolve-conflicts-form - .form-group - %label.col-sm-2.control-label{ "for" => "commit-message" } - #{ _('Commit message') } - .col-sm-10 +- branch_name = link_to @merge_request.source_branch, project_tree_path(@merge_request.project, @merge_request.source_branch), class: "ref-name" +- translation =_('You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}') % { use_ours: '<code>Use Ours</code>', use_theirs: '<code>Use Theirs</code>', branch_name: branch_name } + +%hr +.resolve-conflicts-form + .form-group.row + .col-md-4 + %h4= _('Resolve conflicts on source branch') + .resolve-info + = translation.html_safe + .col-md-8 + %label.label-light{ "for" => "commit-message" } + #{ _('Commit message') } .commit-message-container .max-width-marker %textarea.form-control.js-commit-message#commit-message{ "v-model" => "conflictsData.commitMessage", "rows" => "5" } - .form-group - .col-sm-offset-2.col-sm-10 + .form-group.row + .offset-md-4.col-md-8 .row - .col-xs-6 + .col-6 %button.btn.btn-success.js-submit-button{ type: "button", "@click" => "commit()", ":disabled" => "!readyToCommit" } %span {{commitButtonText}} - .col-xs-6.text-right + .col-6.text-right = link_to "Cancel", project_merge_request_path(@merge_request.project, @merge_request), class: "btn btn-cancel" diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml index 773b12b4536..ca0f7d6098f 100644 --- a/app/views/projects/merge_requests/creations/_new_compare.html.haml +++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml @@ -1,14 +1,14 @@ %h3.page-title New Merge Request -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form js-requires-input" } do |f| .hide.alert.alert-danger.mr-compare-errors .js-merge-request-new-compare.row{ 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) } .col-lg-6 - .panel.panel-default.panel-new-merge-request - .panel-heading + .card.card-new-merge-request + .card-header Source branch - .panel-body.clearfix + .card-body.clearfix .merge-request-select.dropdown = f.hidden_field :source_project_id = dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-source-project" } @@ -32,10 +32,10 @@ %ul.list-unstyled.mr_source_commit .col-lg-6 - .panel.panel-default.panel-new-merge-request - .panel-heading + .card.card-new-merge-request + .card-header Target branch - .panel-body.clearfix + .card-body.clearfix - projects = target_projects(@project) .merge-request-select.dropdown = f.hidden_field :target_project_id @@ -55,7 +55,7 @@ = dropdown_filter(_("Search branches")) = dropdown_content = dropdown_loading - .panel-footer + .card-footer .text-center= icon('spinner spin', class: "js-target-loading") %ul.list-unstyled.mr_target_commit diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml index 68780cedeb1..ebcd99f2a9b 100644 --- a/app/views/projects/merge_requests/creations/_new_submit.html.haml +++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml @@ -7,10 +7,10 @@ %span into %strong.ref-name= target_title - %span.pull-right + %span.float-right = link_to 'Change branches', mr_change_branches_path(@merge_request) %hr -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' } do |f| = render 'shared/issuable/form', f: f, issuable: @merge_request, commits: @commits = f.hidden_field :source_project_id = f.hidden_field :source_branch @@ -24,20 +24,20 @@ There are no commits yet. = custom_icon ('illustration_no_commits') - else - %ul.merge-request-tabs.nav-links.no-top.no-bottom + %ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom %li.commits-tab.active = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do Commits - %span.badge= @commits.size + %span.badge.badge-pill= @commits.size - if @pipelines.any? %li.builds-tab = link_to url_for(safe_params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do Pipelines - %span.badge= @pipelines.size + %span.badge.badge-pill= @pipelines.size %li.diffs-tab = link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do Changes - %span.badge= @merge_request.diff_size + %span.badge.badge-pill= @merge_request.diff_size .tab-content #commits.commits.tab-pane.active diff --git a/app/views/projects/merge_requests/diffs/_commit_widget.html.haml b/app/views/projects/merge_requests/diffs/_commit_widget.html.haml index 2e5594f8cbe..dab95b97346 100644 --- a/app/views/projects/merge_requests/diffs/_commit_widget.html.haml +++ b/app/views/projects/merge_requests/diffs/_commit_widget.html.haml @@ -1,5 +1,5 @@ - if @commit - .info-well.hidden-xs.prepend-top-default + .info-well.d-none.d-sm-block.prepend-top-default .well-segment %ul.blob-commit-info = render 'projects/commits/commit', commit: @commit, merge_request: @merge_request, view_details: true diff --git a/app/views/projects/merge_requests/diffs/_diffs.html.haml b/app/views/projects/merge_requests/diffs/_diffs.html.haml index 986ba5ae02d..19659fe5140 100644 --- a/app/views/projects/merge_requests/diffs/_diffs.html.haml +++ b/app/views/projects/merge_requests/diffs/_diffs.html.haml @@ -5,9 +5,9 @@ - if @merge_request_diff&.empty? .row.empty-state.nothing-here-block - .col-xs-12 + .col-12 .svg-content= image_tag 'illustrations/merge_request_changes_empty.svg' - .col-xs-12 + .col-12 .text-content.text-center %p No changes between diff --git a/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml b/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml index 529fbb8547a..8d7138747fb 100644 --- a/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml +++ b/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml @@ -11,7 +11,7 @@ comparing two versions of the diff - else viewing an old version of the diff - .pull-right + .float-right = link_to diffs_project_merge_request_path(@merge_request.project, @merge_request), class: 'btn btn-sm' do Show latest version = "of the diff" if @commit diff --git a/app/views/projects/merge_requests/invalid.html.haml b/app/views/projects/merge_requests/invalid.html.haml index 6df19d6438b..749228a9664 100644 --- a/app/views/projects/merge_requests/invalid.html.haml +++ b/app/views/projects/merge_requests/invalid.html.haml @@ -10,13 +10,13 @@ - if @merge_request.for_fork? && !@merge_request.source_project fork project was removed - elsif !@merge_request.source_branch_exists? - %span.label.label-inverse= @merge_request.source_branch + %span.badge.badge-inverse= @merge_request.source_branch does not exist in - %span.label.label-info= @merge_request.source_project_path + %span.badge.badge-info= @merge_request.source_project_path - elsif !@merge_request.target_branch_exists? - %span.label.label-inverse= @merge_request.target_branch + %span.badge.badge-inverse= @merge_request.target_branch does not exist in - %span.label.label-info= @merge_request.target_project_path + %span.badge.badge-info= @merge_request.target_project_path - else of internal error diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 15a0e4d7ef5..aa3fb623e58 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -30,26 +30,26 @@ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .fade-left= icon('angle-left') .fade-right= icon('angle-right') - .nav-links.scrolling-tabs - %ul.merge-request-tabs + .nav-links.scrolling-tabs.nav.nav-tabs + %ul.merge-request-tabs.nav-tabs.nav %li.notes-tab = tab_link_for @merge_request, :show, force_link: @commit.present? do Discussion - %span.badge= @merge_request.related_notes.user.count + %span.badge.badge-pill= @merge_request.related_notes.user.count - if @merge_request.source_project %li.commits-tab = tab_link_for @merge_request, :commits do Commits - %span.badge= @commits_count + %span.badge.badge-pill= @commits_count - if @pipelines.any? %li.pipelines-tab = tab_link_for @merge_request, :pipelines do Pipelines - %span.badge.js-pipelines-mr-count= @pipelines.size + %span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size %li.diffs-tab = tab_link_for @merge_request, :diffs do Changes - %span.badge= @merge_request.diff_size + %span.badge.badge-pill= @merge_request.diff_size - if has_vue_discussions_cookie? #js-vue-discussion-counter diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 2e74b1b83cb..4cc59718715 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -1,13 +1,13 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input'} do |f| += form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'milestone-form common-note-form js-quick-submit js-requires-input'} do |f| = form_errors(@milestone) .row .col-md-6 - .form-group - = f.label :title, "Title", class: "control-label" + .form-group.row + = f.label :title, "Title", class: "col-form-label col-sm-2" .col-sm-10 = f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true - .form-group.milestone-description - = f.label :description, "Description", class: "control-label" + .form-group.row.milestone-description + = f.label :description, "Description", class: "col-form-label col-sm-2" .col-sm-10 = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project) } do = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 5ec219fdf00..b478fbbb15e 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -56,7 +56,7 @@ #delete-milestone-modal - %a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" } + %a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: "#" } = icon('angle-double-left') .detail-page-description.milestone-detail diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 4b7be9a223f..75cb8245d36 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -9,7 +9,7 @@ = button_tag class: 'btn btn-success' do = icon('search') .inline.prepend-left-20 - .checkbox.light + .form-check.light = label_tag :filter_ref do = check_box_tag :filter_ref, 1, @options[:filter_ref] %span= _("Begin with the selected commit") diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 5beaa3c6d23..35a09f06bfa 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -28,19 +28,19 @@ %template.push-new-project-tip-template= render partial: "new_project_push_tip" .col-lg-9.js-toggle-container - %ul.nav-links.gitlab-tabs{ role: 'tablist' } + %ul.nav.nav-tabs.nav-links.gitlab-tabs{ role: 'tablist' } %li{ class: active_when(active_tab == 'blank'), role: 'presentation' } %a{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' } - %span.hidden-xs Blank project - %span.visible-xs Blank + %span.d-none.d-sm-block Blank project + %span.d-block.d-sm-none Blank %li{ class: active_when(active_tab == 'template'), role: 'presentation' } %a{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' } - %span.hidden-xs Create from template - %span.visible-xs Template + %span.d-none.d-sm-block Create from template + %span.d-block.d-sm-none Template %li{ class: active_when(active_tab == 'import'), role: 'presentation' } %a{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' } - %span.hidden-xs Import project - %span.visible-xs Import + %span.d-none.d-sm-block Import project + %span.d-block.d-sm-none Import .tab-content.gitlab-tab-content .tab-pane{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' } @@ -63,7 +63,7 @@ %h4 No import options available %p Contact an administrator to enable options for importing your project. -.save-project-loader.hide +.save-project-loader.d-none .center %h2 %i.fa.fa-spinner.fa-spin diff --git a/app/views/projects/no_repo.html.haml b/app/views/projects/no_repo.html.haml index 14d880028c7..08772a0188b 100644 --- a/app/views/projects/no_repo.html.haml +++ b/app/views/projects/no_repo.html.haml @@ -21,4 +21,4 @@ - if can? current_user, :remove_project, @project .prepend-top-20 - = link_to _('Remove project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove pull-right" + = link_to _('Remove project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove float-right" diff --git a/app/views/projects/pages/_access.html.haml b/app/views/projects/pages/_access.html.haml index 82e20eeebb3..73ea30e1d3d 100644 --- a/app/views/projects/pages/_access.html.haml +++ b/app/views/projects/pages/_access.html.haml @@ -1,8 +1,8 @@ - if @project.pages_deployed? - .panel.panel-default - .panel-heading + .card + .card-header Access pages - .panel-body + .card-body %p %strong Congratulations! Your pages are served under: diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml index 7d6c30b7f8d..4ada19a1368 100644 --- a/app/views/projects/pages/_destroy.haml +++ b/app/views/projects/pages/_destroy.haml @@ -1,9 +1,9 @@ - if @project.pages_deployed? - if can?(current_user, :remove_pages, @project) - .panel.panel-default.panel.panel-danger - .panel-heading Remove pages + .card.bg-danger + .card-header Remove pages .errors-holder - .panel-body + .card-body %p Removing the pages will prevent from exposing them to outside world. .form-actions diff --git a/app/views/projects/pages/_https_only.html.haml b/app/views/projects/pages/_https_only.html.haml index 6a3ffce949f..57345edb90b 100644 --- a/app/views/projects/pages/_https_only.html.haml +++ b/app/views/projects/pages/_https_only.html.haml @@ -1,5 +1,5 @@ = form_for @project, url: namespace_project_pages_path(@project.namespace.becomes(Namespace), @project), html: { class: 'inline', title: pages_https_only_title } do |f| - = f.check_box :pages_https_only, class: 'pull-left', disabled: pages_https_only_disabled? + = f.check_box :pages_https_only, class: 'float-left', disabled: pages_https_only_disabled? .prepend-left-20 = f.label :pages_https_only, class: pages_https_only_label_class do diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml index 27bbe52a714..986ca852411 100644 --- a/app/views/projects/pages/_list.html.haml +++ b/app/views/projects/pages/_list.html.haml @@ -1,8 +1,8 @@ - verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled? - if can?(current_user, :update_pages, @project) && @domains.any? - .panel.panel-default - .panel-heading + .card + .card-header Domains (#{@domains.count}) %ul.well-list.pages-domain-list{ class: ("has-verification-status" if verification_enabled) } - @domains.each do |domain| @@ -17,9 +17,9 @@ = icon('external-link') - if domain.subject %p - %span.label.label-gray Certificate: #{domain.subject} + %span.badge.badge-gray Certificate: #{domain.subject} - if domain.expired? - %span.label.label-danger Expired + %span.badge.badge-danger Expired %div = link_to 'Details', project_pages_domain_path(@project, domain), class: "btn btn-sm btn-grouped" = link_to 'Remove', project_pages_domain_path(@project, domain), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" diff --git a/app/views/projects/pages/_no_domains.html.haml b/app/views/projects/pages/_no_domains.html.haml index 7cea5f3e70b..8c93cf7a8ad 100644 --- a/app/views/projects/pages/_no_domains.html.haml +++ b/app/views/projects/pages/_no_domains.html.haml @@ -1,6 +1,6 @@ - if can?(current_user, :update_pages, @project) - .panel.panel-default - .panel-heading + .card + .card-header Domains .nothing-here-block Support for domains and certificates is disabled. diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml index e442e6e9a09..cd9177c0f9e 100644 --- a/app/views/projects/pages/_use.html.haml +++ b/app/views/projects/pages/_use.html.haml @@ -1,8 +1,8 @@ - unless @project.pages_deployed? - .panel.panel-info - .panel-heading + .card.bg-info + .card-header Configure pages - .panel-body + .card-body %p Learn how to upload your static site and have it served by GitLab by following the diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml index 6adaea799b2..7e1a3b9bea6 100644 --- a/app/views/projects/pages/show.html.haml +++ b/app/views/projects/pages/show.html.haml @@ -4,7 +4,7 @@ Pages - if can?(current_user, :update_pages, @project) && (Gitlab.config.pages.external_http || Gitlab.config.pages.external_https) - = link_to new_project_pages_domain_path(@project), class: 'btn btn-new pull-right', title: 'New Domain' do + = link_to new_project_pages_domain_path(@project), class: 'btn btn-new float-right', title: 'New Domain' do New Domain %p.light diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml index d81b07832bb..0d848f7899c 100644 --- a/app/views/projects/pages_domains/_form.html.haml +++ b/app/views/projects/pages_domains/_form.html.haml @@ -4,22 +4,22 @@ - @domain.errors.full_messages.each do |msg| %p= msg -.form-group - = f.label :domain, class: 'control-label' do +.form-group.row + = f.label :domain, class: 'col-form-label col-sm-2' do Domain .col-sm-10 = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control', disabled: @domain.persisted? - if Gitlab.config.pages.external_https - .form-group - = f.label :certificate, class: 'control-label' do + .form-group.row + = f.label :certificate, class: 'col-form-label col-sm-2' do Certificate (PEM) .col-sm-10 = f.text_area :certificate, rows: 5, class: 'form-control' %span.help-inline Upload a certificate for your domain with all intermediates - .form-group - = f.label :key, class: 'control-label' do + .form-group.row + = f.label :key, class: 'col-form-label col-sm-2' do Key (PEM) .col-sm-10 = f.text_area :key, rows: 5, class: 'form-control' diff --git a/app/views/projects/pages_domains/edit.html.haml b/app/views/projects/pages_domains/edit.html.haml index 6c404990492..ee70de22f13 100644 --- a/app/views/projects/pages_domains/edit.html.haml +++ b/app/views/projects/pages_domains/edit.html.haml @@ -5,7 +5,7 @@ = @domain.domain %hr.clearfix %div - = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f| + = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f| = render 'form', { f: f } .form-actions = f.submit 'Save Changes', class: "btn btn-save" diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml index 269df803a2b..376ce3f68aa 100644 --- a/app/views/projects/pages_domains/new.html.haml +++ b/app/views/projects/pages_domains/new.html.haml @@ -4,9 +4,9 @@ New Pages Domain %hr.clearfix %div - = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f| + = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f| = render 'form', { f: f } .form-actions = f.submit 'Create New Domain', class: "btn btn-save" - .pull-right + .float-right = link_to _('Cancel'), project_pages_path(@project), class: 'btn btn-cancel' diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml index 44d66f3b2d0..a8484187493 100644 --- a/app/views/projects/pages_domains/show.html.haml +++ b/app/views/projects/pages_domains/show.html.haml @@ -12,7 +12,7 @@ This domain is not verified. You will need to verify ownership before access is enabled. %h3.page-title.with-button - = link_to 'Edit', edit_project_pages_domain_path(@project, @domain), class: 'btn btn-success pull-right' + = link_to 'Edit', edit_project_pages_domain_path(@project, @domain), class: 'btn btn-success float-right' Pages Domain .table-holder @@ -30,9 +30,9 @@ %td .input-group = text_field_tag :domain_dns, dns_record , class: "monospace js-select-on-focus form-control", readonly: true - .input-group-btn - = clipboard_button(target: '#domain_dns', class: 'btn-default hidden-xs') - %p.help-block + .input-group-append + = clipboard_button(target: '#domain_dns', class: 'btn-default input-group-text d-none d-sm-block') + %p.form-text.text-muted To access this domain create a new DNS record - if verification_enabled @@ -43,16 +43,16 @@ %td = form_tag verify_project_pages_domain_path(@project, @domain) do .status-badge - - text, status = @domain.unverified? ? [_('Unverified'), 'label-danger'] : [_('Verified'), 'label-success'] - .label{ class: status } + - text, status = @domain.unverified? ? [_('Unverified'), 'badge-danger'] : [_('Verified'), 'badge-success'] + .badge{ class: status } = text %button.btn.has-tooltip{ type: "submit", data: { container: 'body' }, title: _("Retry verification") } = sprite_icon('redo') .input-group = text_field_tag :domain_verification, verification_record, class: "monospace js-select-on-focus form-control", readonly: true - .input-group-btn - = clipboard_button(target: '#domain_verification', class: 'btn-default hidden-xs') - %p.help-block + .input-group-append + = clipboard_button(target: '#domain_verification', class: 'btn-default d-none d-sm-block') + %p.form-text.text-muted - help_link = help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') To #{link_to 'verify ownership', help_link} of your domain, add the above key to a TXT record within to your DNS configuration. diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml index 160e325996a..1cdf981fcb4 100644 --- a/app/views/projects/pipeline_schedules/_form.html.haml +++ b/app/views/projects/pipeline_schedules/_form.html.haml @@ -1,24 +1,24 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "form-horizontal js-pipeline-schedule-form" } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "js-pipeline-schedule-form" } do |f| = form_errors(@schedule) - .form-group + .form-group.row .col-md-9 = f.label :description, _('Description'), class: 'label-light' = f.text_field :description, class: 'form-control', required: true, autofocus: true, placeholder: s_('PipelineSchedules|Provide a short description for this pipeline') - .form-group + .form-group.row .col-md-9 = f.label :cron, _('Interval Pattern'), class: 'label-light' #interval-pattern-input{ data: { initial_interval: @schedule.cron } } - .form-group + .form-group.row .col-md-9 = f.label :cron_timezone, _('Cron Timezone'), class: 'label-light' = dropdown_tag(_("Select a timezone"), options: { toggle_class: 'btn js-timezone-dropdown', title: _("Select a timezone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } ) = f.text_field :cron_timezone, value: @schedule.cron_timezone, id: 'schedule_cron_timezone', class: 'hidden', name: 'schedule[cron_timezone]', required: true - .form-group + .form-group.row .col-md-9 = f.label :ref, _('Target Branch'), class: 'label-light' = dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } ) = f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true - .form-group.js-ci-variable-list-section + .form-group.row.js-ci-variable-list-section .col-md-9 %label.label-light #{ s_('PipelineSchedules|Variables') } @@ -32,7 +32,7 @@ = n_('Hide value', 'Hide values', @schedule.variables.size) - else = n_('Reveal value', 'Reveal values', @schedule.variables.size) - .form-group + .form-group.row .col-md-9 = f.label :active, s_('PipelineSchedules|Activated'), class: 'label-light' %div diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml index 55d0e8bb7f9..8d88f0be083 100644 --- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml +++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml @@ -25,7 +25,7 @@ = link_to user_path(pipeline_schedule.owner) do = pipeline_schedule.owner&.name %td - .pull-right.btn-group + .float-right.btn-group - if can?(current_user, :play_pipeline_schedule, pipeline_schedule) = link_to play_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('Play'), class: 'btn' do = icon('play') diff --git a/app/views/projects/pipeline_schedules/_tabs.html.haml b/app/views/projects/pipeline_schedules/_tabs.html.haml index 8996c1b3e38..61f6ad34052 100644 --- a/app/views/projects/pipeline_schedules/_tabs.html.haml +++ b/app/views/projects/pipeline_schedules/_tabs.html.haml @@ -1,18 +1,18 @@ -%ul.nav-links.mobile-separator +%ul.nav-links.mobile-separator.nav.nav-tabs %li{ class: active_when(scope.nil?) }> = link_to schedule_path_proc.call(nil) do = s_("PipelineSchedules|All") - %span.badge.js-totalbuilds-count + %span.badge.badge-pill.js-totalbuilds-count = number_with_delimiter(all_schedules.count(:id)) %li{ class: active_when(scope == 'active') }> = link_to schedule_path_proc.call('active') do = s_("PipelineSchedules|Active") - %span.badge + %span.badge.badge-pill = number_with_delimiter(all_schedules.active.count(:id)) %li{ class: active_when(scope == 'inactive') }> = link_to schedule_path_proc.call('inactive') do = s_("PipelineSchedules|Inactive") - %span.badge + %span.badge.badge-pill = number_with_delimiter(all_schedules.inactive.count(:id)) diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml index bcb6dddba1a..3677666070e 100644 --- a/app/views/projects/pipeline_schedules/index.html.haml +++ b/app/views/projects/pipeline_schedules/index.html.haml @@ -18,5 +18,5 @@ %ul.content-list = render partial: "table" - else - .light-well + .card.bg-light .nothing-here-block= _("No schedules") diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 9db30042bf4..aa53fc3ea28 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -27,7 +27,7 @@ .icon-container.commit-icon = custom_icon("icon_commit") = link_to @commit.short_id, project_commit_path(@project, @pipeline.sha), class: "commit-sha js-details-short" - = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do + = link_to("#", class: "js-details-expand d-none d-sm-none d-md-inline") do %span.text-expander \... %span.js-details-content.hide diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 4dbf95be357..118391aac64 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -1,17 +1,17 @@ .tabs-holder - %ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator + %ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs %li.js-pipeline-tab-link = link_to project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do = _("Pipeline") %li.js-builds-tab-link = link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do = _("Jobs") - %span.badge.js-builds-counter= pipeline.total_size + %span.badge.badge-pill.js-builds-counter= pipeline.total_size - if @pipeline.failed_builds.present? %li.js-failures-tab-link = link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do = _("Failed Jobs") - %span.badge.js-failures-counter= @pipeline.failed_builds.count + %span.badge.badge-pill.js-failures-counter= @pipeline.failed_builds.count .tab-content #js-tab-pipeline.tab-pane diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml index 81984ee94b0..d1e8e9d0d60 100644 --- a/app/views/projects/pipelines/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -6,9 +6,9 @@ = s_("Pipeline|Run Pipeline") %hr -= form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do |f| += form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "js-new-pipeline-form js-requires-input" } do |f| = form_errors(@pipeline) - .form-group + .form-group.row .col-sm-12 = f.label :ref, s_('Pipeline|Create for') = hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch @@ -16,7 +16,7 @@ options: { toggle_class: 'js-branch-select wide git-revision-dropdown-toggle', filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: s_("Pipeline|Search branches"), data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } }) - .help-block + .form-text.text-muted = s_("Pipeline|Existing branch name or tag") .col-sm-12.prepend-top-10.js-ci-variable-list-section @@ -24,7 +24,7 @@ = s_('Pipeline|Variables') %ul.ci-variable-list = render 'ci/variables/variable_row', form_field: 'pipeline', only_key_value: true - .help-block + .form-text.text-muted = (s_("Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default.") % {settings_link: settings_link}).html_safe .form-actions diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml index fdeb5f21fbe..128f52ff648 100644 --- a/app/views/projects/project_members/_groups.html.haml +++ b/app/views/projects/project_members/_groups.html.haml @@ -1,7 +1,7 @@ -.panel.panel-default.project-members-groups - .panel-heading +.card.project-members-groups + .card-header Groups with access to %strong= @project.name - %span.badge= group_links.size + %span.badge.badge-pill= group_links.size %ul.content-list = render partial: 'shared/members/group', collection: group_links, as: :group_link diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index bf5b11ea30c..5064db8d5cd 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -9,7 +9,7 @@ .select-wrapper = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select select-control" = icon('chevron-down') - .help-block.append-bottom-10 + .form-text.text-muted.append-bottom-10 = link_to "Read more", help_page_path("user/permissions"), class: "vlink" about role permissions .form-group diff --git a/app/views/projects/project_members/_new_shared_group.html.haml b/app/views/projects/project_members/_new_shared_group.html.haml index c10ef648a8f..684219735e2 100644 --- a/app/views/projects/project_members/_new_shared_group.html.haml +++ b/app/views/projects/project_members/_new_shared_group.html.haml @@ -9,7 +9,7 @@ .select-wrapper = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control" = icon('chevron-down') - .help-block.append-bottom-10 + .form-text.text-muted.append-bottom-10 = link_to "Read more", help_page_path("user/permissions"), class: "vlink" about role permissions .form-group diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index 16bcf671c25..a30870a241c 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -1,12 +1,12 @@ - project = local_assigns.fetch(:project) - members = local_assigns.fetch(:members) -.panel.panel-default - .panel-heading.flex-project-members-panel +.card + .card-header.flex-project-members-panel %span.flex-project-title Members of %strong= project.name - %span.badge= members.total_count + %span.badge.badge-pill= members.total_count = form_tag project_project_members_path(project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do .form-group = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml index 755128af565..6a52e72bfd8 100644 --- a/app/views/projects/project_members/import.html.haml +++ b/app/views/projects/project_members/import.html.haml @@ -5,9 +5,9 @@ %p.light Only project members will be imported. Group members will be skipped. %hr -= form_tag apply_import_project_project_members_path(@project), method: 'post', class: 'form-horizontal' do - .form-group - = label_tag :source_project_id, "Project", class: 'control-label' += form_tag apply_import_project_project_members_path(@project), method: 'post' do + .form-group.row + = label_tag :source_project_id, "Project", class: 'col-form-label col-sm-2' .col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(@projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true) .form-actions diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index d81103c3a92..a56023e98cd 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -17,7 +17,7 @@ %i Owners .light - if can?(current_user, :admin_project_member, @project) - %ul.nav-links.gitlab-tabs{ role: 'tablist' } + %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' } %li.active{ role: 'presentation' } %a{ href: '#add-member-pane', id: 'add-member-tab', data: { toggle: 'tab' }, role: 'tab' } Add member - if @project.allowed_to_share_with_group? diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml index d1ed438eb21..a2cd7752fc4 100644 --- a/app/views/projects/protected_branches/shared/_branches_list.html.haml +++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml @@ -1,7 +1,7 @@ .protected-branches-list.js-protected-branches-list.qa-protected-branches-list - if @protected_branches.empty? - .panel-heading - %h3.panel-title + .card-header + %h3.card-title Protected branch (#{@protected_branches_count}) %p.settings-message.text-center There are currently no protected branches, protect a branch with the form above. diff --git a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml index 9f0c4f3b3a8..c2d6c034e35 100644 --- a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml +++ b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml @@ -1,33 +1,32 @@ = form_for [@project.namespace.becomes(Namespace), @project, @protected_branch], html: { class: 'new-protected-branch js-new-protected-branch' } do |f| - .panel.panel-default - .panel-heading - %h3.panel-title + .card + .card-header + %h3.card-title Protect a branch - .panel-body - .form-horizontal - = form_errors(@protected_branch) - .form-group - = f.label :name, class: 'col-md-2 text-right' do - Branch: - .col-md-10 - = render partial: "projects/protected_branches/shared/dropdown", locals: { f: f } - .help-block - = link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches') - such as - %code *-stable - or - %code production/* - are supported - .form-group - %label.col-md-2.text-right{ for: 'merge_access_levels_attributes' } - Allowed to merge: - .col-md-10 - = yield :merge_access_levels - .form-group - %label.col-md-2.text-right{ for: 'push_access_levels_attributes' } - Allowed to push: - .col-md-10 - = yield :push_access_levels + .card-body + = form_errors(@protected_branch) + .form-group.row + = f.label :name, class: 'col-md-2 text-right' do + Branch: + .col-md-10 + = render partial: "projects/protected_branches/shared/dropdown", locals: { f: f } + .form-text.text-muted + = link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches') + such as + %code *-stable + or + %code production/* + are supported + .form-group.row + %label.col-md-2.text-right{ for: 'merge_access_levels_attributes' } + Allowed to merge: + .col-md-10 + = yield :merge_access_levels + .form-group.row + %label.col-md-2.text-right{ for: 'push_access_levels_attributes' } + Allowed to push: + .col-md-10 + = yield :push_access_levels - .panel-footer + .card-footer = f.submit 'Protect', class: 'btn-create btn', disabled: true diff --git a/app/views/projects/protected_branches/shared/_matching_branch.html.haml b/app/views/projects/protected_branches/shared/_matching_branch.html.haml index 98793d632e6..2c76bf87945 100644 --- a/app/views/projects/protected_branches/shared/_matching_branch.html.haml +++ b/app/views/projects/protected_branches/shared/_matching_branch.html.haml @@ -3,7 +3,7 @@ = link_to matching_branch.name, project_ref_path(@project, matching_branch.name), class: 'ref-name' - if @project.root_ref?(matching_branch.name) - %span.label.label-info.prepend-left-5 default + %span.badge.badge-info.prepend-left-5 default %td - commit = @project.commit(matching_branch.name) = link_to(commit.short_id, project_commit_path(@project, commit.id), class: 'commit-sha') diff --git a/app/views/projects/protected_branches/shared/_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_protected_branch.html.haml index 2d3b2af00c2..82ef08272d3 100644 --- a/app/views/projects/protected_branches/shared/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/shared/_protected_branch.html.haml @@ -5,7 +5,7 @@ %span.ref-name.qa-protected-branch-name= protected_branch.name - if @project.root_ref?(protected_branch.name) - %span.label.label-info.prepend-left-5 default + %span.badge.badge-info.prepend-left-5 default %td - if protected_branch.wildcard? - matching_branches = protected_branch.matching(repository.branches) diff --git a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml index 5a53c704fcb..0ae5ca3ff36 100644 --- a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml +++ b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml @@ -1,29 +1,28 @@ = form_for [@project.namespace.becomes(Namespace), @project, @protected_tag], html: { class: 'new-protected-tag js-new-protected-tag' } do |f| - .panel.panel-default - .panel-heading - %h3.panel-title + .card + .card-header + %h3.card-title Protect a tag - .panel-body - .form-horizontal - = form_errors(@protected_tag) - .form-group - = f.label :name, class: 'col-md-2 text-right' do - Tag: - .col-md-10.protected-tags-dropdown - = render partial: "projects/protected_tags/shared/dropdown", locals: { f: f } - .help-block - = link_to 'Wildcards', help_page_path('user/project/protected_tags', anchor: 'wildcard-protected-tags') - such as - %code v* - or - %code *-release - are supported - .form-group - %label.col-md-2.text-right{ for: 'create_access_levels_attributes' } - Allowed to create: - .col-md-10 - .create_access_levels-container - = yield :create_access_levels + .card-body + = form_errors(@protected_tag) + .form-group.row + = f.label :name, class: 'col-md-2 text-right' do + Tag: + .col-md-10.protected-tags-dropdown + = render partial: "projects/protected_tags/shared/dropdown", locals: { f: f } + .form-text.text-muted + = link_to 'Wildcards', help_page_path('user/project/protected_tags', anchor: 'wildcard-protected-tags') + such as + %code v* + or + %code *-release + are supported + .form-group.row + %label.col-md-2.text-right{ for: 'create_access_levels_attributes' } + Allowed to create: + .col-md-10 + .create_access_levels-container + = yield :create_access_levels - .panel-footer + .card-footer = f.submit 'Protect', class: 'btn-create btn', disabled: true diff --git a/app/views/projects/protected_tags/shared/_matching_tag.html.haml b/app/views/projects/protected_tags/shared/_matching_tag.html.haml index 05f102d1ca3..133c76cd2ad 100644 --- a/app/views/projects/protected_tags/shared/_matching_tag.html.haml +++ b/app/views/projects/protected_tags/shared/_matching_tag.html.haml @@ -3,7 +3,7 @@ = link_to matching_tag.name, project_ref_path(@project, matching_tag.name), class: 'ref-name' - if @project.root_ref?(matching_tag.name) - %span.label.label-info.prepend-left-5 default + %span.badge.badge-info.prepend-left-5 default %td - commit = @project.commit(matching_tag.name) = link_to(commit.short_id, project_commit_path(@project, commit.id), class: 'commit-sha') diff --git a/app/views/projects/protected_tags/shared/_protected_tag.html.haml b/app/views/projects/protected_tags/shared/_protected_tag.html.haml index c778f7b9781..1702e38df7e 100644 --- a/app/views/projects/protected_tags/shared/_protected_tag.html.haml +++ b/app/views/projects/protected_tags/shared/_protected_tag.html.haml @@ -3,7 +3,7 @@ %span.ref-name= protected_tag.name - if @project.root_ref?(protected_tag.name) - %span.label.label-info.prepend-left-5 default + %span.badge.badge-info.prepend-left-5 default %td - if protected_tag.wildcard? - matching_tags = protected_tag.matching(repository.tags) diff --git a/app/views/projects/protected_tags/shared/_tags_list.html.haml b/app/views/projects/protected_tags/shared/_tags_list.html.haml index 3ed82e51dbe..c3081d75fb4 100644 --- a/app/views/projects/protected_tags/shared/_tags_list.html.haml +++ b/app/views/projects/protected_tags/shared/_tags_list.html.haml @@ -1,7 +1,7 @@ .protected-tags-list.js-protected-tags-list - if @protected_tags.empty? - .panel-heading - %h3.panel-title + .card-header + %h3.card-title Protected tag (#{@protected_tags_count}) %p.settings-message.text-center There are currently no protected tags, protect a tag with the form above. diff --git a/app/views/projects/registry/repositories/_tag.html.haml b/app/views/projects/registry/repositories/_tag.html.haml index 0223372bff8..a4cde53e8c6 100644 --- a/app/views/projects/registry/repositories/_tag.html.haml +++ b/app/views/projects/registry/repositories/_tag.html.haml @@ -24,7 +24,7 @@ \- - if can?(current_user, :update_container_image, @project) %td.content - .controls.hidden-xs.pull-right + .controls.d-none.d-sm-block.float-right = link_to project_registry_repository_tag_path(@project, tag.repository, tag.name), method: :delete, class: 'btn btn-remove has-tooltip', diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index 76f57320f99..0426f2215ad 100644 --- a/app/views/projects/registry/repositories/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -16,11 +16,11 @@ .row.prepend-top-10 .col-lg-12 - .panel.panel-default - .panel-heading - %h4.panel-title + .card + .card-header + %h4.card-title = s_('ContainerRegistry|How to use the Container Registry') - .panel-body + .card-body %p - link_token = link_to(_('personal access token'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank') - link_2fa = link_to(_('2FA enabled'), help_page_path('user/profile/account/two_factor_authentication'), target: '_blank') @@ -30,7 +30,7 @@ %br %p - deploy_token = link_to(_('deploy token'), help_page_path('user/project/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank') - = s_('ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token } + = s_('ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token } %br %p = s_('ContainerRegistry|Once you log in, you’re free to create and upload a container image using the common %{build} and %{push} commands').html_safe % { build: "<code>build</code>".html_safe, push: "<code>push</code>".html_safe } diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index 4d962f9433f..b4787032966 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -11,7 +11,7 @@ %strong= @tag.name - = form_for(@release, method: :put, url: project_tag_release_path(@project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f| + = form_for(@release, method: :put, url: project_tag_release_path(@project, @tag.name), html: { class: 'common-note-form release-form js-quick-submit' }) do |f| = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." = render 'shared/notes/hints' diff --git a/app/views/projects/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml index 87895a15239..ae0d9ab9908 100644 --- a/app/views/projects/repositories/_feed.html.haml +++ b/app/views/projects/repositories/_feed.html.haml @@ -14,5 +14,5 @@ = image_tag avatar_icon_for_email(commit.author_email), class: "", width: 16, alt: '' = markdown(truncate(commit.title, length: 40), pipeline: :single_line, author: commit.author) %td - %span.pull-right.cgray + %span.float-right.cgray = time_ago_with_tooltip(commit.committed_date) diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml index 69218f344f7..a23f5d6f0c3 100644 --- a/app/views/projects/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -15,7 +15,7 @@ %span.commit-sha = runner.short_sha - .pull-right + .float-right - if @project_runners.include?(runner) - if runner.active? = link_to _('Pause'), pause_project_runner_path(@project, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: _("Are you sure?") } @@ -30,7 +30,7 @@ = form_for [@project.namespace.becomes(Namespace), @project, @project.runner_projects.new] do |f| = f.hidden_field :runner_id, value: runner.id = f.submit _('Enable for this project'), class: 'btn btn-sm' - .pull-right + .float-right %small.light \##{runner.id} - if runner.description.present? @@ -39,5 +39,5 @@ - if runner.tag_list.present? %p - runner.tag_list.sort.each do |tag| - %span.label.label-primary + %span.badge.badge-primary = tag diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 684b082efbb..aa30ebdc3b8 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -9,7 +9,7 @@ - if @service.respond_to?(:detailed_description) %p= @service.detailed_description .col-lg-9 - = form_for(@service, as: :service, url: project_service_path(@project, @service.to_param), method: :put, html: { class: 'gl-show-field-errors form-horizontal integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form| + = form_for(@service, as: :service, url: project_service_path(@project, @service.to_param), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form| = render 'shared/service_settings', form: form, subject: @service - if @service.editable? .footer-block.row-content-block diff --git a/app/views/projects/services/_index.html.haml b/app/views/projects/services/_index.html.haml index dac7d4d1bbb..acbab8b85c9 100644 --- a/app/views/projects/services/_index.html.haml +++ b/app/views/projects/services/_index.html.haml @@ -8,13 +8,13 @@ %colgroup %col %col - %col.hidden-xs + %col.d-none.d-sm-block %col{ width: "120" } %thead %tr %th %th Service - %th.hidden-xs Description + %th.d-none.d-sm-block Description %th Last edit - @services.sort_by(&:title).each do |service| %tr @@ -23,7 +23,7 @@ %td = link_to edit_project_service_path(@project, service.to_param) do %strong= service.title - %td.hidden-xs + %td.d-none.d-sm-block = service.description %td.light - if service.updated_at.present? diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml index 2ab0227126a..209b9c71390 100644 --- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml @@ -18,22 +18,22 @@ .help-form .form-group - = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :display_name, "GitLab / #{@project.full_name}", class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(target: '#display_name') + = label_tag :display_name, 'Display name', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :display_name, "GitLab / #{@project.full_name}", class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#display_name', class: 'input-group-text') .form-group - = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(target: '#description') + = label_tag :description, 'Description', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#description', class: 'input-group-text') .form-group - = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block + = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.text-block %p Fill in the word that works best for your team. %p Suggestions: @@ -42,47 +42,47 @@ %code= @project.full_path .form-group - = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(target: '#request_url') + = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#request_url', class: 'input-group-text') .form-group - = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block POST + = label_tag nil, 'Request method', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.text-block POST .form-group - = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(target: '#response_username') + = label_tag :response_username, 'Response username', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :response_username, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#response_username', class: 'input-group-text') .form-group - = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(target: '#response_icon') + = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#response_icon', class: 'input-group-text') .form-group - = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block Yes + = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.text-block Yes .form-group - = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(target: '#autocomplete_hint') + = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :autocomplete_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#autocomplete_hint', class: 'input-group-text') .form-group - = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(target: '#autocomplete_description') + = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#autocomplete_description', class: 'input-group-text') %hr diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml index 2a1b9d4c465..62bef77be97 100644 --- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml @@ -1,6 +1,6 @@ - enabled = Gitlab.config.mattermost.enabled -.well +.card %p This service allows users to perform common operations on this project by entering slash commands in Mattermost. diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml index 44c0b7a90dc..2da8e5428ec 100644 --- a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml @@ -1,7 +1,7 @@ .services-installation-info - unless @service.activated? .row - .col-sm-9.col-sm-offset-3 + .col-sm-9.offset-sm-3 = link_to new_project_mattermost_path(@project), class: 'btn btn-lg' do = custom_icon('mattermost_logo', size: 15) Add to Mattermost diff --git a/app/views/projects/services/prometheus/_help.html.haml b/app/views/projects/services/prometheus/_help.html.haml index 88acb824ba7..15e7362c2ba 100644 --- a/app/views/projects/services/prometheus/_help.html.haml +++ b/app/views/projects/services/prometheus/_help.html.haml @@ -5,5 +5,5 @@ = s_('PrometheusService|Manual configuration') - unless @service.editable? - .well + .card = s_('PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters') diff --git a/app/views/projects/services/prometheus/_show.html.haml b/app/views/projects/services/prometheus/_show.html.haml index 43e6a173108..bda597cc02b 100644 --- a/app/views/projects/services/prometheus/_show.html.haml +++ b/app/views/projects/services/prometheus/_show.html.haml @@ -7,12 +7,12 @@ = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus') .col-lg-9 - .panel.panel-default.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(@project, :json), metrics_help_path: help_page_path('user/project/integrations/prometheus_library/metrics') } } - .panel-heading - %h3.panel-title + .card.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(@project, :json), metrics_help_path: help_page_path('user/project/integrations/prometheus_library/metrics') } } + .card-header + %h3.card-title = s_('PrometheusService|Common metrics') - %span.badge.js-monitored-count 0 - .panel-body + %span.badge.badge-pill.js-monitored-count 0 + .card-body .loading-metrics.js-loading-metrics %p.prepend-top-10.prepend-left-10 = icon('spinner spin', class: 'metrics-load-spinner') @@ -22,13 +22,13 @@ = s_('PrometheusService|Waiting for your first deployment to an environment to find common metrics') %ul.list-unstyled.metrics-list.hidden.js-metrics-list - .panel.panel-default.hidden.js-panel-missing-env-vars - .panel-heading - %h3.panel-title + .card.hidden.js-panel-missing-env-vars + .card-header + %h3.card-title = icon('caret-right lg fw', class: 'panel-toggle js-panel-toggle', 'aria-label' => 'Toggle panel') = s_('PrometheusService|Missing environment variable') - %span.badge.js-env-var-count 0 - .panel-body.hidden + %span.badge.badge-pill.js-env-var-count 0 + .card-body.hidden .flash-container .flash-notice .flash-text diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml index d592a5e4663..44f58ad05e5 100644 --- a/app/views/projects/services/slack_slash_commands/_help.html.haml +++ b/app/views/projects/services/slack_slash_commands/_help.html.haml @@ -1,7 +1,7 @@ - pretty_name = defined?(@project) ? @project.full_name : 'namespace / path' - run_actions_text = "Perform common operations on GitLab project: #{pretty_name}" -.well +.card %p This service allows users to perform common operations on this project by entering slash commands in Slack. @@ -26,8 +26,8 @@ .help-form .form-group - = label_tag nil, 'Command', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block + = label_tag nil, 'Command', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.text-block %p Fill in the word that works best for your team. %p Suggestions: @@ -36,53 +36,53 @@ %code= @project.full_path .form-group - = label_tag :url, 'URL', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(target: '#url') + = label_tag :url, 'URL', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#url', class: 'input-group-text') .form-group - = label_tag nil, 'Method', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block POST + = label_tag nil, 'Method', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.text-block POST .form-group - = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :customize_name, 'GitLab', class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(target: '#customize_name') + = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#customize_name', class: 'input-group-text') .form-group - = label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block + = label_tag nil, 'Customize icon', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.text-block = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36) = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer') .form-group - = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block Show this command in the autocomplete list + = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.text-block Show this command in the autocomplete list .form-group - = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(target: '#autocomplete_description') + = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#autocomplete_description', class: 'input-group-text') .form-group - = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(target: '#autocomplete_usage_hint') + = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text') .form-group - = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(target: '#descriptive_label') + = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#descriptive_label', class: 'input-group-text') %hr diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml index 71e77dae69e..988bcfb5265 100644 --- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml +++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml @@ -10,21 +10,21 @@ %p.settings-message.text-center = message.html_safe = f.fields_for :auto_devops_attributes, @auto_devops do |form| - .radio + .form-check = form.label :enabled_true do = form.radio_button :enabled, 'true' %strong= s_('CICD|Enable Auto DevOps') %br = s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted } - .radio + .form-check = form.label :enabled_false do = form.radio_button :enabled, 'false' %strong= s_('CICD|Disable Auto DevOps') %br = s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted } - .radio + .form-check = form.label :enabled_ do = form.radio_button :enabled, '' %strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" } @@ -35,7 +35,9 @@ = _('Domain') = form.text_field :domain, class: 'form-control', placeholder: 'domain.com' .help-block - = s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.') + = s_('CICD|A domain is required to use Auto Review Apps and Auto Deploy Stages.') + - if cluster_ingress_ip = cluster_ingress_ip(@project) + = s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe } = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank' = f.submit 'Save changes', class: "btn btn-success prepend-top-15" diff --git a/app/views/projects/settings/ci_cd/_badge.html.haml b/app/views/projects/settings/ci_cd/_badge.html.haml index e8028059487..d14360913a4 100644 --- a/app/views/projects/settings/ci_cd/_badge.html.haml +++ b/app/views/projects/settings/ci_cd/_badge.html.haml @@ -2,15 +2,15 @@ .col-lg-12 %h4 = badge.title.capitalize - .panel.panel-default - .panel-heading + .card + .card-header %b = badge.title.capitalize · = badge.to_html - .pull-right + .float-right = render 'shared/ref_switcher', destination: 'badges', align_right: true - .panel-body + .card-body .row .col-md-2.text-center Markdown diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index 80c226ad273..50175f5258c 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -7,8 +7,8 @@ = f.label :runners_token, "Runner token", class: 'label-light' .form-control.js-secret-value-placeholder = '*' * 20 - = f.text_field :runners_token, class: "form-control hide js-secret-value", placeholder: 'xEeFCaDAB89' - %p.help-block The secure token used by the Runner to checkout the project + = f.text_field :runners_token, class: "form-control hidden js-secret-value", placeholder: 'xEeFCaDAB89' + %p.form-text.text-muted The secure token used by the Runner to checkout the project %button.btn.btn-info.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: 'false' } } = _('Reveal value') @@ -19,14 +19,14 @@ %p Choose between <code>clone</code> or <code>fetch</code> to get the recent application code = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy'), target: '_blank' - .radio + .form-check = f.label :build_allow_git_fetch_false do = f.radio_button :build_allow_git_fetch, 'false' %strong git clone %br %span.descr Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job - .radio + .form-check = f.label :build_allow_git_fetch_true do = f.radio_button :build_allow_git_fetch, 'true' %strong git fetch @@ -38,7 +38,7 @@ .form-group = f.label :build_timeout_human_readable, 'Timeout', class: 'label-light' = f.text_field :build_timeout_human_readable, class: 'form-control' - %p.help-block + %p.form-text.text-muted Per job. If a job passes this threshold, it will be marked as failed = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank' @@ -46,17 +46,17 @@ .form-group = f.label :ci_config_path, 'Custom CI config path', class: 'label-light' = f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml' - %p.help-block + %p.form-text.text-muted The path to CI config file. Defaults to <code>.gitlab-ci.yml</code> = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank' %hr .form-group - .checkbox + .form-check = f.label :public_builds do = f.check_box :public_builds %strong Public pipelines - .help-block + .form-text.text-muted Allow public access to pipelines and job details, including output logs and artifacts = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines'), target: '_blank' .bs-callout.bs-callout-info @@ -74,11 +74,11 @@ %hr .form-group - .checkbox + .form-check = f.label :auto_cancel_pending_pipelines do = f.check_box :auto_cancel_pending_pipelines, {}, 'enabled', 'disabled' %strong Auto-cancel redundant, pending pipelines - .help-block + .form-text.text-muted New pipelines will cancel older, pending pipelines on the same branch = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'auto-cancel-pending-pipelines'), target: '_blank' @@ -86,10 +86,12 @@ .form-group = f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light' .input-group - %span.input-group-addon / + %span.input-group-prepend + .input-group-text / = f.text_field :build_coverage_regex, class: 'form-control', placeholder: 'Regular expression' - %span.input-group-addon / - %p.help-block + %span.input-group-append + .input-group-text / + %p.form-text.text-muted A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing'), target: '_blank' diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index 5f596a019f7..7d8dd58e7e0 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -19,7 +19,7 @@ %section.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 - = s_('CICD|Auto DevOps (Beta)') + = s_('CICD|Auto DevOps') %button.btn.btn-default.js-settings-toggle{ type: 'button' } = expanded ? _('Collapse') : _('Expand') %p diff --git a/app/views/projects/settings/integrations/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml index cd003107d66..77d88aed883 100644 --- a/app/views/projects/settings/integrations/_project_hook.html.haml +++ b/app/views/projects/settings/integrations/_project_hook.html.haml @@ -5,7 +5,7 @@ %div - ProjectHook.triggers.each_value do |event| - if hook.public_send(event) - %span.label.label-gray.deploy-project-label= event.to_s.titleize + %span.badge.badge-gray.deploy-project-label= event.to_s.titleize .col-md-4.col-lg-5.text-right-lg.prepend-top-5 %span.append-right-10.inline SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'} diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index f09871c7fcc..b3a9fa9dd91 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -1,6 +1,6 @@ - return unless current_user -.hidden-xs +.d-none.d-sm-block - if can?(current_user, :update_project_snippet, @snippet) = link_to edit_project_snippet_path(@project, @snippet), class: "btn btn-grouped" do Edit @@ -13,7 +13,7 @@ - if @snippet.submittable_as_spam_by?(current_user) = link_to 'Submit as spam', mark_as_spam_project_snippet_path(@project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) - .visible-xs-block.dropdown + .d-block.d-sm-none.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } Options = icon('caret-down') diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 98b4d6339da..f55202c2c5f 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -6,7 +6,7 @@ = link_to tag.name, project_tag_path(@project, tag.name), class: 'item-title ref-name prepend-left-4' - if protected_tag?(@project, tag) - %span.label.label-success.prepend-left-4 + %span.badge.badge-success.prepend-left-4 = s_('TagsPage|protected') - if tag.message.present? diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 10415d011d6..96ecac815c0 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -16,7 +16,7 @@ %span.light = tags_sort_options_hash[@sort] = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable + %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable %li.dropdown-header = s_('TagsPage|Sort by') - tags_sort_options_hash.each do |value, title| diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 1827a3d323c..b596748ca5f 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -10,35 +10,35 @@ = s_('TagsPage|New Tag') %hr -= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal common-note-form tag-form js-quick-submit js-requires-input" do - .form-group - = label_tag :tag_name, nil, class: 'control-label' += form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "common-note-form tag-form js-quick-submit js-requires-input" do + .form-group.row + = label_tag :tag_name, nil, class: 'col-form-label col-sm-2' .col-sm-10 = text_field_tag :tag_name, params[:tag_name], required: true, autofocus: true, class: 'form-control' - .form-group - = label_tag :ref, 'Create from', class: 'control-label' + .form-group.row + = label_tag :ref, 'Create from', class: 'col-form-label col-sm-2' .col-sm-10.create-from .dropdown = hidden_field_tag :ref, default_ref = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide js-branch-select', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do .text-left.dropdown-toggle-text= default_ref = render 'shared/ref_dropdown', dropdown_class: 'wide' - .help-block + .form-text.text-muted = s_('TagsPage|Existing branch name, tag, or commit SHA') - .form-group - = label_tag :message, nil, class: 'control-label' + .form-group.row + = label_tag :message, nil, class: 'col-form-label col-sm-2' .col-sm-10 = text_area_tag :message, @message, required: false, class: 'form-control', rows: 5 - .help-block + .form-text.text-muted = s_('TagsPage|Optionally, add a message to the tag.') %hr - .form-group - = label_tag :release_description, s_('TagsPage|Release notes'), class: 'control-label' + .form-group.row + = label_tag :release_description, s_('TagsPage|Release notes'), class: 'col-form-label col-sm-2' .col-sm-10 = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: s_('TagsPage|Write your release notes or drag files here...'), current_text: @release_description = render 'shared/notes/hints' - .help-block + .form-text.text-muted = s_('TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.') .form-actions = button_tag s_('TagsPage|Create tag'), class: 'btn btn-create' diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 7a3469cdd26..15a960f81b8 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -11,7 +11,7 @@ = icon('tag') = @tag.name - if protected_tag?(@project, @tag) - %span.label.label-success + %span.badge.badge-success = s_('TagsPage|protected') - if @commit = render 'projects/branches/commit', commit: @commit, project: @project diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml index 8c1c532cb3e..f79f3af36d4 100644 --- a/app/views/projects/tree/_blob_item.html.haml +++ b/app/views/projects/tree/_blob_item.html.haml @@ -6,7 +6,7 @@ = link_to project_blob_path(@project, tree_join(@id || @commit.id, blob_item.name)), class: 'str-truncated', title: file_name do %span= file_name - if is_lfs_blob - %span.label.label-lfs.prepend-left-5 LFS - %td.hidden-xs.tree-commit + %span.badge.label-lfs.prepend-left-5 LFS + %td.d-none.d-sm-table-cell.tree-commit %td.tree-time-ago.cgray.text-right = render 'projects/tree/spinner' diff --git a/app/views/projects/tree/_submodule_item.html.haml b/app/views/projects/tree/_submodule_item.html.haml index 04d52361db0..e563c8c4036 100644 --- a/app/views/projects/tree/_submodule_item.html.haml +++ b/app/views/projects/tree/_submodule_item.html.haml @@ -3,4 +3,4 @@ %i.fa.fa-archive.fa-fw = submodule_link(submodule_item, @ref) %td - %td.hidden-xs + %td.d-none.d-sm-table-cell diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index 6ea78851b8d..587aeafa82f 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -4,15 +4,15 @@ %thead %tr %th= s_('ProjectFileTree|Name') - %th.hidden-xs - .pull-left= _('Last commit') + %th.d-none.d-sm-table-cell + .float-left= _('Last commit') %th.text-right= _('Last update') - if @path.present? %tr.tree-item %td.tree-item-file-name = link_to "..", project_tree_path(@project, up_dir_path), class: 'prepend-left-10' %td - %td.hidden-xs + %td.d-none.d-sm-table-cell = render_tree(tree) diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 8587d3b0c0d..4fc1a284693 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -11,18 +11,18 @@ - addtotree_toggle_attributes = { title: _("You can only add files when you are on a branch"), data: { container: 'body' }, class: 'disabled has-tooltip' } %ul.breadcrumb.repo-breadcrumb - %li + %li.breadcrumb-item = link_to project_tree_path(@project, @ref) do = @project.path - path_breadcrumbs do |title, path| - %li + %li.breadcrumb-item = link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path)) - if can_collaborate || can_create_mr_from_fork - %li + %li.breadcrumb-item %a.btn.add-to-tree{ addtotree_toggle_attributes } - = sprite_icon('plus', size: 16, css_class: 'pull-left') - = sprite_icon('arrow-down', size: 16, css_class: 'pull-left') + = sprite_icon('plus', size: 16, css_class: 'float-left') + = sprite_icon('arrow-down', size: 16, css_class: 'float-left') - if on_top_of_branch? .add-to-tree-dropdown %ul.dropdown-menu @@ -82,7 +82,7 @@ - if can_collaborate = succeed " " do - = link_to ide_edit_path(@project, @id, ""), class: 'btn btn-default' do + = link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default' do = _('Web IDE') = render 'projects/buttons/download', project: @project, ref: @ref diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml index af3816fc9f4..ce0cd95b468 100644 --- a/app/views/projects/tree/_tree_item.html.haml +++ b/app/views/projects/tree/_tree_item.html.haml @@ -4,6 +4,6 @@ - path = flatten_tree(@path, tree_item) = link_to project_tree_path(@project, tree_join(@id || @commit.id, path)), class: 'str-truncated', title: path do %span= path - %td.hidden-xs.tree-commit + %td.d-none.d-sm-table-cell.tree-commit %td.tree-time-ago.text-right = render 'projects/tree/spinner' diff --git a/app/views/projects/triggers/_content.html.haml b/app/views/projects/triggers/_content.html.haml index 6c2d603d95d..96a41aa066c 100644 --- a/app/views/projects/triggers/_content.html.haml +++ b/app/views/projects/triggers/_content.html.haml @@ -1,6 +1,6 @@ %p.append-bottom-default Triggers with the - %span.label.label-primary legacy + %span.badge.badge-primary legacy label do not have an associated user and only have access to the current project. %br = succeed '.' do diff --git a/app/views/projects/triggers/_form.html.haml b/app/views/projects/triggers/_form.html.haml index 5f708b3a2ed..3539aea3580 100644 --- a/app/views/projects/triggers/_form.html.haml +++ b/app/views/projects/triggers/_form.html.haml @@ -4,7 +4,7 @@ - if @trigger.token .form-group %label.label-light Token - %p.form-control-static= @trigger.token + %p.form-control-plaintext= @trigger.token .form-group = f.label :key, "Description", class: "label-light" = f.text_field :description, class: "form-control", required: true, title: 'Trigger description is required.', placeholder: "Trigger description" diff --git a/app/views/projects/triggers/_index.html.haml b/app/views/projects/triggers/_index.html.haml index 0f655e4ed83..a15bb4c4f3f 100644 --- a/app/views/projects/triggers/_index.html.haml +++ b/app/views/projects/triggers/_index.html.haml @@ -1,11 +1,11 @@ .row.prepend-top-default.append-bottom-default.triggers-container .col-lg-12 = render "projects/triggers/content" - .panel.panel-default - .panel-heading - %h4.panel-title + .card + .card-header + %h4.card-title Manage your project's triggers - .panel-body + .card-body = render "projects/triggers/form", btn_text: "Add trigger" %hr - if @triggers.any? @@ -26,7 +26,7 @@ %p.settings-message.text-center.append-bottom-default No triggers have been created yet. Add one using the form above. - .panel-footer + .card-footer %p In the following examples, you can see the exact API call you need to diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml index 9201680f119..7e4618e1a88 100644 --- a/app/views/projects/triggers/_trigger.html.haml +++ b/app/views/projects/triggers/_trigger.html.haml @@ -8,9 +8,9 @@ .label-container - if trigger.legacy? - %span.label.label-primary.has-tooltip{ title: "Trigger makes use of deprecated functionality" } legacy + %span.badge.badge-primary.has-tooltip{ title: "Trigger makes use of deprecated functionality" } legacy - if !trigger.can_access_project? - %span.label.label-danger.has-tooltip{ title: "Trigger user has insufficient permissions to project" } invalid + %span.badge.badge-danger.has-tooltip{ title: "Trigger user has insufficient permissions to project" } invalid %td - if trigger.description? && trigger.description.length > 15 diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index d285251d06f..bcceb69954a 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -1,13 +1,13 @@ - commit_message = @page.persisted? ? s_("WikiPageEdit|Update %{page_title}") : s_("WikiPageCreate|Create %{page_title}") - commit_message = commit_message % { page_title: @page.title } -= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'wiki-form common-note-form prepend-top-default js-quick-submit' } do |f| = form_errors(@page) - if @page.persisted? = f.hidden_field :last_commit_sha, value: @page.last_commit_sha - .form-group + .form-group.row .col-sm-12= f.label :title, class: 'control-label-full-width' .col-sm-12 = f.text_field :title, class: 'form-control', value: @page.title @@ -16,12 +16,12 @@ = icon('lightbulb-o') = s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.") = link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'), target: '_blank' - .form-group + .form-group.row .col-sm-12= f.label :format, class: 'control-label-full-width' .col-sm-12 = f.select :format, options_for_select(ProjectWiki::MARKUPS, {selected: @page.format}), {}, class: 'form-control' - .form-group + .form-group.row .col-sm-12= f.label :content, class: 'control-label-full-width' .col-sm-12 = render layout: 'projects/md_preview', locals: { url: project_wiki_preview_markdown_path(@project, @page.slug) } do @@ -31,7 +31,7 @@ .clearfix .error-alert - .help-block + .form-text.text-muted = succeed '.' do = (s_("WikiMarkdownTip|To link to a (new) page, simply type %{link_example}") % { link_example: '<code>[Link Title](page-slug)</code>' }).html_safe @@ -39,16 +39,16 @@ - markdown_link = link_to s_("WikiMarkdownDocs|documentation"), help_page_path('user/markdown', anchor: 'wiki-specific-markdown') = (s_("WikiMarkdownDocs|More examples are in the %{docs_link}") % { docs_link: markdown_link }).html_safe - .form-group + .form-group.row .col-sm-12= f.label :commit_message, class: 'control-label-full-width' .col-sm-12= f.text_field :message, class: 'form-control', rows: 18, value: commit_message .form-actions - if @page && @page.persisted? = f.submit _("Save changes"), class: 'btn-save btn' - .pull-right + .float-right = link_to _("Cancel"), project_wiki_path(@project, @page), class: 'btn btn-cancel btn-grouped' - else = f.submit s_("Wiki|Create page"), class: 'btn-create btn' - .pull-right + .float-right = link_to _("Cancel"), project_wiki_path(@project, :home), class: 'btn btn-cancel' diff --git a/app/views/projects/wikis/_pages_wiki_page.html.haml b/app/views/projects/wikis/_pages_wiki_page.html.haml index efa16d38f84..cbb441d7509 100644 --- a/app/views/projects/wikis/_pages_wiki_page.html.haml +++ b/app/views/projects/wikis/_pages_wiki_page.html.haml @@ -1,5 +1,5 @@ %li = link_to wiki_page.title, project_wiki_path(@project, wiki_page) %small (#{wiki_page.format}) - .pull-right + .float-right %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.last_version.authored_date) }).html_safe diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml index 2c7551c6f8c..a23396dc0d8 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -1,7 +1,7 @@ %aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "50", "spy" => "affix" } } .sidebar-container .block.wiki-sidebar-header.append-bottom-default - %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" } + %a.gutter-toggle.float-right.d-block.d-sm-block.d-md-none.js-sidebar-wiki-toggle{ href: "#" } = icon('angle-double-right') - git_access_url = project_wikis_git_access_path(@project) diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 9d3d4072027..35c7dc2984a 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -28,9 +28,16 @@ = link_to project_wiki_history_path(@project, @page), class: "btn" do = s_("Wiki|Page history") - if can?(current_user, :admin_wiki, @project) - = link_to project_wiki_path(@project, @page), data: { confirm: s_("WikiPageConfirmDelete|Are you sure you want to delete this page?")}, method: :delete, class: "btn btn-danger" do - = _("Delete") + %button.btn.btn-danger{ data: { toggle: 'modal', + target: '#delete-wiki-modal', + delete_wiki_url: project_wiki_path(@project, @page), + page_title: @page.title.capitalize }, + id: 'delete-wiki-button', + type: 'button' } + = _('Delete') = render 'form' = render 'sidebar' + +#delete-wiki-modal.modal.fade diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index 10dbbc0e42c..909987d8090 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -2,7 +2,7 @@ - page_title s_("WikiClone|Git Access"), _("Wiki") .wiki-page-header.has-sidebar-toggle - %button.btn.btn-default.visible-xs.visible-sm.pull-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } + %button.btn.btn-default.d-block.d-sm-block.d-md-none.float-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') .git-access-header diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index 7d43fd61081..ff9a7b53a86 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -1,80 +1,80 @@ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .fade-left= icon('angle-left') .fade-right= icon('angle-right') - %ul.nav-links.search-filter.scrolling-tabs + %ul.nav-links.search-filter.scrolling-tabs.nav.nav-tabs - if @project - if project_search_tabs?(:blobs) %li{ class: active_when(@scope == 'blobs') } = link_to search_filter_path(scope: 'blobs') do Code - %span.badge + %span.badge.badge-pill = @search_results.blobs_count - if project_search_tabs?(:issues) %li{ class: active_when(@scope == 'issues') } = link_to search_filter_path(scope: 'issues') do Issues - %span.badge + %span.badge.badge-pill = limited_count(@search_results.limited_issues_count) - if project_search_tabs?(:merge_requests) %li{ class: active_when(@scope == 'merge_requests') } = link_to search_filter_path(scope: 'merge_requests') do Merge requests - %span.badge + %span.badge.badge-pill = limited_count(@search_results.limited_merge_requests_count) - if project_search_tabs?(:milestones) %li{ class: active_when(@scope == 'milestones') } = link_to search_filter_path(scope: 'milestones') do Milestones - %span.badge + %span.badge.badge-pill = limited_count(@search_results.limited_milestones_count) - if project_search_tabs?(:notes) %li{ class: active_when(@scope == 'notes') } = link_to search_filter_path(scope: 'notes') do Comments - %span.badge + %span.badge.badge-pill = limited_count(@search_results.limited_notes_count) - if project_search_tabs?(:wiki) %li{ class: active_when(@scope == 'wiki_blobs') } = link_to search_filter_path(scope: 'wiki_blobs') do Wiki - %span.badge + %span.badge.badge-pill = @search_results.wiki_blobs_count - if project_search_tabs?(:commits) %li{ class: active_when(@scope == 'commits') } = link_to search_filter_path(scope: 'commits') do Commits - %span.badge + %span.badge.badge-pill = @search_results.commits_count - elsif @show_snippets %li{ class: active_when(@scope == 'snippet_blobs') } = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do Snippet Contents - %span.badge + %span.badge.badge-pill = @search_results.snippet_blobs_count %li{ class: active_when(@scope == 'snippet_titles') } = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do Titles and Filenames - %span.badge + %span.badge.badge-pill = @search_results.snippet_titles_count - else %li{ class: active_when(@scope == 'projects') } = link_to search_filter_path(scope: 'projects') do Projects - %span.badge + %span.badge.badge-pill = limited_count(@search_results.limited_projects_count) %li{ class: active_when(@scope == 'issues') } = link_to search_filter_path(scope: 'issues') do Issues - %span.badge + %span.badge.badge-pill = limited_count(@search_results.limited_issues_count) %li{ class: active_when(@scope == 'merge_requests') } = link_to search_filter_path(scope: 'merge_requests') do Merge requests - %span.badge + %span.badge.badge-pill = limited_count(@search_results.limited_merge_requests_count) %li{ class: active_when(@scope == 'milestones') } = link_to search_filter_path(scope: 'milestones') do Milestones - %span.badge + %span.badge.badge-pill = limited_count(@search_results.limited_milestones_count) diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml index e4902d368e7..6837f64f132 100644 --- a/app/views/search/_filter.html.haml +++ b/app/views/search/_filter.html.haml @@ -11,7 +11,7 @@ - else Any = icon("chevron-down") - .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-align-right + .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-right = dropdown_title("Filter results by group") = dropdown_filter("Search groups") = dropdown_content @@ -26,7 +26,7 @@ - else Any = icon("chevron-down") - .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-align-right + .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-right = dropdown_title("Filter results by project") = dropdown_filter("Search projects") = dropdown_content diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml index b7a27ef6be2..c413c3d4abb 100644 --- a/app/views/search/results/_issue.html.haml +++ b/app/views/search/results/_issue.html.haml @@ -4,8 +4,8 @@ = link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do %span.term.str-truncated= issue.title - if issue.closed? - %span.label.label-danger.prepend-left-5 Closed - .pull-right ##{issue.iid} + %span.badge.badge-danger.prepend-left-5 Closed + .float-right ##{issue.iid} - if issue.description.present? .description.term = search_md_sanitize(issue, :description) diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml index 8b0fd74f680..519176af108 100644 --- a/app/views/search/results/_merge_request.html.haml +++ b/app/views/search/results/_merge_request.html.haml @@ -3,10 +3,10 @@ = link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do %span.term.str-truncated= merge_request.title - if merge_request.merged? - %span.label.label-primary.prepend-left-5 Merged + %span.badge.badge-primary.prepend-left-5 Merged - elsif merge_request.closed? - %span.label.label-danger.prepend-left-5 Closed - .pull-right= merge_request.to_reference + %span.badge.badge-danger.prepend-left-5 Closed + .float-right= merge_request.to_reference - if merge_request.description.present? .description.term = search_md_sanitize(merge_request, :description) diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml index d46c4d11e51..9c8afb2165b 100644 --- a/app/views/search/results/_snippet_title.html.haml +++ b/app/views/search/results/_snippet_title.html.haml @@ -3,13 +3,13 @@ = link_to reliable_snippet_path(snippet_title) do = truncate(snippet_title.title, length: 60) - if snippet_title.private? - %span.label.label-gray + %span.badge.badge-gray %i.fa.fa-lock private - %span.cgray.monospace.tiny.pull-right.term + %span.cgray.monospace.tiny.float-right.term = snippet_title.file_name - %small.pull-right.cgray + %small.float-right.cgray - if snippet_title.project_id? = link_to snippet_title.project.full_name, project_path(snippet_title.project) diff --git a/app/views/shared/_allow_request_access.html.haml b/app/views/shared/_allow_request_access.html.haml index 53a99a736c0..0e698570d7d 100644 --- a/app/views/shared/_allow_request_access.html.haml +++ b/app/views/shared/_allow_request_access.html.haml @@ -1,4 +1,4 @@ -.checkbox +.form-check = form.label :request_access_enabled do = form.check_box :request_access_enabled %strong Allow users to request access diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml index d3fa324e460..084d295f2c1 100644 --- a/app/views/shared/_auto_devops_callout.html.haml +++ b/app/views/shared/_auto_devops_callout.html.haml @@ -3,7 +3,7 @@ = custom_icon('icon_autodevops') .banner-body.prepend-left-10.append-bottom-10 - %h5.banner-title= s_('AutoDevOps|Auto DevOps (Beta)') + %h5.banner-title= s_('AutoDevOps|Auto DevOps') %p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.') %p - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer') diff --git a/app/views/shared/_choose_group_avatar_button.html.haml b/app/views/shared/_choose_group_avatar_button.html.haml index 75c65520350..0552fe62090 100644 --- a/app/views/shared/_choose_group_avatar_button.html.haml +++ b/app/views/shared/_choose_group_avatar_button.html.haml @@ -1,4 +1,4 @@ %button.btn.js-choose-group-avatar-button{ type: 'button' }= _("Choose File ...") %span.file_name.js-avatar-filename= _("No file chosen") = f.file_field :avatar, class: "js-group-avatar-input hidden" -.help-block= _("The maximum file size allowed is 200KB.") +.form-text.text-muted= _("The maximum file size allowed is 200KB.") diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 687cd4d1532..5fc02ba3160 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -1,13 +1,13 @@ - project = project || @project .git-clone-holder.input-group - .input-group-btn + .input-group-prepend - if allowed_protocols_present? - .clone-dropdown-btn.btn + .input-group-text.clone-dropdown-btn.btn %span = enabled_project_button(project, enabled_protocol) - else - %a#clone-dropdown.btn.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } } + %a#clone-dropdown.input-group-text.btn.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } } %span = default_clone_protocol.upcase = icon('caret-down') @@ -18,5 +18,5 @@ = http_clone_button(project) = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: 'Project clone URL' } - .input-group-btn - = clipboard_button(target: '#project_clone', title: _("Copy URL to clipboard"), class: "btn-default btn-clipboard") + .input-group-append + = clipboard_button(target: '#project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard") diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index 2329de9e11f..68c14c307ac 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -1,7 +1,7 @@ -.form-group.commit_message-group +.form-group.row.commit_message-group - nonce = SecureRandom.hex - descriptions = local_assigns.slice(:message_with_description, :message_without_description) - = label_tag "commit_message-#{nonce}", class: 'control-label' do + = label_tag "commit_message-#{nonce}", class: 'col-form-label col-sm-2' do #{ _('Commit message') } .col-sm-10 .commit-message-container diff --git a/app/views/shared/_commit_well.html.haml b/app/views/shared/_commit_well.html.haml index 50e3d80a84d..6f1fe9bfdc5 100644 --- a/app/views/shared/_commit_well.html.haml +++ b/app/views/shared/_commit_well.html.haml @@ -1,4 +1,4 @@ -.info-well.hidden-xs.project-last-commit.append-bottom-default +.info-well.d-none.d-sm-block.project-last-commit.append-bottom-default .well-segment %ul.blob-commit-info = render 'projects/commits/commit', commit: commit, ref: ref, project: project diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml index f94f8ffc604..7c326d36d99 100644 --- a/app/views/shared/_confirm_modal.html.haml +++ b/app/views/shared/_confirm_modal.html.haml @@ -2,9 +2,9 @@ .modal-dialog .modal-content .modal-header - %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title Confirmation required + %a.close{ href: "#", "data-dismiss" => "modal" } × .modal-body %p.text-danger.js-confirm-text diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index e7fa7477e0c..ecb5b1c6ebc 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -1,7 +1,7 @@ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .fade-left= icon('angle-left') .fade-right= icon('angle-right') - %ul.nav-links.event-filter.scrolling-tabs + %ul.nav-links.event-filter.scrolling-tabs.nav.nav-tabs = event_filter_link EventFilter.all, _('All'), s_('EventFilterBy|Filter by all') - if event_filter_visible(:repository) = event_filter_link EventFilter.push, _('Push events'), s_('EventFilterBy|Filter by push events') diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml index aea0a8fd8e0..0c8d90d92f5 100644 --- a/app/views/shared/_field.html.haml +++ b/app/views/shared/_field.html.haml @@ -11,9 +11,9 @@ .form-group - if type == "password" && value.present? - = form.label name, "Enter new #{title.downcase}", class: "control-label" + = form.label name, "Enter new #{title.downcase}", class: "col-form-label" - else - = form.label name, title, class: "control-label" + = form.label name, title, class: "col-form-label" .col-sm-10 - if type == 'text' = form.text_field name, class: "form-control", placeholder: placeholder, required: required, disabled: disabled @@ -26,4 +26,4 @@ - elsif type == 'password' = form.password_field name, autocomplete: "new-password", class: "form-control", required: value.blank? && required, disabled: disabled - if help - %span.help-block= help + %span.form-text.text-muted= help diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index 403d22c79f8..dbed4b94d61 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -2,15 +2,16 @@ - group_path = root_url - group_path << parent.full_path + '/' if parent -.form-group - = f.label :path, class: 'control-label' do +.form-group.row + = f.label :path, class: 'col-form-label col-sm-2' do Group path .col-sm-10 .input-group.gl-field-error-anchor - .group-root-path.input-group-addon.has-tooltip{ title: group_path, :'data-placement' => 'bottom' } - %span>= root_url - - if parent - %strong= parent.full_path + '/' + .group-root-path.input-group-prepend.has-tooltip{ title: group_path, :'data-placement' => 'bottom' } + .input-group-text + %span>= root_url + - if parent + %strong= parent.full_path + '/' = f.hidden_field :parent_id = f.text_field :path, placeholder: 'open-source', class: 'form-control', autofocus: local_assigns[:autofocus] || false, required: true, @@ -24,8 +25,8 @@ = succeed '.' do = link_to 'Learn more', help_page_path('user/group/index', anchor: 'changing-a-groups-path'), target: '_blank' -.form-group.group-name-holder - = f.label :name, class: 'control-label' do +.form-group.row.group-name-holder + = f.label :name, class: 'col-form-label col-sm-2' do Group name .col-sm-10 = f.text_field :name, class: 'form-control', @@ -33,14 +34,14 @@ title: 'You can choose a descriptive name different from the path.' - if @group.persisted? - .form-group.group-name-holder - = f.label :id, class: 'control-label' do + .form-group.row.group-name-holder + = f.label :id, class: 'col-form-label col-sm-2' do = _("Group ID") .col-sm-10 = f.text_field :id, class: 'form-control', readonly: true -.form-group.group-description-holder - = f.label :description, class: 'control-label' +.form-group.row.group-description-holder + = f.label :description, class: 'col-form-label col-sm-2' .col-sm-10 = f.text_area :description, maxlength: 250, class: 'form-control js-gfm-input', rows: 4 diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml index 3806ead6c87..35673303b85 100644 --- a/app/views/shared/_import_form.html.haml +++ b/app/views/shared/_import_form.html.haml @@ -1,13 +1,13 @@ - ci_cd_only = local_assigns.fetch(:ci_cd_only, false) -.form-group.import-url-data +.form-group.row.import-url-data = f.label :import_url, class: 'label-light' do %span = _('Git repository URL') = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', required: true - .well.prepend-top-20 + .card.prepend-top-20 %ul %li = _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml index 430d9a9dd76..6cc8c485666 100644 --- a/app/views/shared/_issuable_meta_data.html.haml +++ b/app/views/shared/_issuable_meta_data.html.haml @@ -5,21 +5,21 @@ - issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count - if issuable_mr > 0 - %li.issuable-mr.hidden-xs.has-tooltip{ title: _('Related merge requests') } + %li.issuable-mr.d-none.d-sm-block.has-tooltip{ title: _('Related merge requests') } = image_tag('icon-merge-request-unmerged.svg', class: 'icon-merge-request-unmerged') = issuable_mr - if upvotes > 0 - %li.issuable-upvotes.hidden-xs.has-tooltip{ title: _('Upvotes') } + %li.issuable-upvotes.d-none.d-sm-block.has-tooltip{ title: _('Upvotes') } = icon('thumbs-up') = upvotes - if downvotes > 0 - %li.issuable-downvotes.hidden-xs.has-tooltip{ title: _('Downvotes') } + %li.issuable-downvotes.d-none.d-sm-block.has-tooltip{ title: _('Downvotes') } = icon('thumbs-down') = downvotes -%li.issuable-comments.hidden-xs +%li.issuable-comments.d-none.d-sm-block = link_to issuable_url, class: ['has-tooltip', ('no-comments' if note_count.zero?)], title: _('Comments') do = icon('comments') = note_count diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml index 49555b6ff4e..987a5d4f13f 100644 --- a/app/views/shared/_issues.html.haml +++ b/app/views/shared/_issues.html.haml @@ -1,5 +1,5 @@ - if @issues.to_a.any? - .panel.panel-default.panel-small.panel-without-border + .card.card-small.card-without-border %ul.content-list.issues-list.issuable-list = render partial: 'projects/issues/issue', collection: @issues = paginate @issues, theme: "gitlab" diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index 836df57a3a2..ba5b65a209d 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -8,11 +8,11 @@ %li.label-list-item{ id: label_css_id, data: { id: label.id } } = render "shared/label_row", label: label - .visible-xs.visible-sm-inline-block.dropdown + .d-inline-block.d-sm-none.dropdown %button.btn.btn-default.label-options-toggle{ type: 'button', data: { toggle: "dropdown" } } Options = icon('caret-down') - .dropdown-menu.dropdown-menu-align-right + .dropdown-menu.dropdown-menu-right %ul - if show_label_merge_requests_link %li @@ -46,7 +46,7 @@ data: {confirm: 'Remove this label? Are you sure?'}, class: 'text-danger' - .pull-right.hidden-xs.hidden-sm + .float-right.d-none.d-sm-none.d-md-block - if can?(current_user, :admin_label, label) - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group) %button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'), diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index bd4f191502e..f1c1ca9b2c9 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -22,7 +22,7 @@ - if label.description.present? .description-text = markdown_field(label, :description) - .hidden-xs.hidden-sm + .d-none.d-sm-none.d-md-block - if show_label_issues_link = link_to_label(label, subject: subject) { 'Issues' } - if show_label_merge_requests_link diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml index 0517896cfbd..700ec4b606f 100644 --- a/app/views/shared/_merge_requests.html.haml +++ b/app/views/shared/_merge_requests.html.haml @@ -1,5 +1,5 @@ - if @merge_requests.to_a.any? - .panel.panel-default.panel-small.panel-without-border + .card.card-small.card-without-border %ul.content-list.mr-list.issuable-list = render partial: 'projects/merge_requests/merge_request', collection: @merge_requests diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml index 034b76b978f..6c1ac20d544 100644 --- a/app/views/shared/_milestones_filter.html.haml +++ b/app/views/shared/_milestones_filter.html.haml @@ -1,13 +1,13 @@ -%ul.nav-links.mobile-separator +%ul.nav-links.mobile-separator.nav.nav-tabs %li{ class: milestone_class_for_state(params[:state], 'opened', true) }> = link_to milestones_filter_path(state: 'opened') do Open - %span.badge= counts[:opened] + %span.badge.badge-pill= counts[:opened] %li{ class: milestone_class_for_state(params[:state], 'closed') }> = link_to milestones_filter_path(state: 'closed', sort: 'due_date_desc') do Closed - %span.badge= counts[:closed] + %span.badge.badge-pill= counts[:closed] %li{ class: milestone_class_for_state(params[:state], 'all') }> = link_to milestones_filter_path(state: 'all', sort: 'due_date_desc') do All - %span.badge= counts[:all] + %span.badge.badge-pill= counts[:all] diff --git a/app/views/shared/_milestones_sort_dropdown.html.haml b/app/views/shared/_milestones_sort_dropdown.html.haml index 9b2f2fdcc93..a6ba3b59365 100644 --- a/app/views/shared/_milestones_sort_dropdown.html.haml +++ b/app/views/shared/_milestones_sort_dropdown.html.haml @@ -6,7 +6,7 @@ - else = sort_title_due_date_soon = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort + %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-sort %li = link_to page_filter_path(sort: sort_value_due_date_soon, label: true) do = sort_title_due_date_soon diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml index 9221fd1e025..81c33eeea4f 100644 --- a/app/views/shared/_new_commit_form.html.haml +++ b/app/views/shared/_new_commit_form.html.haml @@ -7,8 +7,8 @@ = hidden_field_tag 'branch_name', @ref - else - if can?(current_user, :push_code, @project) - .form-group.branch - = label_tag 'branch_name', _('Target Branch'), class: 'control-label' + .form-group.row.branch + = label_tag 'branch_name', _('Target Branch'), class: 'col-form-label col-sm-2' .col-sm-10 = text_field_tag 'branch_name', branch_name, required: true, class: "form-control js-branch-name ref-name" diff --git a/app/views/shared/_new_merge_request_checkbox.html.haml b/app/views/shared/_new_merge_request_checkbox.html.haml index 133c31f09c4..8dbdf63f9f4 100644 --- a/app/views/shared/_new_merge_request_checkbox.html.haml +++ b/app/views/shared/_new_merge_request_checkbox.html.haml @@ -1,4 +1,4 @@ -.checkbox +.form-check - nonce = SecureRandom.hex = label_tag "create_merge_request-#{nonce}" do = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" diff --git a/app/views/shared/_personal_access_tokens_table.html.haml b/app/views/shared/_personal_access_tokens_table.html.haml index c5e4d6e2871..cadac1cc99d 100644 --- a/app/views/shared/_personal_access_tokens_table.html.haml +++ b/app/views/shared/_personal_access_tokens_table.html.haml @@ -35,7 +35,7 @@ = text_field_tag 'impersonation-token-token', token.token, readonly: true, class: "form-control" = clipboard_button(text: token.token) - path = impersonation ? revoke_admin_user_impersonation_token_path(token.user, token) : revoke_profile_personal_access_token_path(token) - %td= link_to "Revoke", path, method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this #{type} Token? This action cannot be undone." } + %td= link_to "Revoke", path, method: :put, class: "btn btn-danger float-right", data: { confirm: "Are you sure you want to revoke this #{type} Token? This action cannot be undone." } - else .settings-message.text-center This user has no active #{type} Tokens. diff --git a/app/views/shared/_project_limit.html.haml b/app/views/shared/_project_limit.html.haml index f4eb8e491b9..2c52eccccb6 100644 --- a/app/views/shared/_project_limit.html.haml +++ b/app/views/shared/_project_limit.html.haml @@ -1,8 +1,8 @@ - if cookies[:hide_project_limit_message].blank? && !current_user.hide_project_limit && !current_user.can_create_project? && current_user.projects_limit > 0 - .project-limit-message.alert.alert-warning.hidden-xs + .project-limit-message.alert.alert-warning.d-none.d-sm-block You won't be able to create new projects because you have reached your project limit. - .pull-right + .float-right = link_to "Don't show again", profile_path(user: {hide_project_limit: true}), method: :put, class: 'alert-link' | = link_to 'Remind later', '#', class: 'hide-project-limit-message alert-link' diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index f1c39b9e923..4e7061eef1c 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -9,7 +9,7 @@ = hidden_field_tag key, value, id: nil .dropdown = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown qa-branches-select" } - .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging.qa-branches-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } + .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging.qa-branches-dropdown{ class: ("dropdown-menu-right" if local_assigns[:align_right]) } .dropdown-page-one = dropdown_title _("Switch branch/tag") = dropdown_filter _("Search branches and tags") diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index a41aaed66a3..0ebf365c7bd 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -3,24 +3,24 @@ - if lookup_context.template_exists?('help', "projects/services/#{@service.to_param}", true) = render "projects/services/#{@service.to_param}/help", subject: subject - elsif @service.help.present? - .well + .card = markdown @service.help .service-settings - if @service.show_active_box? - .form-group - = form.label :active, "Active", class: "control-label" + .form-group.row + = form.label :active, "Active", class: "col-form-label col-sm-2" .col-sm-10 = form.check_box :active, disabled: disable_fields_service?(@service) - if @service.configurable_events.present? - .form-group - = form.label :url, "Trigger", class: 'control-label' + .form-group.row + = form.label :url, "Trigger", class: 'col-form-label col-sm-2' .col-sm-10 - @service.configurable_events.each do |event| %div - = form.check_box service_event_field_name(event), class: 'pull-left' + = form.check_box service_event_field_name(event), class: 'float-left' .prepend-left-20 = form.label service_event_field_name(event), class: 'list-label' do %strong diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index 7ff5e679f17..be6d4f1c32b 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -2,10 +2,10 @@ - viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues' .dropdown.inline.prepend-left-10 - %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } } + %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } } = sorted_by = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable.dropdown-menu-sort + %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort %li = sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority, label: true), sorted_by) = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sorted_by) diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml index 192d2502aaf..d67409ffe14 100644 --- a/app/views/shared/_visibility_level.html.haml +++ b/app/views/shared/_visibility_level.html.haml @@ -2,7 +2,7 @@ .form-group.visibility-level-setting - if with_label - = f.label :visibility_level, class: 'control-label' do + = f.label :visibility_level, class: 'col-form-label col-sm-2' do Visibility Level = link_to icon('question-circle'), help_page_path("public_access/public_access") %div{ :class => ("col-sm-10" if with_label) } diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml index 0ec7677a566..bdf550cd8b6 100644 --- a/app/views/shared/_visibility_radios.html.haml +++ b/app/views/shared/_visibility_radios.html.haml @@ -2,7 +2,7 @@ - disallowed = disallowed_visibility_level?(form_model, level) - restricted = restricted_visibility_levels.include?(level) - disabled = disallowed || restricted - .radio{ class: [('disabled' if disabled), ('restricted' if restricted)] } + .form-check.pl-0{ class: [('disabled' if disabled), ('restricted' if restricted)] } = form.label "#{model_method}_#{level}" do = form.radio_button model_method, level, checked: (selected_level == level), disabled: disabled = visibility_level_icon(level) diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml index dac60094686..496b94ec953 100644 --- a/app/views/shared/boards/_show.html.haml +++ b/app/views/shared/boards/_show.html.haml @@ -13,7 +13,7 @@ %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal #board-app.boards-app{ "v-cloak" => true, data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" } - .hidden-xs.hidden-sm + .d-none.d-sm-none.d-md-block = render 'shared/issuable/search_bar', type: :boards .boards-list diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml index 4bff6468bb0..67476a3f573 100644 --- a/app/views/shared/boards/components/_board.html.haml +++ b/app/views/shared/boards/components/_board.html.haml @@ -14,18 +14,18 @@ %span.has-tooltip{ "v-if": "list.type === \"label\"", ":title" => '(list.label ? list.label.description : "")', data: { container: "body", placement: "bottom" }, - class: "label color-label title board-title-text", - ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.text_color ? list.label.text_color : \"#2e2e2e\") }" } + class: "badge color-label title board-title-text", + ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.textColor ? list.label.textColor : \"#2e2e2e\") }" } {{ list.title }} - if can?(current_user, :admin_list, current_board_parent) %board-delete{ "inline-template" => true, ":list" => "list", "v-if" => "!list.preset && list.id" } - %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" } + %button.board-delete.has-tooltip.float-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" } = icon("trash") .issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank"' } - %span.issue-count-badge-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' } + %span.issue-count-badge-count.float-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' } {{ list.issuesSize }} - if can?(current_user, :admin_list, current_board_parent) %button.issue-count-badge-add-button.btn.btn-sm.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button", diff --git a/app/views/shared/boards/components/_sidebar.html.haml b/app/views/shared/boards/components/_sidebar.html.haml index b385cc3f962..774dafe5f2c 100644 --- a/app/views/shared/boards/components/_sidebar.html.haml +++ b/app/views/shared/boards/components/_sidebar.html.haml @@ -3,14 +3,14 @@ %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" } .issuable-sidebar .block.issuable-sidebar-header - %span.issuable-header-text.hide-collapsed.pull-left + %span.issuable-header-text.hide-collapsed.float-left %strong {{ issue.title }} %br/ %span = precede "#" do {{ issue.iid }} - %a.gutter-toggle.pull-right{ role: "button", + %a.gutter-toggle.float-right{ role: "button", href: "#", "@click.prevent" => "closeSidebar", "aria-label" => "Toggle sidebar" } diff --git a/app/views/shared/boards/components/sidebar/_due_date.html.haml b/app/views/shared/boards/components/sidebar/_due_date.html.haml index d13b998e6f4..10217b6cbf0 100644 --- a/app/views/shared/boards/components/sidebar/_due_date.html.haml +++ b/app/views/shared/boards/components/sidebar/_due_date.html.haml @@ -3,7 +3,7 @@ Due date - if can_admin_issue? = icon("spinner spin", class: "block-loading") - = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right" + = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link float-right" .value .value-content %span.no-value{ "v-if" => "!issue.dueDate" } diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml index 1c73534c642..a112a9f1f7e 100644 --- a/app/views/shared/boards/components/sidebar/_labels.html.haml +++ b/app/views/shared/boards/components/sidebar/_labels.html.haml @@ -3,13 +3,13 @@ Labels - if can_admin_issue? = icon("spinner spin", class: "block-loading") - = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right" + = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link float-right" .value.issuable-show-labels.dont-hide %span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" } None %a{ href: "#", "v-for" => "label in issue.labels" } - %span.label.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" } + %span.badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" } {{ label.title }} - if can_admin_issue? .selectbox diff --git a/app/views/shared/boards/components/sidebar/_milestone.html.haml b/app/views/shared/boards/components/sidebar/_milestone.html.haml index f51c4a97f2b..f2bedd5e3c9 100644 --- a/app/views/shared/boards/components/sidebar/_milestone.html.haml +++ b/app/views/shared/boards/components/sidebar/_milestone.html.haml @@ -3,7 +3,7 @@ Milestone - if can_admin_issue? = icon("spinner spin", class: "block-loading") - = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right" + = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link float-right" .value %span.no-value{ "v-if" => "!issue.milestone" } None diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml index 0b003125912..5c74e71b644 100644 --- a/app/views/shared/builds/_tabs.html.haml +++ b/app/views/shared/builds/_tabs.html.haml @@ -1,24 +1,24 @@ -%ul.nav-links.mobile-separator +%ul.nav-links.mobile-separator.nav.nav-tabs %li{ class: active_when(scope.nil?) }> = link_to build_path_proc.call(nil) do All - %span.badge.js-totalbuilds-count + %span.badge.badge-pill.js-totalbuilds-count = limited_counter_with_delimiter(all_builds) %li{ class: active_when(scope == 'pending') }> = link_to build_path_proc.call('pending') do Pending - %span.badge + %span.badge.badge-pill = limited_counter_with_delimiter(all_builds.pending) %li{ class: active_when(scope == 'running') }> = link_to build_path_proc.call('running') do Running - %span.badge + %span.badge.badge-pill = limited_counter_with_delimiter(all_builds.running) %li{ class: active_when(scope == 'finished') }> = link_to build_path_proc.call('finished') do Finished - %span.badge + %span.badge.badge-pill = limited_counter_with_delimiter(all_builds.finished) diff --git a/app/views/shared/deploy_keys/_form.html.haml b/app/views/shared/deploy_keys/_form.html.haml index 87c2965bb21..913c065e188 100644 --- a/app/views/shared/deploy_keys/_form.html.haml +++ b/app/views/shared/deploy_keys/_form.html.haml @@ -5,26 +5,26 @@ = form_errors(deploy_key) .form-group - = form.label :title, class: 'control-label' + = form.label :title, class: 'col-form-label col-sm-2' .col-sm-10= form.text_field :title, class: 'form-control' .form-group - if deploy_key.new_record? - = form.label :key, class: 'control-label' + = form.label :key, class: 'col-form-label col-sm-2' .col-sm-10 %p.light Paste a machine public key here. Read more about how to generate it = link_to 'here', help_page_path('ssh/README') = form.text_area :key, class: 'form-control thin_area', rows: 5 - else - = form.label :fingerprint, class: 'control-label' + = form.label :fingerprint, class: 'col-form-label col-sm-2' .col-sm-10 = form.text_field :fingerprint, class: 'form-control', readonly: 'readonly' - if deploy_keys_project.present? = form.fields_for :deploy_keys_projects, deploy_keys_project do |deploy_keys_project_form| .form-group - .control-label + .col-form-label.col-sm-2 .col-sm-10 = deploy_keys_project_form.label :can_push do = deploy_keys_project_form.check_box :can_push diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml index 62437f5fc9d..c7c33288e9d 100644 --- a/app/views/shared/empty_states/_issues.html.haml +++ b/app/views/shared/empty_states/_issues.html.haml @@ -3,10 +3,10 @@ - has_button = button_path || project_select_button .row.empty-state - .col-xs-12 + .col-12 .svg-content = image_tag 'illustrations/issues.svg' - .col-xs-12 + .col-12 .text-content - if current_user %h4 diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml index 04db9de3606..e8749ee3956 100644 --- a/app/views/shared/empty_states/_labels.html.haml +++ b/app/views/shared/empty_states/_labels.html.haml @@ -1,8 +1,8 @@ .row.empty-state.labels - .col-xs-12 + .col-12 .svg-content = image_tag 'illustrations/labels.svg' - .col-xs-12 + .col-12 .text-content %h4= _("Labels can be applied to issues and merge requests to categorize them.") %p= _("You can also star a label to make it a priority label.") diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml index 2edf3557df4..014220761a9 100644 --- a/app/views/shared/empty_states/_merge_requests.html.haml +++ b/app/views/shared/empty_states/_merge_requests.html.haml @@ -3,10 +3,10 @@ - has_button = button_path || project_select_button .row.empty-state.merge-requests - .col-xs-12 + .col-12 .svg-content = image_tag 'illustrations/merge_requests.svg' - .col-xs-12 + .col-12 .text-content - if has_button %h4 diff --git a/app/views/shared/form_elements/_description.html.haml b/app/views/shared/form_elements/_description.html.haml index 38e9899ca4b..e5dfa7dbf71 100644 --- a/app/views/shared/form_elements/_description.html.haml +++ b/app/views/shared/form_elements/_description.html.haml @@ -9,8 +9,8 @@ - else - preview_url = preview_markdown_path(project) -.form-group.detail-page-description - = form.label :description, 'Description', class: 'control-label' +.form-group.row.detail-page-description + = form.label :description, 'Description', class: 'col-form-label col-sm-2' .col-sm-10 = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml index 8607be9cd06..2237b93a10b 100644 --- a/app/views/shared/groups/_dropdown.html.haml +++ b/app/views/shared/groups/_dropdown.html.haml @@ -13,7 +13,7 @@ %span.dropdown-label = options_hash[default_sort_by] = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable + %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable %li.dropdown-header = _("Sort by") - options_hash.each do |value, title| diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index 90395600d4e..a1b901aaffa 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -16,7 +16,7 @@ .avatar-container.s40 = link_to group do - = group_icon(group, class: "avatar s40 hidden-xs") + = group_icon(group, class: "avatar s40 d-none d-sm-block") .title = link_to group.full_name, group, class: 'group-name' diff --git a/app/views/shared/hook_logs/_content.html.haml b/app/views/shared/hook_logs/_content.html.haml index c80b179d525..532712ee6d1 100644 --- a/app/views/shared/hook_logs/_content.html.haml +++ b/app/views/shared/hook_logs/_content.html.haml @@ -6,8 +6,8 @@ %p %strong Trigger: - %td.hidden-xs - %span.label.label-gray.deploy-project-label + %td.d-none.d-sm-block + %span.badge.badge-gray.deploy-project-label = hook_log.trigger.singularize.titleize %p %strong Elapsed time: diff --git a/app/views/shared/hook_logs/_status_label.html.haml b/app/views/shared/hook_logs/_status_label.html.haml index b4ea8e6f952..993880b7d6e 100644 --- a/app/views/shared/hook_logs/_status_label.html.haml +++ b/app/views/shared/hook_logs/_status_label.html.haml @@ -1,3 +1,3 @@ -- label_status = hook_log.success? ? 'label-success' : 'label-danger' +- label_status = hook_log.success? ? 'badge-success' : 'badge-danger' %span{ class: "label #{label_status}" } = hook_log.response_status diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml index 0d507cc7a6e..ca02424215c 100644 --- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml +++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml @@ -4,9 +4,9 @@ .issuable-sidebar.hidden = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: "bulk-update" do .block.issuable-sidebar-header - .filter-item.inline.update-issues-btn.pull-left + .filter-item.inline.update-issues-btn.float-left = button_tag "Update all", class: "btn update-selected-issues btn-info", disabled: true - = button_tag "Cancel", class: "btn btn-default js-bulk-update-menu-hide pull-right" + = button_tag "Cancel", class: "btn btn-default js-bulk-update-menu-hide float-right" .block .title Status diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml index 9ef015047c9..933d4b2ea65 100644 --- a/app/views/shared/issuable/_close_reopen_button.html.haml +++ b/app/views/shared/issuable/_close_reopen_button.html.haml @@ -4,11 +4,11 @@ - if can_update && is_current_user = link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method, - class: "hidden-xs hidden-sm btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}" + class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}" = link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method, - class: "hidden-xs hidden-sm btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}" + class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}" - elsif can_update && !is_current_user = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable - else = link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), - class: 'hidden-xs hidden-sm btn btn-grouped btn-close-color', title: 'Report abuse' + class: 'd-none d-sm-none d-md-block btn btn-grouped btn-close-color', title: 'Report abuse' diff --git a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml index 39a5171c1d6..0d59c9304b4 100644 --- a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml +++ b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml @@ -1,12 +1,12 @@ - display_issuable_type = issuable_display_type(issuable) - button_action = issuable.closed? ? 'reopen' : 'close' - display_button_action = button_action.capitalize -- button_responsive_class = 'hidden-xs hidden-sm' +- button_responsive_class = 'd-none d-sm-none d-md-block' - button_class = "#{button_responsive_class} btn btn-grouped js-issuable-close-button js-btn-issue-action issuable-close-button" - toggle_class = "#{button_responsive_class} btn btn-nr dropdown-toggle js-issuable-close-toggle" - button_method = issuable_close_reopen_button_method(issuable) -.pull-left.btn-group.prepend-left-10.issuable-close-dropdown.droplab-dropdown.js-issuable-close-dropdown +.float-left.btn-group.prepend-left-10.issuable-close-dropdown.droplab-dropdown.js-issuable-close-dropdown = link_to "#{display_button_action} #{display_issuable_type}", close_reopen_issuable_path(issuable), method: button_method, class: "#{button_class} btn-#{button_action}", title: "#{display_button_action} #{display_issuable_type}" @@ -14,7 +14,7 @@ data: { 'dropdown-trigger' => '#issuable-close-menu' }, 'aria-label' => 'Toggle dropdown' do = icon('caret-down', class: 'toggle-icon icon') - %ul#issuable-close-menu.js-issuable-close-menu.dropdown-menu{ class: button_responsive_class, data: { dropdown: true } } + %ul#issuable-close-menu.js-issuable-close-menu.dropdown-menu{ data: { dropdown: true } } %li.close-item{ class: "#{issuable_button_visibility(issuable, true) || 'droplab-item-selected'}", data: { text: "Close #{display_issuable_type}", url: close_issuable_path(issuable), button_class: "#{button_class} btn-close", toggle_class: "#{toggle_class} btn-close-color", method: button_method } } diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 1bd5b4164b1..1cd8ce0826c 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -25,7 +25,7 @@ = render "shared/issuable/label_dropdown", selected: selected_labels, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" } - unless @no_filters_set - .pull-right + .float-right = render 'shared/sort_dropdown' - has_labels = @labels && @labels.any? diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 4c8f03f1498..fbc608b207a 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -11,8 +11,8 @@ = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank", rel: 'noopener noreferrer' and make sure your changes will not unintentionally remove theirs -.form-group - = form.label :title, class: 'control-label' +.form-group.row + = form.label :title, class: 'col-form-label col-sm-2' = render 'shared/issuable/form/template_selector', issuable: issuable = render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?) @@ -20,9 +20,9 @@ = render 'shared/form_elements/description', model: issuable, form: form, project: project - if issuable.respond_to?(:confidential) - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox + .form-group.row + .offset-sm-2.col-sm-10 + .form-check = form.label :confidential do = form.check_box :confidential This issue is confidential and should only be visible to team members with at least Reporter access. @@ -36,8 +36,8 @@ = render 'shared/issuable/form/contribution', issuable: issuable, form: form - if @merge_request_to_resolve_discussions_of - .form-group - .col-sm-10.col-sm-offset-2 + .form-group.row + .col-sm-10.offset-sm-2 = icon('info-circle') - if @merge_request_to_resolve_discussions_of.discussions_can_be_resolved_by?(current_user) = hidden_field_tag 'merge_request_to_resolve_discussions_of', @merge_request_to_resolve_discussions_of.iid @@ -57,7 +57,7 @@ - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) .row-content-block{ class: (is_footer ? "footer-block" : "middle-block") } - .pull-right + .float-right - if issuable.new_record? = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel' - else diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml index 91aa329eb93..9f4021802df 100644 --- a/app/views/shared/issuable/_label_page_create.html.haml +++ b/app/views/shared/issuable/_label_page_create.html.haml @@ -12,7 +12,7 @@ .dropdown-label-color-preview.js-dropdown-label-color-preview %input#new_label_color.default-dropdown-input{ type: "text", placeholder: _('Assign custom color like #FF0000') } .clearfix - %button.btn.btn-primary.pull-left.js-new-label-btn{ type: "button" } + %button.btn.btn-primary.float-left.js-new-label-btn{ type: "button" } = _('Create') - %button.btn.btn-default.pull-right.js-cancel-label-btn{ type: "button" } + %button.btn.btn-default.float-right.js-cancel-label-btn{ type: "button" } = _('Cancel') diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index a5f40ea934b..157637dbd11 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -2,7 +2,7 @@ - page_context_word = type.to_s.humanize(capitalize: false) - display_count = local_assigns.fetch(:display_count, :true) -%ul.nav-links.issues-state-filters.mobile-separator +%ul.nav-links.issues-state-filters.mobile-separator.nav.nav-tabs %li{ class: active_when(params[:state] == 'opened') }> = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do #{issuables_state_counter_text(type, :opened, display_count)} diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index fc6f71ef60f..644f7c4dd28 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -107,7 +107,7 @@ .dropdown.prepend-left-10#js-add-list %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data } Add list - .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable + .dropdown-menu.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" } - if can?(current_user, :admin_label, board.parent) = render partial: "shared/issuable/label_page_create" diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 093033775a9..602729b172a 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -5,9 +5,9 @@ - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .block.issuable-sidebar-header - if current_user - %span.issuable-header-text.hide-collapsed.pull-left + %span.issuable-header-text.hide-collapsed.float-left = _('Todo') - %a.gutter-toggle.pull-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left' } } + %a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } } = sidebar_gutter_toggle_icon - if current_user = render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable @@ -19,7 +19,7 @@ .block.assignee = render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable, signed_in: current_user.present? .block.milestone - .sidebar-collapsed-icon.has-tooltip{ title: milestone_tooltip_title(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } } + .sidebar-collapsed-icon.has-tooltip{ title: milestone_tooltip_title(issuable.milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } = icon('clock-o', 'aria-hidden': 'true') %span.milestone-title - if issuable.milestone @@ -30,10 +30,10 @@ = _('Milestone') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable - = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right' + = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right' .value.hide-collapsed - if issuable.milestone - = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_due_date(issuable.milestone), data: { container: "body", html: 1 } + = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_due_date(issuable.milestone), data: { container: "body", html: 'true' } - else %span.no-value = _('None') @@ -49,7 +49,7 @@ = icon('spinner spin', 'aria-hidden': 'true') - if issuable.has_attribute?(:due_date) .block.due_date - .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 1 }, title: sidebar_due_date_tooltip_label(issuable) } + .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable) } = icon('calendar', 'aria-hidden': 'true') %span.js-due-date-sidebar-value = issuable.due_date.try(:to_s, :medium) || 'None' @@ -57,7 +57,7 @@ = _('Due date') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right' + = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right' .value.hide-collapsed %span.value-content - if issuable.due_date @@ -86,7 +86,7 @@ - if @labels - selected_labels = issuable.labels .block.labels - .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } } + .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body", boundary: 'viewport' } } = icon('tags', 'aria-hidden': 'true') %span = selected_labels.size @@ -94,7 +94,7 @@ = _('Labels') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable - = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right' + = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right' .value.issuable-show-labels.dont-hide.hide-collapsed{ class: ("has-labels" if selected_labels.any?) } - if selected_labels.any? - selected_labels.each do |label| @@ -133,7 +133,7 @@ - project_ref = cross_project_reference(@project, issuable) .block.project-reference .sidebar-collapsed-icon.dont-change-state - = clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left") + = clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left", boundary: 'viewport') .cross-project-reference.hide-collapsed %span = _('Reference:') @@ -142,11 +142,11 @@ = clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left") - if current_user && issuable.can_move?(current_user) .block.js-sidebar-move-issue-block - .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body' }, title: _('Move issue') } + .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') } = custom_icon('icon_arrow_right') .dropdown.sidebar-move-issue-dropdown.hide-collapsed %button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button', - data: { toggle: 'dropdown' } } + data: { toggle: 'dropdown', display: 'static' } } = _('Move issue') .dropdown-menu.dropdown-menu-selectable = dropdown_title(_('Move issue')) diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml index 21006a76b28..e1cde527ad7 100644 --- a/app/views/shared/issuable/_sidebar_assignees.html.haml +++ b/app/views/shared/issuable/_sidebar_assignees.html.haml @@ -4,7 +4,7 @@ = _('Assignee') = icon('spinner spin') - else - .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: sidebar_assignee_tooltip_label(issuable) } + .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body", boundary: 'viewport' }, title: sidebar_assignee_tooltip_label(issuable) } - if issuable.assignee = link_to_member(@project, issuable.assignee, size: 24) - else @@ -13,15 +13,15 @@ = _('Assignee') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable - = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right' + = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right' - if !signed_in - %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => _('Toggle sidebar') } + %a.gutter-toggle.float-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => _('Toggle sidebar') } = sidebar_gutter_toggle_icon .value.hide-collapsed - if issuable.assignee = link_to_member(@project, issuable.assignee, size: 32, extra_class: 'bold') do - if !issuable.can_be_merged_by?(issuable.assignee) - %span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: _('Not allowed to merge') } + %span.float-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: _('Not allowed to merge') } = icon('exclamation-triangle', 'aria-hidden': 'true') %span.username = issuable.assignee.to_reference diff --git a/app/views/shared/issuable/_sidebar_todo.html.haml b/app/views/shared/issuable/_sidebar_todo.html.haml index 74327fb1ba8..583b33a8a1b 100644 --- a/app/views/shared/issuable/_sidebar_todo.html.haml +++ b/app/views/shared/issuable/_sidebar_todo.html.haml @@ -3,7 +3,7 @@ - todo_content = is_collapsed ? icon('plus-square') : _('Add todo') %button.issuable-todo-btn.js-issuable-todo{ type: 'button', - class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn pull-right'), + class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn float-right'), title: (todo.nil? ? _('Add todo') : _('Mark todo as done')), 'aria-label' => (todo.nil? ? _('Add todo') : _('Mark todo as done')), data: issuable_todo_button_data(issuable, todo, is_collapsed) } diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml index 9a589387255..fbc96baa0f7 100644 --- a/app/views/shared/issuable/form/_branch_chooser.html.haml +++ b/app/views/shared/issuable/form/_branch_chooser.html.haml @@ -6,13 +6,13 @@ %hr - if issuable.new_record? - .form-group - = form.label :source_branch, class: 'control-label' + .form-group.row + = form.label :source_branch, class: 'col-form-label col-sm-2' .col-sm-10 .issuable-form-select-holder = form.select(:source_branch, [issuable.source_branch], {}, { class: 'source_branch select2 ref-name', disabled: true }) -.form-group - = form.label :target_branch, class: 'control-label' +.form-group.row + = form.label :target_branch, class: 'col-form-label col-sm-2' .col-sm-10.target-branch-select-dropdown-container .issuable-form-select-holder = form.hidden_field(:target_branch, diff --git a/app/views/shared/issuable/form/_contribution.html.haml b/app/views/shared/issuable/form/_contribution.html.haml index de508278d7c..f12c23cbc64 100644 --- a/app/views/shared/issuable/form/_contribution.html.haml +++ b/app/views/shared/issuable/form/_contribution.html.haml @@ -8,13 +8,13 @@ %hr .form-group - .control-label + .col-form-label = _('Contribution') .col-sm-10 - .checkbox + .form-check = form.label :allow_maintainer_to_push do = form.check_box :allow_maintainer_to_push, disabled: !issuable.can_allow_maintainer_to_push?(current_user) = _('Allow edits from maintainers.') = link_to 'About this feature', help_page_path('user/project/merge_requests/maintainer_access') - .help-block + .form-text.text-muted = allow_maintainer_push_unavailable_reason(issuable) diff --git a/app/views/shared/issuable/form/_issue_assignee.html.haml b/app/views/shared/issuable/form/_issue_assignee.html.haml index 9b2b6e572e7..0d13fccaf3e 100644 --- a/app/views/shared/issuable/form/_issue_assignee.html.haml +++ b/app/views/shared/issuable/form/_issue_assignee.html.haml @@ -11,7 +11,7 @@ Assignee = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable - = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right' + = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link float-right' .value.hide-collapsed - if assignees.any? - assignees.each do |assignee| diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml index 8f6509a8ce8..1df881e4102 100644 --- a/app/views/shared/issuable/form/_merge_params.html.haml +++ b/app/views/shared/issuable/form/_merge_params.html.haml @@ -7,10 +7,10 @@ -# This comment and the following line should only exist in CE. - return unless issuable.can_remove_source_branch?(current_user) -.form-group - .col-sm-10.col-sm-offset-2 +.form-group.row + .col-sm-10.offset-sm-2 - if issuable.can_remove_source_branch?(current_user) - .checkbox + .form-check = label_tag 'merge_request[force_remove_source_branch]' do = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch? diff --git a/app/views/shared/issuable/form/_merge_request_assignee.html.haml b/app/views/shared/issuable/form/_merge_request_assignee.html.haml index d7740eddcca..05c03dedd91 100644 --- a/app/views/shared/issuable/form/_merge_request_assignee.html.haml +++ b/app/views/shared/issuable/form/_merge_request_assignee.html.haml @@ -9,12 +9,12 @@ Assignee = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable - = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right' + = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link float-right' .value.hide-collapsed - if merge_request.assignee = link_to_member(@project, merge_request.assignee, size: 32, extra_class: 'bold') do - unless merge_request.can_be_merged_by?(merge_request.assignee) - %span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' } + %span.float-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' } = icon('exclamation-triangle', 'aria-hidden': 'true') %span.username = merge_request.assignee.to_reference diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml index 1608bd59cf1..1e27253aaeb 100644 --- a/app/views/shared/issuable/form/_metadata.html.haml +++ b/app/views/shared/issuable/form/_metadata.html.haml @@ -8,28 +8,28 @@ %hr .row - %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") } - .form-group.issue-assignee + %div{ class: (has_due_date ? "col-lg-6" : "col-12") } + .form-group.row.issue-assignee - if issuable.is_a?(Issue) = render "shared/issuable/form/metadata_issue_assignee", issuable: issuable, form: form, has_due_date: has_due_date - else = render "shared/issuable/form/metadata_merge_request_assignee", issuable: issuable, form: form, has_due_date: has_due_date - .form-group.issue-milestone - = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" - .col-sm-10{ class: ("col-lg-8" if has_due_date) } + .form-group.row.issue-milestone + = form.label :milestone_id, "Milestone", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-2"}" + .col-10{ class: ("col-md-8" if has_due_date) } .issuable-form-select-holder = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" - .form-group + .form-group.row - has_labels = @labels && @labels.any? - = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" + = form.label :label_ids, "Labels", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-2"}" = form.hidden_field :label_ids, multiple: true, value: '' - .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } + .col-10{ class: "#{"col-md-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } .issuable-form-select-holder = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label" - if has_due_date .col-lg-6 - .form-group - = form.label :due_date, "Due date", class: "control-label" - .col-sm-10 + .form-group.row + = form.label :due_date, "Due date", class: "col-form-label col-md-2 col-lg-4" + .col-8 .issuable-form-select-holder = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" diff --git a/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml b/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml index 567cde764e2..6d4f9ccd66f 100644 --- a/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml +++ b/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml @@ -1,5 +1,5 @@ -= form.label :assignee_ids, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" -.col-sm-10{ class: ("col-lg-8" if has_due_date) } += form.label :assignee_ids, "Assignee", class: "col-form-label #{"col-md-2 col-lg-4" if has_due_date}" +.col-sm-10{ class: ("col-md-8" if has_due_date) } .issuable-form-select-holder.selectbox - issuable.assignees.each do |assignee| = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { meta: assignee.name, avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username } diff --git a/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml b/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml index d0ea4e149df..3521f71f409 100644 --- a/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml +++ b/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml @@ -1,4 +1,4 @@ -= form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" += form.label :assignee_id, "Assignee", class: "col-form-label #{has_due_date ? "col-lg-4" : "col-sm-2"}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder = form.hidden_field :assignee_id diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml index e81639f35ea..c4f30f5f4d9 100644 --- a/app/views/shared/issuable/form/_title.html.haml +++ b/app/views/shared/issuable/form/_title.html.haml @@ -9,7 +9,7 @@ autocomplete: 'off', class: 'form-control pad qa-issuable-form-title' - if issuable.respond_to?(:work_in_progress?) - %p.help-block + %p.form-text.text-muted .js-wip-explanation %a.js-toggle-wip{ href: '', tabindex: -1 } Remove the @@ -30,7 +30,7 @@ merge request from being merged before it's ready. - if no_issuable_templates && can?(current_user, :push_code, issuable.project) - %p.help-block + %p.form-text.text-muted Add = link_to 'description templates', help_page_path('user/project/description_templates'), tabindex: -1 to help your contributors communicate effectively! diff --git a/app/views/shared/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml index e8b04f56839..f79f66b144f 100644 --- a/app/views/shared/labels/_form.html.haml +++ b/app/views/shared/labels/_form.html.haml @@ -1,21 +1,22 @@ -= form_for @label, as: :label, url: url, html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f| += form_for @label, as: :label, url: url, html: { class: 'label-form js-quick-submit js-requires-input' } do |f| = form_errors(@label) - .form-group - = f.label :title, class: 'control-label' + .form-group.row + = f.label :title, class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :title, class: "form-control", required: true, autofocus: true - .form-group - = f.label :description, class: 'control-label' + .form-group.row + = f.label :description, class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :description, class: "form-control js-quick-submit" - .form-group - = f.label :color, "Background color", class: 'control-label' + .form-group.row + = f.label :color, "Background color", class: 'col-form-label col-sm-2' .col-sm-10 .input-group - .input-group-addon.label-color-preview + .input-group-prepend + .input-group-text.label-color-preview = f.text_field :color, class: "form-control" - .help-block + .form-text.text-muted Choose any color. %br Or you can choose one of suggested colors below diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml index fc634856061..67b8843a27f 100644 --- a/app/views/shared/members/_group.html.haml +++ b/app/views/shared/members/_group.html.haml @@ -14,7 +14,7 @@ %span{ class: ('text-warning' if group_link.expires_soon?) } Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)} .controls.member-controls - = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'form-horizontal js-edit-member-form' do + = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form' do = 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", @@ -23,7 +23,7 @@ %span.dropdown-toggle-text = group_link.human_access = icon("chevron-down") - .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable + .dropdown-menu.dropdown-select.dropdown-menu-right.dropdown-menu-selectable = dropdown_title("Change permissions") .dropdown-content %ul @@ -40,6 +40,6 @@ method: :delete, data: { confirm: "Are you sure you want to remove #{group.name}?" }, class: 'btn btn-remove prepend-left-10' do - %span.visible-xs-block + %span.d-block.d-sm-none Delete - = icon('trash', class: 'hidden-xs') + = icon('trash', class: 'd-none d-sm-block') diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 1961ad6d616..42b2d27c44a 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -14,10 +14,10 @@ %span.cgray= user.to_reference - if user == current_user - %span.label.label-success.prepend-left-5 It's you + %span.badge.badge-success.prepend-left-5 It's you - if user.blocked? - %label.label.label-danger + %label.badge.badge-danger %strong Blocked - if user.two_factor_enabled? @@ -57,11 +57,11 @@ - if member.can_resend_invite? = link_to icon('paper-plane'), polymorphic_path([:resend_invite, member]), method: :post, - class: 'btn btn-default prepend-left-10 hidden-xs', + class: 'btn btn-default prepend-left-10 d-none d-sm-block', title: 'Resend invite' - if user != current_user && member.can_update? - = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| + = form_for member, remote: true, html: { class: 'js-edit-member-form form-horizontal' } do |f| = f.hidden_field :access_level .member-form-control.dropdown.append-right-5 %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", @@ -69,7 +69,7 @@ %span.dropdown-toggle-text = member.human_access = icon("chevron-down") - .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable + .dropdown-menu.dropdown-select.dropdown-menu-right.dropdown-menu-selectable = dropdown_title("Change permissions") .dropdown-content %ul @@ -93,10 +93,10 @@ method: :post, class: 'btn btn-success prepend-left-10', title: 'Grant access' do - %span{ class: ('visible-xs-block' unless force_mobile_view) } + %span{ class: ('d-block d-sm-none' unless force_mobile_view) } Grant access - unless force_mobile_view - = icon('check inverse', class: 'hidden-xs') + = icon('check inverse', class: 'd-none d-sm-block') - if member.can_remove? - if current_user == user @@ -110,9 +110,9 @@ data: { confirm: remove_member_message(member) }, class: 'btn btn-remove prepend-left-10', title: remove_member_title(member) do - %span{ class: ('visible-xs-block' unless force_mobile_view) } + %span{ class: ('d-block d-sm-none' unless force_mobile_view) } Delete - unless force_mobile_view - = icon('trash', class: 'hidden-xs') + = icon('trash', class: 'd-none d-sm-block') - else %span.member-access-text= member.human_access diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml index 1fbd6bcc4cb..54679ab86cc 100644 --- a/app/views/shared/members/_requests.html.haml +++ b/app/views/shared/members/_requests.html.haml @@ -4,10 +4,10 @@ - return if requesters.empty? -.panel.panel-default.prepend-top-default{ class: ('panel-mobile' if force_mobile_view ) } - .panel-heading +.card.prepend-top-default{ class: ('card-mobile' if force_mobile_view ) } + .card-header Users requesting access to %strong= membership_source.name - %span.badge= requesters.size + %span.badge.badge-pill= requesters.size %ul.content-list.members-list = render partial: 'shared/members/member', collection: requesters, as: :member, locals: { force_mobile_view: force_mobile_view } diff --git a/app/views/shared/members/_sort_dropdown.html.haml b/app/views/shared/members/_sort_dropdown.html.haml index bad0891f9f2..56b8c8f033e 100644 --- a/app/views/shared/members/_sort_dropdown.html.haml +++ b/app/views/shared/members/_sort_dropdown.html.haml @@ -1,6 +1,6 @@ .dropdown.inline.member-sort-dropdown = dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' }) - %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable + %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable %li.dropdown-header Sort by - member_sort_options_hash.each do |value, title| diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml index a74cdbe274b..608dd35182d 100644 --- a/app/views/shared/milestones/_form_dates.html.haml +++ b/app/views/shared/milestones/_form_dates.html.haml @@ -1,12 +1,11 @@ .col-md-6 - .form-group - = f.label :start_date, "Start Date", class: "control-label" + .form-group.row + = f.label :start_date, "Start Date", class: "col-form-label col-sm-2" .col-sm-10 = f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date" - %a.inline.pull-right.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date -.col-md-6 - .form-group - = f.label :due_date, "Due Date", class: "control-label" + %a.inline.float-right.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date + .form-group.row + = f.label :due_date, "Due Date", class: "col-form-label col-sm-2" .col-sm-10 = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date" - %a.inline.pull-right.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date + %a.inline.float-right.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml index 7175e275f95..d8e4d2ff88c 100644 --- a/app/views/shared/milestones/_issuables.html.haml +++ b/app/views/shared/milestones/_issuables.html.haml @@ -1,9 +1,9 @@ - show_counter = local_assigns.fetch(:show_counter, false) - primary = local_assigns.fetch(:primary, false) -- panel_class = primary ? 'panel-primary' : 'panel-default' +- panel_class = primary ? 'bg-primary text-white' : '' -.panel{ class: panel_class } - .panel-heading +.card{ class: panel_class } + .card-header .title = title - if show_counter diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml index a26b3b8009e..6797520650d 100644 --- a/app/views/shared/milestones/_labels_tab.html.haml +++ b/app/views/shared/milestones/_labels_tab.html.haml @@ -10,7 +10,7 @@ %span.prepend-description-left = markdown_field(label, :description) - .pull-right.hidden-xs.hidden-sm.hidden-md + .float-right.d-none.d-lg-block.d-xl-block = link_to milestones_label_path(options.merge(state: 'opened')), class: 'btn btn-transparent btn-action' do - pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue' = link_to milestones_label_path(options.merge(state: 'closed')), class: 'btn btn-transparent btn-action' do diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index ac494814f55..09bbd04c2bf 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -11,7 +11,7 @@ %span - Project Milestone .col-sm-6 - .pull-right.light #{milestone.percent_complete(current_user)}% complete + .float-right.light #{milestone.percent_complete(current_user)}% complete .row .col-sm-6 = link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path @@ -26,19 +26,19 @@ .projects - milestone.milestones.each do |milestone| = link_to milestone_path(milestone) do - %span.label.label-gray + %span.badge.badge-gray = dashboard ? milestone.project.full_name : milestone.project.name - if @group .col-sm-6.milestone-actions - if can?(current_user, :admin_milestones, @group) - if milestone.group_milestone? - = link_to edit_group_milestone_path(@group, milestone), class: "btn btn-xs btn-grouped" do + = link_to edit_group_milestone_path(@group, milestone), class: "btn btn-sm btn-grouped" do Edit \ - if milestone.closed? - = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen" + = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen" - else - = link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-xs btn-grouped btn-close" + = link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-sm btn-grouped btn-close" - if @project .row @@ -46,12 +46,12 @@ = render('shared/milestone_expired', milestone: milestone) .col-sm-6.milestone-actions - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? - = link_to edit_project_milestone_path(milestone.project, milestone), class: "btn btn-xs btn-grouped" do + = link_to edit_project_milestone_path(milestone.project, milestone), class: "btn btn-sm btn-grouped" do Edit \ - if @project.group - %button.js-promote-project-milestone-button.btn.btn-xs.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'), + %button.js-promote-project-milestone-button.btn.btn-sm.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'), disabled: true, type: 'button', data: { url: promote_project_milestone_path(milestone.project, milestone), @@ -62,9 +62,9 @@ toggle: 'modal' } } = _('Promote') - = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped" + = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-sm btn-close btn-grouped" - %button.js-delete-milestone-button.btn.btn-xs.btn-grouped.btn-danger{ data: { toggle: 'modal', + %button.js-delete-milestone-button.btn.btn-sm.btn-grouped.btn-danger{ data: { toggle: 'modal', target: '#delete-milestone-modal', milestone_id: milestone.id, milestone_title: markdown_field(milestone, :title), diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml index 8e9a1b56bb8..95138af3950 100644 --- a/app/views/shared/milestones/_sidebar.html.haml +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -4,7 +4,7 @@ %aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } .issuable-sidebar.milestone-sidebar .block.milestone-progress.issuable-sidebar-header - %a.gutter-toggle.pull-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left' } } + %a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } } = sidebar_gutter_toggle_icon .title.hide-collapsed %strong.bold== #{milestone.percent_complete(current_user)}% @@ -14,7 +14,7 @@ = milestone_progress_bar(milestone) .block.milestone-progress.hide-expanded - .sidebar-collapsed-icon.has-tooltip{ title: milestone_progress_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } } + .sidebar-collapsed-icon.has-tooltip{ title: milestone_progress_tooltip_text(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } %span== #{milestone.percent_complete(current_user)}% = milestone_progress_bar(milestone) @@ -22,7 +22,7 @@ .title Start date - if @project && can?(current_user, :admin_milestone, @project) - = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link pull-right' + = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link float-right' .value %span.value-content - if milestone.start_date @@ -36,29 +36,29 @@ %span.collapsed-milestone-date - if milestone.start_date && milestone.due_date - if milestone.start_date.year == milestone.due_date.year - .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } } + .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } = milestone.start_date.strftime('%b %-d') - else - .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } } + .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } = milestone.start_date.strftime('%b %-d %Y') .date-separator - - .due_date.has-tooltip{ title: milestone_time_for(milestone.due_date, :end), data: { container: 'body', html: 1, placement: 'left' } } + .due_date.has-tooltip{ title: milestone_time_for(milestone.due_date, :end), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } = milestone.due_date.strftime('%b %-d %Y') - elsif milestone.start_date From - .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } } + .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } = milestone.start_date.strftime('%b %-d %Y') - elsif milestone.due_date Until - .milestone-date.has-tooltip{ title: milestone_time_for(milestone.due_date, :end), data: { container: 'body', html: 1, placement: 'left' } } + .milestone-date.has-tooltip{ title: milestone_time_for(milestone.due_date, :end), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } = milestone.due_date.strftime('%b %-d %Y') - else - .has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } } + .has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } None .title.hide-collapsed Due date - if @project && can?(current_user, :admin_milestone, @project) - = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link pull-right' + = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link float-right' .value.hide-collapsed %span.value-content - if milestone.due_date @@ -72,15 +72,15 @@ - if !project || can?(current_user, :read_issue, project) .block.issues - .sidebar-collapsed-icon.has-tooltip{ title: milestone_issues_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } } + .sidebar-collapsed-icon.has-tooltip{ title: milestone_issues_tooltip_text(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } %strong = custom_icon('issues') %span= milestone.issues_visible_to_user(current_user).count .title.hide-collapsed Issues - %span.badge= milestone.issues_visible_to_user(current_user).count + %span.badg.badge-pille= milestone.issues_visible_to_user(current_user).count - if show_new_issue_link?(project) - = link_to new_project_issue_path(project, issue: { milestone_id: milestone.id }), class: "pull-right", title: "New Issue" do + = link_to new_project_issue_path(project, issue: { milestone_id: milestone.id }), class: "float-right", title: "New Issue" do New issue .value.hide-collapsed.bold %span.milestone-stat @@ -100,13 +100,13 @@ = icon('spinner spin') .block.merge-requests - .sidebar-collapsed-icon.has-tooltip{ title: milestone_merge_requests_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } } + .sidebar-collapsed-icon.has-tooltip{ title: milestone_merge_requests_tooltip_text(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } %strong = custom_icon('mr_bold') %span= milestone.merge_requests.count .title.hide-collapsed Merge requests - %span.badge= milestone.merge_requests.count + %span.badge.badge-pill= milestone.merge_requests.count .value.hide-collapsed.bold - if !project || can?(current_user, :read_merge_request, project) %span.milestone-stat @@ -136,10 +136,10 @@ - if milestone_ref.present? .block.reference .sidebar-collapsed-icon.dont-change-state - = clipboard_button(text: milestone_ref, title: "Copy reference to clipboard", placement: "left") + = clipboard_button(text: milestone_ref, title: "Copy reference to clipboard", placement: "left", boundary: 'viewport') .cross-project-reference.hide-collapsed %span Reference: %cite{ title: milestone_ref } = milestone_ref - = clipboard_button(text: milestone_ref, title: "Copy reference to clipboard", placement: "left") + = clipboard_button(text: milestone_ref, title: "Copy reference to clipboard", placement: "left", boundary: 'viewport') diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index b95a4ea674d..e6a65161ed6 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -3,29 +3,29 @@ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .fade-left= icon('angle-left') .fade-right= icon('angle-right') - %ul.nav-links.scrolling-tabs.js-milestone-tabs + %ul.nav-links.scrolling-tabs.js-milestone-tabs.nav.nav-tabs - if issues_accessible %li.active = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do Issues - %span.badge= milestone.issues_visible_to_user(current_user).size + %span.badge.badge-pill= milestone.issues_visible_to_user(current_user).size %li = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do Merge Requests - %span.badge= milestone.merge_requests.size + %span.badge.badge-pill= milestone.merge_requests.size - else %li.active = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do Merge Requests - %span.badge= milestone.merge_requests.size + %span.badge.badge-pill= milestone.merge_requests.size %li = link_to '#tab-participants', 'data-toggle' => 'tab', 'data-endpoint': milestone_participants_tab_path(milestone) do Participants - %span.badge= milestone.participants.count + %span.badge.badge-pill= milestone.participants.count %li = link_to '#tab-labels', 'data-toggle' => 'tab', 'data-endpoint': milestone_labels_tab_path(milestone) do Labels - %span.badge= milestone.labels.count + %span.badge.badge-pill= milestone.labels.count - issues = milestone.sorted_issues(current_user) - show_project_name = local_assigns.fetch(:show_project_name, false) diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index 797ff034bb2..ee0e35cedc6 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -5,7 +5,7 @@ - is_dynamic_milestone = milestone.legacy_group_milestone? || milestone.dashboard_milestone? .detail-page-header - %a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" } + %a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: "#" } = icon('angle-double-left') .status-box{ class: "status-box-#{milestone.closed? ? 'closed' : 'open'}" } @@ -22,7 +22,7 @@ · = milestone_date_range(milestone) - if group - .pull-right + .float-right - if can?(current_user, :admin_milestones, group) - if milestone.group_milestone? = link_to edit_group_milestone_path(group, milestone), class: "btn btn btn-grouped" do diff --git a/app/views/shared/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml index 4b9af78bc1a..ed336df4e9d 100644 --- a/app/views/shared/notes/_comment_button.html.haml +++ b/app/views/shared/notes/_comment_button.html.haml @@ -1,6 +1,6 @@ - noteable_name = @note.noteable.human_class_name -.pull-left.btn-group.append-right-10.droplab-dropdown.comment-type-dropdown.js-comment-type-dropdown +.float-left.btn-group.append-right-10.droplab-dropdown.comment-type-dropdown.js-comment-type-dropdown %input.btn.btn-nr.btn-create.comment-btn.js-comment-button.js-comment-submit-button{ type: 'submit', value: 'Comment' } - if @note.can_be_discussion_note? diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml index bc1ac3d8ac2..00eae553279 100644 --- a/app/views/shared/notes/_hints.html.haml +++ b/app/views/shared/notes/_hints.html.haml @@ -32,4 +32,4 @@ = icon('file-image-o', class: 'toolbar-button-icon') Attach a file - %button.btn.btn-default.btn-xs.hide.button-cancel-uploading-files{ type: 'button' } Cancel + %button.btn.btn-default.btn-sm.hide.button-cancel-uploading-files{ type: 'button' } Cancel diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml index 893a7f26ebd..ca6e3602f05 100644 --- a/app/views/shared/notes/_note.html.haml +++ b/app/views/shared/notes/_note.html.haml @@ -25,7 +25,7 @@ - elsif note_counter == 0 - counter = badge_counter if local_assigns[:badge_counter] - badge_class = "hidden" if @fresh_discussion || counter.nil? - %span.badge{ class: badge_class } + %span.badge.badge-pill{ class: badge_class } = counter .timeline-content .note-header @@ -41,8 +41,9 @@ - if note.system %span.system-note-message = markdown_field(note, :note) - %a{ href: "##{dom_id(note)}" } - = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') + %span.system-note-separator + · + %a.system-note-separator{ href: "##{dom_id(note)}" }= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') - unless note.system? .note-actions - if note.for_personal_snippet? diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml index 1db7c4e67cf..b98d5339d2d 100644 --- a/app/views/shared/notes/_notes_with_form.html.haml +++ b/app/views/shared/notes/_notes_with_form.html.haml @@ -13,7 +13,7 @@ .timeline-entry-inner .flash-container.timeline-content - .timeline-icon.hidden-xs.hidden-sm + .timeline-icon.d-none.d-sm-none.d-md-block %a.author_link{ href: user_path(current_user) } = image_tag avatar_icon_for_user(current_user), alt: current_user.to_reference, class: 'avatar s40' .timeline-content.timeline-content-form diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml index 9186c2ba9c9..d830225d169 100644 --- a/app/views/shared/notifications/_custom_notifications.html.haml +++ b/app/views/shared/notifications/_custom_notifications.html.haml @@ -22,7 +22,7 @@ - NotificationSetting::EMAIL_EVENTS.each_with_index do |event, index| - field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]" .form-group - .checkbox{ class: ("prepend-top-0" if index == 0) } + .form-check{ class: ("prepend-top-0" if index == 0) } %label{ for: field_id } = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.public_send(event)) %strong diff --git a/app/views/shared/plugins/_index.html.haml b/app/views/shared/plugins/_index.html.haml index fc643c3ecc2..7bcc54e7459 100644 --- a/app/views/shared/plugins/_index.html.haml +++ b/app/views/shared/plugins/_index.html.haml @@ -10,8 +10,8 @@ .col-lg-8.append-bottom-default - if plugins.any? - .panel.panel-default - .panel-heading + .card + .card-header Plugins (#{plugins.count}) %ul.content-list - plugins.each do |file| @@ -19,5 +19,5 @@ .monospace = File.basename(file) - else - %p.light-well.text-center + %p.card.bg-light.text-center No plugins found. diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml index 3d917346f6b..98b258d9275 100644 --- a/app/views/shared/projects/_dropdown.html.haml +++ b/app/views/shared/projects/_dropdown.html.haml @@ -1,8 +1,8 @@ - @sort ||= sort_value_latest_activity .dropdown.js-project-filter-dropdown-wrap - toggle_text = projects_sort_options_hash[@sort] - = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' }) - %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable + = dropdown_toggle(toggle_text, { toggle: 'dropdown', display: 'static' }, { id: 'sort-projects-dropdown' }) + %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable %li.dropdown-header Sort by - projects_sort_options_hash.each do |value, title| diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 0687f6d961d..88f0675f795 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -46,7 +46,7 @@ .controls .prepend-top-0 - if project.archived - %span.prepend-left-10.label.label-warning archived + %span.prepend-left-10.badge.badge-warning archived - if can?(current_user, :read_cross_project) && project.pipeline_status.has_status? %span.prepend-left-10 = render_project_pipeline_status(project.pipeline_status) diff --git a/app/views/shared/runners/_form.html.haml b/app/views/shared/runners/_form.html.haml index 302a543cf12..660123d8b07 100644 --- a/app/views/shared/runners/_form.html.haml +++ b/app/views/shared/runners/_form.html.haml @@ -1,56 +1,56 @@ -= form_for runner, url: runner_form_url, html: { class: 'form-horizontal' } do |f| += form_for runner, url: runner_form_url do |f| = form_errors(runner) - .form-group - = label :active, "Active", class: 'control-label' + .form-group.row + = label :active, "Active", class: 'col-form-label col-sm-2' .col-sm-10 - .checkbox + .form-check = f.check_box :active %span.light Paused Runners don't accept new jobs - .form-group - = label :protected, "Protected", class: 'control-label' + .form-group.row + = label :protected, "Protected", class: 'col-form-label col-sm-2' .col-sm-10 - .checkbox + .form-check = f.check_box :access_level, {}, 'ref_protected', 'not_protected' %span.light This runner will only run on pipelines triggered on protected branches - .form-group - = label :run_untagged, 'Run untagged jobs', class: 'control-label' + .form-group.row + = label :run_untagged, 'Run untagged jobs', class: 'col-form-label col-sm-2' .col-sm-10 - .checkbox + .form-check = f.check_box :run_untagged %span.light Indicates whether this runner can pick jobs without tags - unless runner.group_type? - .form-group - = label :locked, _('Lock to current projects'), class: 'control-label' + .form-group.row + = label :locked, _('Lock to current projects'), class: 'col-form-label col-sm-2' .col-sm-10 - .checkbox + .form-check = f.check_box :locked %span.light= _('When a runner is locked, it cannot be assigned to other projects') - .form-group - = label_tag :token, class: 'control-label' do + .form-group.row + = label_tag :token, class: 'col-form-label col-sm-2' do Token .col-sm-10 = f.text_field :token, class: 'form-control', readonly: true - .form-group - = label_tag :ip_address, class: 'control-label' do + .form-group.row + = label_tag :ip_address, class: 'col-form-label col-sm-2' do IP Address .col-sm-10 = f.text_field :ip_address, class: 'form-control', readonly: true - .form-group - = label_tag :description, class: 'control-label' do + .form-group.row + = label_tag :description, class: 'col-form-label col-sm-2' do Description .col-sm-10 = f.text_field :description, class: 'form-control' - .form-group - = label_tag :maximum_timeout_human_readable, class: 'control-label' do + .form-group.row + = label_tag :maximum_timeout_human_readable, class: 'col-form-label col-sm-2' do Maximum job timeout .col-sm-10 = f.text_field :maximum_timeout_human_readable, class: 'form-control' - .help-block This timeout will take precedence when lower than Project-defined timeout - .form-group - = label_tag :tag_list, class: 'control-label' do + .form-text.text-muted This timeout will take precedence when lower than Project-defined timeout + .form-group.row + = label_tag :tag_list, class: 'col-form-label col-sm-2' do Tags .col-sm-10 = f.text_field :tag_list, value: runner.tag_list.sort.join(', '), class: 'form-control' - .help-block You can setup jobs to only use Runners with specific tags. Separate tags with commas. + .form-text.text-muted You can setup jobs to only use Runners with specific tags. Separate tags with commas. .form-actions = f.submit 'Save changes', class: 'btn btn-save' diff --git a/app/views/shared/runners/_runner_description.html.haml b/app/views/shared/runners/_runner_description.html.haml index 1d59c2f7078..da5c032add5 100644 --- a/app/views/shared/runners/_runner_description.html.haml +++ b/app/views/shared/runners/_runner_description.html.haml @@ -9,8 +9,8 @@ %div %ul %li - %span.label.label-success active + %span.badge.badge-success active = _('- Runner is active and can process any new jobs') %li - %span.label.label-danger paused + %span.badge.badge-danger paused = _('- Runner is paused and will not receive any new jobs') diff --git a/app/views/shared/runners/show.html.haml b/app/views/shared/runners/show.html.haml index 480a224b6d5..e50b7fa68dd 100644 --- a/app/views/shared/runners/show.html.haml +++ b/app/views/shared/runners/show.html.haml @@ -2,7 +2,7 @@ %h3.page-title Runner ##{@runner.id} - .pull-right + .float-right - if @runner.shared? %span.runner-state.runner-state-shared Shared @@ -36,7 +36,7 @@ %td Tags %td - @runner.tag_list.sort.each do |tag| - %span.label.label-primary + %span.badge.badge-primary = tag %tr %td Name @@ -66,6 +66,6 @@ %td Last contact %td - if @runner.contacted_at - #{time_ago_in_words(@runner.contacted_at)} ago + = time_ago_with_tooltip @runner.contacted_at - else Never diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml index 11f0fa7c49f..2132fcbccc5 100644 --- a/app/views/shared/snippets/_blob.html.haml +++ b/app/views/shared/snippets/_blob.html.haml @@ -2,7 +2,7 @@ .js-file-title.file-title-flex-parent = render 'projects/blob/header_content', blob: blob - .file-actions.hidden-xs + .file-actions.d-none.d-sm-block = render 'projects/blob/viewer_switcher', blob: blob .btn-group{ role: "group" }< diff --git a/app/views/shared/snippets/_embed.html.haml b/app/views/shared/snippets/_embed.html.haml index 2d93e51a2d9..36f56fbad1a 100644 --- a/app/views/shared/snippets/_embed.html.haml +++ b/app/views/shared/snippets/_embed.html.haml @@ -15,7 +15,7 @@ %span.logo-text GitLab - .file-actions.hidden-xs + .file-actions.d-none.d-sm-block .btn-group{ role: "group" }< = embedded_snippet_raw_button diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index c75c882a693..858adc8be37 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -2,11 +2,11 @@ = page_specific_javascript_tag('lib/ace.js') .snippet-form-holder - = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input js-quick-submit common-note-form" } do |f| + = form_for @snippet, url: url, html: { class: "snippet-form js-requires-input js-quick-submit common-note-form" } do |f| = form_errors(@snippet) - .form-group - = f.label :title, class: 'control-label' + .form-group.row + = f.label :title, class: 'col-form-label col-sm-2' .col-sm-10 = f.text_field :title, class: 'form-control', required: true, autofocus: true @@ -15,8 +15,8 @@ = render 'shared/visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet .file-editor - .form-group - = f.label :file_name, "File", class: 'control-label' + .form-group.row + = f.label :file_name, "File", class: 'col-form-label col-sm-2' .col-sm-10 .file-holder.snippet .js-file-title.file-title diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 836230ae8ee..ddf54866b99 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -7,7 +7,7 @@ %span.creator Authored = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago') - by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")} + by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "d-none d-sm-inline")} .detail-page-header-actions - if @snippet.project_id? @@ -45,6 +45,6 @@ %strong.embed-toggle-list-item= _("Share") %input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed } .input-group-btn - %button.js-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '#snippet-url-area' } + %button.js-clipboard-btn.snippet-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '.js-snippet-url-area' } = sprite_icon('duplicate', size: 16) .clearfix diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index 3acec88c2e3..e036b21b23f 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -1,13 +1,13 @@ - link_project = local_assigns.fetch(:link_project, false) %li.snippet-row - = image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 hidden-xs", alt: '' + = image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 d-none d-sm-block", alt: '' .title = link_to reliable_snippet_path(snippet) do = snippet.title - if snippet.file_name - %span.snippet-filename.monospace.hidden-xs + %span.snippet-filename.monospace.d-none.d-sm-inline-block = snippet.file_name %ul.controls @@ -28,10 +28,10 @@ = link_to user_snippets_path(snippet.author) do = snippet.author_name - if link_project && snippet.project_id? - %span.hidden-xs + %span.d-none.d-sm-block in = link_to project_path(snippet.project) do = snippet.project.full_name - .pull-right.snippet-updated-at + .float-right.snippet-updated-at %span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')} diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index d36ca032558..0d1c007dd78 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -6,76 +6,76 @@ .form-group = form.label :token, 'Secret Token', class: 'label-light' = form.text_field :token, class: 'form-control', placeholder: '' - %p.help-block + %p.form-text.text-muted Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header. .form-group = form.label :url, 'Trigger', class: 'label-light' %ul.list-unstyled %li - = form.check_box :push_events, class: 'pull-left' + = form.check_box :push_events, class: 'float-left' .prepend-left-20 = form.label :push_events, class: 'list-label' do %strong Push events %p.light This URL will be triggered by a push to the repository %li - = form.check_box :tag_push_events, class: 'pull-left' + = form.check_box :tag_push_events, class: 'float-left' .prepend-left-20 = form.label :tag_push_events, class: 'list-label' do %strong Tag push events %p.light This URL will be triggered when a new tag is pushed to the repository %li - = form.check_box :note_events, class: 'pull-left' + = form.check_box :note_events, class: 'float-left' .prepend-left-20 = form.label :note_events, class: 'list-label' do %strong Comments %p.light This URL will be triggered when someone adds a comment %li - = form.check_box :confidential_note_events, class: 'pull-left' + = form.check_box :confidential_note_events, class: 'float-left' .prepend-left-20 = form.label :confidential_note_events, class: 'list-label' do %strong Confidential Comments %p.light This URL will be triggered when someone adds a comment on a confidential issue %li - = form.check_box :issues_events, class: 'pull-left' + = form.check_box :issues_events, class: 'float-left' .prepend-left-20 = form.label :issues_events, class: 'list-label' do %strong Issues events %p.light This URL will be triggered when an issue is created/updated/merged %li - = form.check_box :confidential_issues_events, class: 'pull-left' + = form.check_box :confidential_issues_events, class: 'float-left' .prepend-left-20 = form.label :confidential_issues_events, class: 'list-label' do %strong Confidential Issues events %p.light This URL will be triggered when a confidential issue is created/updated/merged %li - = form.check_box :merge_requests_events, class: 'pull-left' + = form.check_box :merge_requests_events, class: 'float-left' .prepend-left-20 = form.label :merge_requests_events, class: 'list-label' do %strong Merge request events %p.light This URL will be triggered when a merge request is created/updated/merged %li - = form.check_box :job_events, class: 'pull-left' + = form.check_box :job_events, class: 'float-left' .prepend-left-20 = form.label :job_events, class: 'list-label' do %strong Job events %p.light This URL will be triggered when the job status changes %li - = form.check_box :pipeline_events, class: 'pull-left' + = form.check_box :pipeline_events, class: 'float-left' .prepend-left-20 = form.label :pipeline_events, class: 'list-label' do %strong Pipeline events %p.light This URL will be triggered when the pipeline status changes %li - = form.check_box :wiki_page_events, class: 'pull-left' + = form.check_box :wiki_page_events, class: 'float-left' .prepend-left-20 = form.label :wiki_page_events, class: 'list-label' do %strong Wiki Page events @@ -83,7 +83,7 @@ This URL will be triggered when a wiki page is created/updated .form-group = form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox' - .checkbox + .form-check = form.label :enable_ssl_verification do = form.check_box :enable_ssl_verification %strong Enable SSL verification diff --git a/app/views/shared/web_hooks/_test_button.html.haml b/app/views/shared/web_hooks/_test_button.html.haml index cf1d5e061c6..5ece8b1d4c7 100644 --- a/app/views/shared/web_hooks/_test_button.html.haml +++ b/app/views/shared/web_hooks/_test_button.html.haml @@ -6,7 +6,7 @@ %button.btn{ 'data-toggle' => 'dropdown', class: button_class } Test = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } + %ul.dropdown-menu.dropdown-menu-right{ role: 'menu' } - triggers.each_value do |event| %li = link_to_test_hook(hook, event) diff --git a/app/views/sherlock/file_samples/show.html.haml b/app/views/sherlock/file_samples/show.html.haml index 1a6e2542dc1..7255d352775 100644 --- a/app/views/sherlock/file_samples/show.html.haml +++ b/app/views/sherlock/file_samples/show.html.haml @@ -4,7 +4,7 @@ - header_title t('sherlock.title'), sherlock_transactions_path .row-content-block - .pull-right + .float-right = link_to(sherlock_transaction_path(@transaction), class: 'btn') do %i.fa.fa-arrow-left = t('sherlock.transaction') diff --git a/app/views/sherlock/queries/_backtrace.html.haml b/app/views/sherlock/queries/_backtrace.html.haml index 30e956e5f40..4f5146cefb9 100644 --- a/app/views/sherlock/queries/_backtrace.html.haml +++ b/app/views/sherlock/queries/_backtrace.html.haml @@ -1,6 +1,6 @@ .prepend-top-default - .panel.panel-default - .panel-heading + .card + .card-header %strong = t('sherlock.application_backtrace') %ul.well-list @@ -15,8 +15,8 @@ = t('sherlock.line') = location.line - .panel.panel-default - .panel-heading + .card + .card-header %strong = t('sherlock.full_backtrace') %ul.well-list diff --git a/app/views/sherlock/queries/_general.html.haml b/app/views/sherlock/queries/_general.html.haml index 5a447f791dc..34c0cc4da39 100644 --- a/app/views/sherlock/queries/_general.html.haml +++ b/app/views/sherlock/queries/_general.html.haml @@ -1,6 +1,6 @@ .prepend-top-default - .panel.panel-default - .panel-heading + .card + .card-header %strong = t('sherlock.general') %ul.well-list @@ -23,10 +23,10 @@ = t('sherlock.line') = frame.line - .panel.panel-default - .panel-heading - .pull-right - %button.js-clipboard-trigger.btn.btn-xs{ title: t('sherlock.copy_to_clipboard'), type: :button } + .card + .card-header + .float-right + %button.js-clipboard-trigger.btn.btn-sm{ title: t('sherlock.copy_to_clipboard'), type: :button } %i.fa.fa-clipboard %pre.hidden = @query.formatted_query @@ -38,10 +38,10 @@ :preserve #{highlight("#{@query.id}.sql", @query.formatted_query)} - .panel.panel-default - .panel-heading - .pull-right - %button.js-clipboard-trigger.btn.btn-xs{ title: t('sherlock.copy_to_clipboard'), type: :button } + .card + .card-header + .float-right + %button.js-clipboard-trigger.btn.btn-sm{ title: t('sherlock.copy_to_clipboard'), type: :button } %i.fa.fa-clipboard %pre.hidden = @query.explain diff --git a/app/views/sherlock/queries/show.html.haml b/app/views/sherlock/queries/show.html.haml index c45da6ee9a4..413130a2907 100644 --- a/app/views/sherlock/queries/show.html.haml +++ b/app/views/sherlock/queries/show.html.haml @@ -1,7 +1,7 @@ - page_title t('sherlock.title'), t('sherlock.transaction'), t('sherlock.query') - header_title t('sherlock.title'), sherlock_transactions_path -%ul.nav-links +%ul.nav-links.nav.nav-tabs %li.active %a{ href: "#tab-general", data: { toggle: "tab" } } = t('sherlock.general') @@ -10,7 +10,7 @@ = t('sherlock.backtrace') .row-content-block - .pull-right + .float-right = link_to(sherlock_transaction_path(@transaction), class: 'btn') do %i.fa.fa-arrow-left = t('sherlock.transaction') diff --git a/app/views/sherlock/transactions/_file_samples.html.haml b/app/views/sherlock/transactions/_file_samples.html.haml index 4349c9b7ace..5b3448605f2 100644 --- a/app/views/sherlock/transactions/_file_samples.html.haml +++ b/app/views/sherlock/transactions/_file_samples.html.haml @@ -21,4 +21,4 @@ %td = link_to(t('sherlock.view'), sherlock_transaction_file_sample_path(@transaction, sample), - class: 'btn btn-xs') + class: 'btn btn-sm') diff --git a/app/views/sherlock/transactions/_general.html.haml b/app/views/sherlock/transactions/_general.html.haml index a37fb5d449a..7ec8dde8421 100644 --- a/app/views/sherlock/transactions/_general.html.haml +++ b/app/views/sherlock/transactions/_general.html.haml @@ -1,6 +1,6 @@ .prepend-top-default - .panel.panel-default - .panel-heading + .card + .card-header %strong = t('sherlock.general') %ul.well-list diff --git a/app/views/sherlock/transactions/_queries.html.haml b/app/views/sherlock/transactions/_queries.html.haml index b8d93e9ff45..c1ec4b91bb6 100644 --- a/app/views/sherlock/transactions/_queries.html.haml +++ b/app/views/sherlock/transactions/_queries.html.haml @@ -21,4 +21,4 @@ %td = link_to(t('sherlock.view'), sherlock_transaction_query_path(@transaction, query), - class: 'btn btn-xs') + class: 'btn btn-sm') diff --git a/app/views/sherlock/transactions/index.html.haml b/app/views/sherlock/transactions/index.html.haml index 6ed7e9e21a6..4d9df01ae31 100644 --- a/app/views/sherlock/transactions/index.html.haml +++ b/app/views/sherlock/transactions/index.html.haml @@ -2,7 +2,7 @@ - header_title t('sherlock.title'), sherlock_transactions_path .row-content-block - .pull-right + .float-right = link_to(destroy_all_sherlock_transactions_path, class: 'btn btn-danger', method: :delete) do @@ -37,5 +37,5 @@ %td = time_ago_with_tooltip trans.finished_at %td - = link_to(sherlock_transaction_path(trans), class: 'btn btn-xs') do + = link_to(sherlock_transaction_path(trans), class: 'btn btn-sm') do = t('sherlock.view') diff --git a/app/views/sherlock/transactions/show.html.haml b/app/views/sherlock/transactions/show.html.haml index eab91e8fbe4..565b337d446 100644 --- a/app/views/sherlock/transactions/show.html.haml +++ b/app/views/sherlock/transactions/show.html.haml @@ -1,23 +1,23 @@ - page_title t('sherlock.title'), t('sherlock.transaction') - header_title t('sherlock.title'), sherlock_transactions_path -%ul.nav-links +%ul.nav-links.nav.nav-tabs %li.active %a{ href: "#tab-general", data: { toggle: "tab" } } = t('sherlock.general') %li %a{ href: "#tab-queries", data: { toggle: "tab" } } = t('sherlock.queries') - %span.badge + %span.badge.badge-pill #{@transaction.queries.length} %li %a{ href: "#tab-file-samples", data: { toggle: "tab" } } = t('sherlock.file_samples') - %span.badge + %span.badge.badge-pill #{@transaction.file_samples.length} .row-content-block - .pull-right + .float-right = link_to(sherlock_transactions_path, class: 'btn') do %i.fa.fa-arrow-left = t('sherlock.all_transactions') diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index a7f118d3f7d..ae69d0d07c7 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -1,6 +1,6 @@ - return unless current_user -.hidden-xs +.d-none.d-sm-block - if can?(current_user, :update_personal_snippet, @snippet) = link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do Edit @@ -11,7 +11,7 @@ New snippet - if @snippet.submittable_as_spam_by?(current_user) = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' -.visible-xs-block.dropdown +.d-block.d-sm-none.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } Options = icon('caret-down') diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml index 65aa4fbc757..dc4b0fd9ba0 100644 --- a/app/views/snippets/_snippets_scope_menu.html.haml +++ b/app/views/snippets/_snippets_scope_menu.html.haml @@ -1,11 +1,11 @@ - subject = local_assigns.fetch(:subject, current_user) - include_private = local_assigns.fetch(:include_private, false) -.nav-links.snippet-scope-menu.mobile-separator +.nav-links.snippet-scope-menu.mobile-separator.nav.nav-tabs %li{ class: active_when(params[:scope].nil?) } = link_to subject_snippets_path(subject) do All - %span.badge + %span.badge.badge-pill - if include_private = subject.snippets.count - else @@ -15,17 +15,17 @@ %li{ class: active_when(params[:scope] == "are_private") } = link_to subject_snippets_path(subject, scope: 'are_private') do Private - %span.badge + %span.badge.badge-pill = subject.snippets.are_private.count %li{ class: active_when(params[:scope] == "are_internal") } = link_to subject_snippets_path(subject, scope: 'are_internal') do Internal - %span.badge + %span.badge.badge-pill = subject.snippets.are_internal.count %li{ class: active_when(params[:scope] == "are_public") } = link_to subject_snippets_path(subject, scope: 'are_public') do Public - %span.badge + %span.badge.badge-pill = subject.snippets.are_public.count diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml index 7e4918a6085..9b4a7dbe68d 100644 --- a/app/views/snippets/index.html.haml +++ b/app/views/snippets/index.html.haml @@ -1,12 +1,12 @@ - page_title "By #{@user.name}", "Snippets" %ol.breadcrumb - %li + %li.breadcrumb-item = link_to snippets_path do Snippets - %li + %li.breadcrumb-item = @user.name - .pull-right.hidden-xs + .float-right.d-none.d-sm-block = link_to user_path(@user) do #{@user.name} profile page diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index fb909237b9a..b2ec7166832 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -81,7 +81,7 @@ .scrolling-tabs-container .fade-left= icon('angle-left') .fade-right= icon('angle-right') - %ul.nav-links.user-profile-nav.scrolling-tabs + %ul.nav-links.user-profile-nav.scrolling-tabs.nav.nav-tabs - if profile_tab?(:activity) %li.js-activity-tab = link_to user_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do @@ -107,7 +107,7 @@ .tab-content - if profile_tab?(:activity) #activity.tab-pane - .row-content-block.calender-block.white.second-block.hidden-xs + .row-content-block.calender-block.white.second-block.d-none.d-sm-block .user-calendar{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } } %h4.center.light %i.fa.fa-spinner.fa-spin diff --git a/bin/secpick b/bin/secpick index 76ae231e913..5029fe57cfe 100755 --- a/bin/secpick +++ b/bin/secpick @@ -5,7 +5,6 @@ require 'rainbow/refinement' using Rainbow BRANCH_PREFIX = 'security'.freeze -STABLE_BRANCH_SUFFIX = 'stable'.freeze REMOTE = 'dev'.freeze options = { version: nil, branch: nil, sha: nil } @@ -37,9 +36,9 @@ abort("Missing options. Use #{$0} --help to see the list of options available".r abort("Wrong version format #{options[:version].bold}".red) unless options[:version] =~ /\A\d*\-\d*\Z/ branch = [BRANCH_PREFIX, options[:branch], options[:version]].join('-').freeze -stable_branch = "#{options[:version]}-#{STABLE_BRANCH_SUFFIX}".freeze +stable_branch = "#{BRANCH_PREFIX}-#{options[:version]}".freeze -command = "git checkout #{stable_branch} && git pull #{REMOTE} #{stable_branch} && git checkout -B #{branch} && git cherry-pick #{options[:sha]} && git push #{REMOTE} #{branch}" +command = "git fetch #{REMOTE} #{stable_branch} && git checkout #{stable_branch} && git pull #{REMOTE} #{stable_branch} && git checkout -B #{branch} && git cherry-pick #{options[:sha]} && git push #{REMOTE} #{branch}" _stdin, stdout, stderr = Open3.popen3(command) diff --git a/bin/spinach b/bin/spinach deleted file mode 100755 index eda81c9ed8a..00000000000 --- a/bin/spinach +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env ruby - -# Remove this block when removing rails5? code. -gemfile = %w[1 true].include?(ENV["RAILS5"]) ? "Gemfile.rails5" : "Gemfile" -ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../#{gemfile}", __dir__) - -begin - load File.expand_path('../spring', __FILE__) -rescue LoadError => e - raise unless e.message.include?('spring') -end -require 'bundler/setup' -load Gem.bin_path('spinach', 'spinach') diff --git a/changelogs/unreleased/10244-add-project-ci-cd-settings.yml b/changelogs/unreleased/10244-add-project-ci-cd-settings.yml deleted file mode 100644 index 89f9a0fe03c..00000000000 --- a/changelogs/unreleased/10244-add-project-ci-cd-settings.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Introduce new ProjectCiCdSetting model with group_runners_enabled -merge_request: 18144 -author: -type: performance diff --git a/changelogs/unreleased/16957-issue-due-email.yml b/changelogs/unreleased/16957-issue-due-email.yml deleted file mode 100644 index 83944ca4f73..00000000000 --- a/changelogs/unreleased/16957-issue-due-email.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add cron job to email users on issue due date -merge_request: 17985 -author: Stuart Nelson -type: added diff --git a/changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml b/changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml new file mode 100644 index 00000000000..9287243a7e3 --- /dev/null +++ b/changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml @@ -0,0 +1,5 @@ +--- +title: Fix double-brackets being linkified in wiki markdown +merge_request: 18524 +author: brewingcode +type: fixed diff --git a/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml b/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml new file mode 100644 index 00000000000..a97e8a2b5cc --- /dev/null +++ b/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml @@ -0,0 +1,5 @@ +--- +title: Add API endpoint to render markdown text +merge_request: 18926 +author: "@blackst0ne" +type: added diff --git a/changelogs/unreleased/21677-run-pipeline-word.yml b/changelogs/unreleased/21677-run-pipeline-word.yml deleted file mode 100644 index 9cc280244e4..00000000000 --- a/changelogs/unreleased/21677-run-pipeline-word.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improves wording in new pipeline page -merge_request: -author: -type: other diff --git a/changelogs/unreleased/22647-width-contributors-graphs.yml b/changelogs/unreleased/22647-width-contributors-graphs.yml new file mode 100644 index 00000000000..87be3a25d8a --- /dev/null +++ b/changelogs/unreleased/22647-width-contributors-graphs.yml @@ -0,0 +1,5 @@ +--- +title: Fix width of contributors graphs +merge_request: 18639 +author: Paul Vorbach +type: fixed diff --git a/changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml b/changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml new file mode 100644 index 00000000000..2b4727c5f03 --- /dev/null +++ b/changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml @@ -0,0 +1,6 @@ +--- +title: Fix an issue where the notification email address would be set to an unconfirmed + email address +merge_request: 18474 +author: +type: fixed diff --git a/changelogs/unreleased/25010-collapsed-sidebar-tooltips.yml b/changelogs/unreleased/25010-collapsed-sidebar-tooltips.yml deleted file mode 100644 index 1226fb4eefd..00000000000 --- a/changelogs/unreleased/25010-collapsed-sidebar-tooltips.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve tooltips in collapsed right sidebar -merge_request: 17714 -author: -type: changed diff --git a/changelogs/unreleased/30739-fix-joined-information-on-project-members-page.yml b/changelogs/unreleased/30739-fix-joined-information-on-project-members-page.yml deleted file mode 100644 index f2d5b503661..00000000000 --- a/changelogs/unreleased/30739-fix-joined-information-on-project-members-page.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix `joined` information on project members page -merge_request: 18290 -author: Fabian Schneider -type: fixed diff --git a/changelogs/unreleased/32617-fix-template-selector-menu-visibility.yml b/changelogs/unreleased/32617-fix-template-selector-menu-visibility.yml deleted file mode 100644 index c73be5a901e..00000000000 --- a/changelogs/unreleased/32617-fix-template-selector-menu-visibility.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Fix template selector menu visibility when toggling preview mode in file edit - view -merge_request: 18118 -author: Fabian Schneider -type: fixed diff --git a/changelogs/unreleased/33697-pipelines-json-endpoint.yml b/changelogs/unreleased/33697-pipelines-json-endpoint.yml deleted file mode 100644 index d44e2729415..00000000000 --- a/changelogs/unreleased/33697-pipelines-json-endpoint.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Use VueJS for rendering pipeline stages -merge_request: -author: -type: changed diff --git a/changelogs/unreleased/33697-remove-ujs-action-big-graph.yml b/changelogs/unreleased/33697-remove-ujs-action-big-graph.yml deleted file mode 100644 index 43ce52c1209..00000000000 --- a/changelogs/unreleased/33697-remove-ujs-action-big-graph.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Prevent pipeline actions in dropdown to redirct to a new page -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/34262-show-current-labels-when-editing.yml b/changelogs/unreleased/34262-show-current-labels-when-editing.yml deleted file mode 100644 index d3b15b9ddd1..00000000000 --- a/changelogs/unreleased/34262-show-current-labels-when-editing.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Keep current labels visible when editing them in the sidebar -merge_request: -author: -type: changed diff --git a/changelogs/unreleased/36762-reconcile-project-templates-with-auto-devops.yml b/changelogs/unreleased/36762-reconcile-project-templates-with-auto-devops.yml deleted file mode 100644 index 8169b18f875..00000000000 --- a/changelogs/unreleased/36762-reconcile-project-templates-with-auto-devops.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Reconcile project templates with Auto DevOps -merge_request: 18737 -author: -type: changed diff --git a/changelogs/unreleased/36983-osw-heading-labels-color-fix.yml b/changelogs/unreleased/36983-osw-heading-labels-color-fix.yml deleted file mode 100644 index 082e0544dea..00000000000 --- a/changelogs/unreleased/36983-osw-heading-labels-color-fix.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adjust issue boards list header label text color -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml b/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml new file mode 100644 index 00000000000..9f07fcdfa0b --- /dev/null +++ b/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml @@ -0,0 +1,5 @@ +--- +title: Apply NestingDepth (level 5) (pages/pipelines.scss) +merge_request: 18830 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/39710-search-placeholder-cut-off.yml b/changelogs/unreleased/39710-search-placeholder-cut-off.yml new file mode 100644 index 00000000000..59290768c6a --- /dev/null +++ b/changelogs/unreleased/39710-search-placeholder-cut-off.yml @@ -0,0 +1,5 @@ +--- +title: 'Fixes: Runners search input placeholder is cut off' +merge_request: 19015 +author: Jacopo Beschi @jacopo-beschi +type: fixed diff --git a/changelogs/unreleased/40402-time-estimate-system-notes-can-be-confusing.yml b/changelogs/unreleased/40402-time-estimate-system-notes-can-be-confusing.yml deleted file mode 100644 index e47577f9058..00000000000 --- a/changelogs/unreleased/40402-time-estimate-system-notes-can-be-confusing.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add a comma to the time estimate system notes -merge_request: 18326 -author: -type: changed diff --git a/changelogs/unreleased/40487-axios-pipelines.yml b/changelogs/unreleased/40487-axios-pipelines.yml deleted file mode 100644 index 437d5e87e1a..00000000000 --- a/changelogs/unreleased/40487-axios-pipelines.yml +++ /dev/null @@ -1,4 +0,0 @@ -title: Replace vue resource with axios in pipelines table -merge_request: -author: -type: other
\ No newline at end of file diff --git a/changelogs/unreleased/40855_remove_authentication_in_readonly_issue_api.yml b/changelogs/unreleased/40855_remove_authentication_in_readonly_issue_api.yml new file mode 100644 index 00000000000..58686639594 --- /dev/null +++ b/changelogs/unreleased/40855_remove_authentication_in_readonly_issue_api.yml @@ -0,0 +1,5 @@ +--- +title: made listing and showing public issue apis available without authentication +merge_request: 18638 +author: haseebeqx +type: changed diff --git a/changelogs/unreleased/41059-calculate-artifact-size-more-efficiently.yml b/changelogs/unreleased/41059-calculate-artifact-size-more-efficiently.yml deleted file mode 100644 index e3f94bbf081..00000000000 --- a/changelogs/unreleased/41059-calculate-artifact-size-more-efficiently.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve DB performance of calculating total artifacts size -merge_request: 17839 -author: -type: performance diff --git a/changelogs/unreleased/41082-make-deploykeys-table-more-clearly-structured.yml b/changelogs/unreleased/41082-make-deploykeys-table-more-clearly-structured.yml deleted file mode 100644 index 23704c2b37b..00000000000 --- a/changelogs/unreleased/41082-make-deploykeys-table-more-clearly-structured.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Make project deploy keys table more clearly structured -merge_request: 18279 -author: -type: changed diff --git a/changelogs/unreleased/41748-vertical-misalignment-login-box.yml b/changelogs/unreleased/41748-vertical-misalignment-login-box.yml deleted file mode 100644 index 77a97400323..00000000000 --- a/changelogs/unreleased/41748-vertical-misalignment-login-box.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Refactor CSS to eliminate vertical misalignment of login nav -merge_request: 16275 -author: Takuya Noguchi -type: fixed diff --git a/changelogs/unreleased/41981-allow-group-owner-to-enable-runners-from-subgroups.yml b/changelogs/unreleased/41981-allow-group-owner-to-enable-runners-from-subgroups.yml deleted file mode 100644 index 30481e7af84..00000000000 --- a/changelogs/unreleased/41981-allow-group-owner-to-enable-runners-from-subgroups.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Allow group owner to enable runners from subgroups (#41981)' -merge_request: 18009 -author: -type: fixed diff --git a/changelogs/unreleased/42099-port-push-mirroring-to-ce-ce-port-v-2.yml b/changelogs/unreleased/42099-port-push-mirroring-to-ce-ce-port-v-2.yml deleted file mode 100644 index f23521ea416..00000000000 --- a/changelogs/unreleased/42099-port-push-mirroring-to-ce-ce-port-v-2.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adds push mirrors to GitLab Community Edition -merge_request: 18715 -author: -type: changed diff --git a/changelogs/unreleased/42531-open-invite-404.yml b/changelogs/unreleased/42531-open-invite-404.yml new file mode 100644 index 00000000000..73729f4a929 --- /dev/null +++ b/changelogs/unreleased/42531-open-invite-404.yml @@ -0,0 +1,5 @@ +--- +title: Automatically accepts project/group invite by email after user signup +merge_request: 17634 +author: Jacopo Beschi @jacopo-beschi +type: changed diff --git a/changelogs/unreleased/42543-hide-divergence-graph-on-branches-for-mobile.yml b/changelogs/unreleased/42543-hide-divergence-graph-on-branches-for-mobile.yml deleted file mode 100644 index 7452a264bfd..00000000000 --- a/changelogs/unreleased/42543-hide-divergence-graph-on-branches-for-mobile.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove ahead/behind graphs on project branches on mobile -merge_request: 18415 -author: Takuya Noguchi -type: other diff --git a/changelogs/unreleased/42803-show-new-branch-mr-button.yml b/changelogs/unreleased/42803-show-new-branch-mr-button.yml deleted file mode 100644 index d689ff7f001..00000000000 --- a/changelogs/unreleased/42803-show-new-branch-mr-button.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Show new branch/mr button even when branch exists -merge_request: 17712 -author: Jacopo Beschi @jacopo-beschi -type: added diff --git a/changelogs/unreleased/42889-avoid-return-inside-block.yml b/changelogs/unreleased/42889-avoid-return-inside-block.yml deleted file mode 100644 index e3e1341ddcc..00000000000 --- a/changelogs/unreleased/42889-avoid-return-inside-block.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Rubocop rule to avoid returning from a block -merge_request: 18000 -author: Jacopo Beschi @jacopo-beschi -type: added diff --git a/changelogs/unreleased/42936-improve-ns-factory-avoid-duplicates.yml b/changelogs/unreleased/42936-improve-ns-factory-avoid-duplicates.yml deleted file mode 100644 index 54b523b65a1..00000000000 --- a/changelogs/unreleased/42936-improve-ns-factory-avoid-duplicates.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Fix discussions API setting created_at for notable in a group or notable in - a project in a group with owners -merge_request: 18464 -author: -type: fixed diff --git a/changelogs/unreleased/43111-controller-projects-mergerequestscontroller-index-executes-more-than-100-sql-queries.yml b/changelogs/unreleased/43111-controller-projects-mergerequestscontroller-index-executes-more-than-100-sql-queries.yml deleted file mode 100644 index 120e319acfb..00000000000 --- a/changelogs/unreleased/43111-controller-projects-mergerequestscontroller-index-executes-more-than-100-sql-queries.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Reduce queries on merge requests list page for merge requests from forks -merge_request: 18561 -author: -type: performance diff --git a/changelogs/unreleased/43367-fix-board-long-strings.yml b/changelogs/unreleased/43367-fix-board-long-strings.yml new file mode 100644 index 00000000000..6228741601e --- /dev/null +++ b/changelogs/unreleased/43367-fix-board-long-strings.yml @@ -0,0 +1,5 @@ +--- +title: Fix issue board bug with long strings in titles +merge_request: 18924 +author: +type: fixed diff --git a/changelogs/unreleased/43404-pipelines-commit.yml b/changelogs/unreleased/43404-pipelines-commit.yml deleted file mode 100644 index 0b9a4a6451f..00000000000 --- a/changelogs/unreleased/43404-pipelines-commit.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Breaks commit not found message in pipelines table -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/43466-make-auto-devops-settings-first-class.yml b/changelogs/unreleased/43466-make-auto-devops-settings-first-class.yml deleted file mode 100644 index f5c5415159c..00000000000 --- a/changelogs/unreleased/43466-make-auto-devops-settings-first-class.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Create settings section for autodevops -merge_request: 18321 -author: -type: changed diff --git a/changelogs/unreleased/43469-gcp-account-offer.yml b/changelogs/unreleased/43469-gcp-account-offer.yml deleted file mode 100644 index 323a4b81731..00000000000 --- a/changelogs/unreleased/43469-gcp-account-offer.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add GCP signup offer to cluster index / create pages -merge_request: 18684 -author: -type: added diff --git a/changelogs/unreleased/43557-osw-present-merge-sha-commit.yml b/changelogs/unreleased/43557-osw-present-merge-sha-commit.yml deleted file mode 100644 index a7128f7481e..00000000000 --- a/changelogs/unreleased/43557-osw-present-merge-sha-commit.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Display merge commit SHA in merge widget after merge -merge_request: 18722 -author: -type: added diff --git a/changelogs/unreleased/43567-replace-gke.yml b/changelogs/unreleased/43567-replace-gke.yml deleted file mode 100644 index 8ec79fc3d4d..00000000000 --- a/changelogs/unreleased/43567-replace-gke.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Replace GKE acronym with Google Kubernetes Engine -merge_request: -author: -type: other diff --git a/changelogs/unreleased/43617-mailsig.yml b/changelogs/unreleased/43617-mailsig.yml deleted file mode 100644 index 2c7568e32ca..00000000000 --- a/changelogs/unreleased/43617-mailsig.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Use RFC 3676 mail signature delimiters -merge_request: 17979 -author: Enrico Scholz -type: changed diff --git a/changelogs/unreleased/43673-operations-tab-mvc.yml b/changelogs/unreleased/43673-operations-tab-mvc.yml new file mode 100644 index 00000000000..cd580e7a8d6 --- /dev/null +++ b/changelogs/unreleased/43673-operations-tab-mvc.yml @@ -0,0 +1,5 @@ +--- +title: Move project sidebar sub-entries 'Environments' and 'Kubernetes' from 'CI/CD' to a new entry 'Operations' +merge_request: 18941 +author: +type: changed diff --git a/changelogs/unreleased/44059-specify-variables-when-executing-a-manual-pipeline-from-the-ui.yml b/changelogs/unreleased/44059-specify-variables-when-executing-a-manual-pipeline-from-the-ui.yml deleted file mode 100644 index 8854eeb5fba..00000000000 --- a/changelogs/unreleased/44059-specify-variables-when-executing-a-manual-pipeline-from-the-ui.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Enable specifying variables when executing a manual pipeline -merge_request: 18440 -author: -type: changed diff --git a/changelogs/unreleased/44224-remove-gl.yml b/changelogs/unreleased/44224-remove-gl.yml deleted file mode 100644 index 1c792883f09..00000000000 --- a/changelogs/unreleased/44224-remove-gl.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Removes modal boards store and mixins from global scope -merge_request: -author: -type: other diff --git a/changelogs/unreleased/44296-commit-path.yml b/changelogs/unreleased/44296-commit-path.yml deleted file mode 100644 index b658178f8dc..00000000000 --- a/changelogs/unreleased/44296-commit-path.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Verifiy if pipeline has commit idetails and render information in MR widget - when branch is deleted -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/44447-expose-deploy-token-to-ci-cd.yml b/changelogs/unreleased/44447-expose-deploy-token-to-ci-cd.yml deleted file mode 100644 index d01b797b1ff..00000000000 --- a/changelogs/unreleased/44447-expose-deploy-token-to-ci-cd.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Expose Deploy Token data as environment varialbes on CI/CD jobs -merge_request: 18414 -author: -type: added diff --git a/changelogs/unreleased/44541-fix-file-tree-commit-status-cache.yml b/changelogs/unreleased/44541-fix-file-tree-commit-status-cache.yml deleted file mode 100644 index ff734fe0c05..00000000000 --- a/changelogs/unreleased/44541-fix-file-tree-commit-status-cache.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix pipeline status in branch/tag tree page -merge_request: 17995 -author: -type: fixed diff --git a/changelogs/unreleased/44579-ide-add-pipeline-to-status-bar.yml b/changelogs/unreleased/44579-ide-add-pipeline-to-status-bar.yml new file mode 100644 index 00000000000..21e7c795815 --- /dev/null +++ b/changelogs/unreleased/44579-ide-add-pipeline-to-status-bar.yml @@ -0,0 +1,5 @@ +--- +title: Add pipeline status to the status bar of the Web IDE +merge_request: +author: +type: added diff --git a/changelogs/unreleased/44582-clear-pipeline-status-cache.yml b/changelogs/unreleased/44582-clear-pipeline-status-cache.yml deleted file mode 100644 index 1777f2ffaab..00000000000 --- a/changelogs/unreleased/44582-clear-pipeline-status-cache.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Now `rake cache:clear` will also clear pipeline status cache -merge_request: 18257 -author: -type: fixed diff --git a/changelogs/unreleased/44697-prevue.yml b/changelogs/unreleased/44697-prevue.yml deleted file mode 100644 index 9fdce5869ae..00000000000 --- a/changelogs/unreleased/44697-prevue.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Make toggle markdown preview shortcut only toggle selected field -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/44799-api-naming-issue-scope.yml b/changelogs/unreleased/44799-api-naming-issue-scope.yml new file mode 100644 index 00000000000..75c6ea4cd0d --- /dev/null +++ b/changelogs/unreleased/44799-api-naming-issue-scope.yml @@ -0,0 +1,5 @@ +--- +title: Rename issue scope created-by-me to created_by_me, and assigned-to-me to assigned_to_me +merge_request: 44799 +author: +type: deprecated diff --git a/changelogs/unreleased/44833-ide-clean-up-status-bar.yml b/changelogs/unreleased/44833-ide-clean-up-status-bar.yml deleted file mode 100644 index 4c827e57195..00000000000 --- a/changelogs/unreleased/44833-ide-clean-up-status-bar.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Clean up WebIDE status bar and add useful info -merge_request: -author: -type: changed diff --git a/changelogs/unreleased/44834-ide-remove-branch-from-bottom-bar.yml b/changelogs/unreleased/44834-ide-remove-branch-from-bottom-bar.yml deleted file mode 100644 index d3f838ad362..00000000000 --- a/changelogs/unreleased/44834-ide-remove-branch-from-bottom-bar.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove branch name from the status bar of WebIDE -merge_request: -author: -type: changed diff --git a/changelogs/unreleased/44879.yml b/changelogs/unreleased/44879.yml deleted file mode 100644 index b51e057bb7b..00000000000 --- a/changelogs/unreleased/44879.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add the signature verfication badge to the compare view -merge_request: 18245 -author: Marc Shaw -type: added diff --git a/changelogs/unreleased/44985-fix-protected-branch-delete-modal.yml b/changelogs/unreleased/44985-fix-protected-branch-delete-modal.yml deleted file mode 100644 index 4af2af2a561..00000000000 --- a/changelogs/unreleased/44985-fix-protected-branch-delete-modal.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix confirmation modal for deleting a protected branch -merge_request: 18176 -author: Paul Bonaud @PaulRbR -type: fixed diff --git a/changelogs/unreleased/45065-users-projects-json-sort.yml b/changelogs/unreleased/45065-users-projects-json-sort.yml new file mode 100644 index 00000000000..89a1d7eb36f --- /dev/null +++ b/changelogs/unreleased/45065-users-projects-json-sort.yml @@ -0,0 +1,5 @@ +--- +title: Order UsersController#projects.json by updated_at +merge_request: 18227 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/45159-fix-illustration.yml b/changelogs/unreleased/45159-fix-illustration.yml deleted file mode 100644 index 3b9cb45b916..00000000000 --- a/changelogs/unreleased/45159-fix-illustration.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adds illustration for when job log was erased -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/45397-update-faraday_middleware-to-0-12-2.yml b/changelogs/unreleased/45397-update-faraday_middleware-to-0-12-2.yml deleted file mode 100644 index 3370ec3feba..00000000000 --- a/changelogs/unreleased/45397-update-faraday_middleware-to-0-12-2.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update faraday_middlewar to 0.12.2 -merge_request: 18397 -author: Takuya Noguchi -type: security diff --git a/changelogs/unreleased/45398-fix-rss-button.yml b/changelogs/unreleased/45398-fix-rss-button.yml deleted file mode 100644 index 2c8ee6f0564..00000000000 --- a/changelogs/unreleased/45398-fix-rss-button.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix tabs container styles to make RSS button clickable -merge_request: 18559 -author: -type: fixed diff --git a/changelogs/unreleased/45404-remove-gemnasium-badge-from-project-s-readme-md.yml b/changelogs/unreleased/45404-remove-gemnasium-badge-from-project-s-readme-md.yml new file mode 100644 index 00000000000..af2cb65445c --- /dev/null +++ b/changelogs/unreleased/45404-remove-gemnasium-badge-from-project-s-readme-md.yml @@ -0,0 +1,5 @@ +--- +title: Remove Gemnasium badge from project README.md +merge_request: 19136 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/45436-markdown-is-not-rendering-error-loading-viewer-undefined-method-html_escape.yml b/changelogs/unreleased/45436-markdown-is-not-rendering-error-loading-viewer-undefined-method-html_escape.yml deleted file mode 100644 index 0f1d111ca58..00000000000 --- a/changelogs/unreleased/45436-markdown-is-not-rendering-error-loading-viewer-undefined-method-html_escape.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix undefined `html_escape` method during markdown rendering -merge_request: 18418 -author: -type: fixed diff --git a/changelogs/unreleased/45451-user-deletion-modal-with-same-info-for-delete-user-or-delete-user-and-contributions.yml b/changelogs/unreleased/45451-user-deletion-modal-with-same-info-for-delete-user-or-delete-user-and-contributions.yml deleted file mode 100644 index 707a18745c8..00000000000 --- a/changelogs/unreleased/45451-user-deletion-modal-with-same-info-for-delete-user-or-delete-user-and-contributions.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Correct text and functionality for delete user / delete user and contributions - modal. -merge_request: 18463 -author: Marc Schwede -type: fixed diff --git a/changelogs/unreleased/45481-sane-pages-artifacts.yml b/changelogs/unreleased/45481-sane-pages-artifacts.yml deleted file mode 100644 index b9c68b70012..00000000000 --- a/changelogs/unreleased/45481-sane-pages-artifacts.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Don't automatically remove artifacts for pages jobs after pages:deploy has - run -merge_request: 18628 -author: -type: fixed diff --git a/changelogs/unreleased/45572-members-invitations-scheduled-before-commit.yml b/changelogs/unreleased/45572-members-invitations-scheduled-before-commit.yml deleted file mode 100644 index 7cdea436d47..00000000000 --- a/changelogs/unreleased/45572-members-invitations-scheduled-before-commit.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Ensure member notifications are sent after the member actual creation/update in the DB -merge_request: 18538 -author: -type: fixed diff --git a/changelogs/unreleased/45576-fix-create-project-for-user-endpoint.yml b/changelogs/unreleased/45576-fix-create-project-for-user-endpoint.yml deleted file mode 100644 index 12631c75b44..00000000000 --- a/changelogs/unreleased/45576-fix-create-project-for-user-endpoint.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix project creation for user endpoint when jobs_enabled parameter supplied -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/45584-add-nip-io-domain-suggestion-in-auto-devops.yml b/changelogs/unreleased/45584-add-nip-io-domain-suggestion-in-auto-devops.yml new file mode 100644 index 00000000000..31b4c29e03d --- /dev/null +++ b/changelogs/unreleased/45584-add-nip-io-domain-suggestion-in-auto-devops.yml @@ -0,0 +1,5 @@ +--- +title: Display help text below auto devops domain with nip.io domain name (#45561) +merge_request: 18496 +author: +type: added diff --git a/changelogs/unreleased/45666-project-ci-lint-links.yml b/changelogs/unreleased/45666-project-ci-lint-links.yml deleted file mode 100644 index dbf803c0921..00000000000 --- a/changelogs/unreleased/45666-project-ci-lint-links.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update links to /ci/lint with ones to project ci/lint -merge_request: 18539 -author: Takuya Noguchi -type: fixed diff --git a/changelogs/unreleased/45761-replace-actionview-time_ago_in_words.yml b/changelogs/unreleased/45761-replace-actionview-time_ago_in_words.yml deleted file mode 100644 index adf4db90407..00000000000 --- a/changelogs/unreleased/45761-replace-actionview-time_ago_in_words.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Replace time_ago_in_words with JS-based one -merge_request: 18607 -author: Takuya Noguchi -type: performance diff --git a/changelogs/unreleased/45827-expose_readme_url_in_project_api.yml b/changelogs/unreleased/45827-expose_readme_url_in_project_api.yml new file mode 100644 index 00000000000..7c495cf4dc0 --- /dev/null +++ b/changelogs/unreleased/45827-expose_readme_url_in_project_api.yml @@ -0,0 +1,5 @@ +--- +title: Expose readme url in Project API +merge_request: 18960 +author: Imre Farkas +type: changed diff --git a/changelogs/unreleased/45850-close-mr-checkout-modal-on-escape.yml b/changelogs/unreleased/45850-close-mr-checkout-modal-on-escape.yml new file mode 100644 index 00000000000..c3955c9d8b3 --- /dev/null +++ b/changelogs/unreleased/45850-close-mr-checkout-modal-on-escape.yml @@ -0,0 +1,5 @@ +--- +title: Closes MR check out branch modal with escape +merge_request: Jacopo Beschi @jacopo-beschi +author: 19050 +type: added diff --git a/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml b/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml new file mode 100644 index 00000000000..b9e70bc5679 --- /dev/null +++ b/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml @@ -0,0 +1,5 @@ +--- +title: Fix unscrollable Markdown preview of WebIDE on Firefox +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/46010-add-index-to-runner-type.yml b/changelogs/unreleased/46010-add-index-to-runner-type.yml new file mode 100644 index 00000000000..fb8340e37b2 --- /dev/null +++ b/changelogs/unreleased/46010-add-index-to-runner-type.yml @@ -0,0 +1,5 @@ +--- +title: Add index on runner_type for ci_runners +merge_request: 18897 +author: +type: performance diff --git a/changelogs/unreleased/46049-import-export-import-is-broken-due-to-the-addition-of-a-ci-table.yml b/changelogs/unreleased/46049-import-export-import-is-broken-due-to-the-addition-of-a-ci-table.yml deleted file mode 100644 index 77e4bb50082..00000000000 --- a/changelogs/unreleased/46049-import-export-import-is-broken-due-to-the-addition-of-a-ci-table.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Resolve Import/Export ci_cd_settings error updating the project -merge_request: 46049 -author: -type: fixed diff --git a/changelogs/unreleased/46082-runner-contacted_at-is-not-always-a-time-type.yml b/changelogs/unreleased/46082-runner-contacted_at-is-not-always-a-time-type.yml new file mode 100644 index 00000000000..07f67251b24 --- /dev/null +++ b/changelogs/unreleased/46082-runner-contacted_at-is-not-always-a-time-type.yml @@ -0,0 +1,5 @@ +--- +title: Fix Runner contacted at tooltip cache. +merge_request: 18810 +author: +type: fixed diff --git a/changelogs/unreleased/46193-fix-big-estimate.yml b/changelogs/unreleased/46193-fix-big-estimate.yml new file mode 100644 index 00000000000..d0da0c10033 --- /dev/null +++ b/changelogs/unreleased/46193-fix-big-estimate.yml @@ -0,0 +1,5 @@ +--- +title: Fixes 500 error on /estimate BIG_VALUE +merge_request: 18964 +author: Jacopo Beschi @jacopo-beschi +type: fixed diff --git a/changelogs/unreleased/46210-terms-acceptance-dropdown-menu.yml b/changelogs/unreleased/46210-terms-acceptance-dropdown-menu.yml deleted file mode 100644 index 8a7c549e356..00000000000 --- a/changelogs/unreleased/46210-terms-acceptance-dropdown-menu.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 46210 Display logo and user dropdown on mobile for terms page and fix styling -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/46354-deprecate_gemnasium_service.yml b/changelogs/unreleased/46354-deprecate_gemnasium_service.yml new file mode 100644 index 00000000000..c5ead45d883 --- /dev/null +++ b/changelogs/unreleased/46354-deprecate_gemnasium_service.yml @@ -0,0 +1,5 @@ +--- +title: Deprecate Gemnasium project service +merge_request: 18954 +author: +type: deprecated diff --git a/changelogs/unreleased/46361-does-not-log-failed-sign-in-attempts-when-the-database-is-in-read-only-mode.yml b/changelogs/unreleased/46361-does-not-log-failed-sign-in-attempts-when-the-database-is-in-read-only-mode.yml new file mode 100644 index 00000000000..e4255f11ecf --- /dev/null +++ b/changelogs/unreleased/46361-does-not-log-failed-sign-in-attempts-when-the-database-is-in-read-only-mode.yml @@ -0,0 +1,5 @@ +--- +title: Does not log failed sign-in attempts when the database is in read-only mode +merge_request: 18957 +author: +type: fixed diff --git a/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml b/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml new file mode 100644 index 00000000000..609968f3230 --- /dev/null +++ b/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml @@ -0,0 +1,5 @@ +--- +title: Adds keyboard shortcut `g e` for Environments on Project pages +merge_request: 19002 +author: +type: added diff --git a/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml b/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml new file mode 100644 index 00000000000..48e51b2615e --- /dev/null +++ b/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml @@ -0,0 +1,5 @@ +--- +title: Adds keyboard shortcut `g k` for Kubernetes on Project pages +merge_request: 19002 +author: +type: added diff --git a/changelogs/unreleased/winh-new-mergerequest-branch-picker.yml b/changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml index 401ecd09ef2..9a7cf0d6944 100644 --- a/changelogs/unreleased/winh-new-mergerequest-branch-picker.yml +++ b/changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml @@ -1,5 +1,5 @@ --- -title: Load branches on new merge request page asynchronously -merge_request: 18315 +title: Changes keyboard shortcut of Activity feed to `g v` +merge_request: 19002 author: type: changed diff --git a/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml b/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml new file mode 100644 index 00000000000..f416e35030e --- /dev/null +++ b/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml @@ -0,0 +1,5 @@ +--- +title: Removes outdated `g t` shortcut for TODO in favor of `Shift+T` +merge_request: 19002 +author: +type: removed diff --git a/changelogs/unreleased/46600-fix-gitlab-revision-when-not-in-git-repo.yml b/changelogs/unreleased/46600-fix-gitlab-revision-when-not-in-git-repo.yml new file mode 100644 index 00000000000..1d0b11cfd2a --- /dev/null +++ b/changelogs/unreleased/46600-fix-gitlab-revision-when-not-in-git-repo.yml @@ -0,0 +1,6 @@ +--- +title: Replace Gitlab::REVISION with Gitlab.revision and handle installations without + a .git directory +merge_request: 19125 +author: +type: fixed diff --git a/changelogs/unreleased/4950-unassign-slash-command-preview-fix.yml b/changelogs/unreleased/4950-unassign-slash-command-preview-fix.yml deleted file mode 100644 index 0b8c14ae699..00000000000 --- a/changelogs/unreleased/4950-unassign-slash-command-preview-fix.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix unassign slash command preview -merge_request: 18447 -author: -type: fixed diff --git a/changelogs/unreleased/5750-backport-checksum-git-commanderror-exit-status-128.yml b/changelogs/unreleased/5750-backport-checksum-git-commanderror-exit-status-128.yml deleted file mode 100644 index d778b44c110..00000000000 --- a/changelogs/unreleased/5750-backport-checksum-git-commanderror-exit-status-128.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Raise NoRepository error for non-valid repositories when calculating repository - checksum -merge_request: 18594 -author: -type: fixed diff --git a/changelogs/unreleased/5794-we-should-failover-gracefully-when-we-can-t-connect-to-geo-tracking-database-ce.yml b/changelogs/unreleased/5794-we-should-failover-gracefully-when-we-can-t-connect-to-geo-tracking-database-ce.yml deleted file mode 100644 index 4db0ff4f3a0..00000000000 --- a/changelogs/unreleased/5794-we-should-failover-gracefully-when-we-can-t-connect-to-geo-tracking-database-ce.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: ShaAttribute no longer stops startup if database is missing -merge_request: 18726 -author: -type: fixed diff --git a/changelogs/unreleased/8088_embedded_snippets_support.yml b/changelogs/unreleased/8088_embedded_snippets_support.yml deleted file mode 100644 index 7bd77a69dbd..00000000000 --- a/changelogs/unreleased/8088_embedded_snippets_support.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adds Embedded Snippets Support -merge_request: 15695 -author: haseebeqx -type: added diff --git a/changelogs/unreleased/ab-43706-composite-primary-keys.yml b/changelogs/unreleased/ab-43706-composite-primary-keys.yml new file mode 100644 index 00000000000..b17050a64c8 --- /dev/null +++ b/changelogs/unreleased/ab-43706-composite-primary-keys.yml @@ -0,0 +1,5 @@ +--- +title: Add NOT NULL constraints to project_authorizations. +merge_request: 18980 +author: +type: other diff --git a/changelogs/unreleased/ab-44259-atomic-internal-ids-for-all-models.yml b/changelogs/unreleased/ab-44259-atomic-internal-ids-for-all-models.yml deleted file mode 100644 index e154f7dbc85..00000000000 --- a/changelogs/unreleased/ab-44259-atomic-internal-ids-for-all-models.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Transition to atomic internal ids for all models. -merge_request: 44259 -author: -type: other diff --git a/changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml b/changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml new file mode 100644 index 00000000000..88ef62ebc0e --- /dev/null +++ b/changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml @@ -0,0 +1,5 @@ +--- +title: Increase text limit for GPG keys (mysql only). +merge_request: 19069 +author: +type: other diff --git a/changelogs/unreleased/accessible-text.yml b/changelogs/unreleased/accessible-text.yml deleted file mode 100755 index d39d5a9eb2c..00000000000 --- a/changelogs/unreleased/accessible-text.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Replace "Click" with "Select" to be more inclusive of people with accessibility - requirements -merge_request: 18386 -author: Mark Lapierre -type: other diff --git a/changelogs/unreleased/add-copy-metadata-command.yml b/changelogs/unreleased/add-copy-metadata-command.yml deleted file mode 100644 index 3bf25ae6ce0..00000000000 --- a/changelogs/unreleased/add-copy-metadata-command.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add Copy metadata quick action -merge_request: 16473 -author: Mateusz Bajorski -type: added diff --git a/changelogs/unreleased/add-git-commit-message-predefined-variable.yml b/changelogs/unreleased/add-git-commit-message-predefined-variable.yml deleted file mode 100644 index 183fe69936e..00000000000 --- a/changelogs/unreleased/add-git-commit-message-predefined-variable.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add CI_COMMIT_MESSAGE, CI_COMMIT_TITLE and CI_COMMIT_DESCRIPTION predefined variables -merge_request: 18672 -author: -type: added diff --git a/changelogs/unreleased/add-loading-icon-padding-for-pipeline-environments.yml b/changelogs/unreleased/add-loading-icon-padding-for-pipeline-environments.yml deleted file mode 100644 index a6304418517..00000000000 --- a/changelogs/unreleased/add-loading-icon-padding-for-pipeline-environments.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add loading icon padding for pipeline environments -merge_request: 18631 -author: George Tsiolis -type: fixed diff --git a/changelogs/unreleased/add-padding-to-profile-description.yml b/changelogs/unreleased/add-padding-to-profile-description.yml deleted file mode 100644 index 4628a10eb3f..00000000000 --- a/changelogs/unreleased/add-padding-to-profile-description.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add padding to profile description -merge_request: 18663 -author: George Tsiolis -type: changed diff --git a/changelogs/unreleased/align-project-avatar-on-small-viewports.yml b/changelogs/unreleased/align-project-avatar-on-small-viewports.yml deleted file mode 100644 index 320e7fbc294..00000000000 --- a/changelogs/unreleased/align-project-avatar-on-small-viewports.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Align project avatar on small viewports -merge_request: 18513 -author: George Tsiolis -type: changed diff --git a/changelogs/unreleased/ash-mckenzie-include-sha-with-version.yml b/changelogs/unreleased/ash-mckenzie-include-sha-with-version.yml deleted file mode 100644 index b49c48e0fe1..00000000000 --- a/changelogs/unreleased/ash-mckenzie-include-sha-with-version.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: git SHA is now displayed alongside the GitLab version on the Admin Dashboard -merge_request: -author: -type: added diff --git a/changelogs/unreleased/blackst0ne-add-missing-changelog-type-to-docs.yml b/changelogs/unreleased/blackst0ne-add-missing-changelog-type-to-docs.yml deleted file mode 100644 index f8790fa45aa..00000000000 --- a/changelogs/unreleased/blackst0ne-add-missing-changelog-type-to-docs.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add missing changelog type to docs -merge_request: 18526 -author: "@blackst0ne" -type: other diff --git a/changelogs/unreleased/blackst0ne-remove-spinach.yml b/changelogs/unreleased/blackst0ne-remove-spinach.yml new file mode 100644 index 00000000000..104da257bad --- /dev/null +++ b/changelogs/unreleased/blackst0ne-remove-spinach.yml @@ -0,0 +1,5 @@ +--- +title: Remove Spinach +merge_request: 18869 +author: '@blackst0ne' +type: other diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-builds-artifacts-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-builds-artifacts-feature.yml deleted file mode 100644 index 98c56cf2b57..00000000000 --- a/changelogs/unreleased/blackst0ne-replace-spinach-project-builds-artifacts-feature.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Replace the `project/builds/artifacts.feature` spinach test with an rspec analog' -merge_request: 18729 -author: '@blackst0ne' -type: other diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-branches-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-branches-feature.yml deleted file mode 100644 index bcfba4ae70d..00000000000 --- a/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-branches-feature.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: "Replace the `project/commits/branches.feature` spinach test with an rspec analog" -merge_request: 18302 -author: "@blackst0ne" -type: other diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-comments-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-comments-feature.yml deleted file mode 100644 index e7077f27555..00000000000 --- a/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-comments-feature.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Replace the `project/commits/comments.feature` spinach test with an rspec analog -merge_request: 18356 -author: "@blackst0ne" -type: other diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml new file mode 100644 index 00000000000..2ac43490c26 --- /dev/null +++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml @@ -0,0 +1,5 @@ +--- +title: 'Replace the `project/forked_merge_requests.feature` spinach test with an rspec analog' +merge_request: 18867 +author: '@blackst0ne' +type: other diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-milestones-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-milestones-feature.yml deleted file mode 100644 index 0dcac0a80eb..00000000000 --- a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-milestones-feature.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Replace the `project/issues/milestones.feature` spinach test with an rspec analog -merge_request: 18300 -author: "@blackst0ne" -type: other diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-source-markdown-render-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-source-markdown-render-feature.yml deleted file mode 100644 index 657ed782880..00000000000 --- a/changelogs/unreleased/blackst0ne-replace-spinach-project-source-markdown-render-feature.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Replace the `project/source/markdown_render.feature` spinach test with an rspec analog -merge_request: 18525 -author: "@blackst0ne" -type: other diff --git a/changelogs/unreleased/break-issue-title-for-board-card-title-and-issueable-header-text.yml b/changelogs/unreleased/break-issue-title-for-board-card-title-and-issueable-header-text.yml deleted file mode 100644 index 7acde223962..00000000000 --- a/changelogs/unreleased/break-issue-title-for-board-card-title-and-issueable-header-text.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Break issue title for board card title and issuable header text -merge_request: 18674 -author: George Tsiolis -type: changed diff --git a/changelogs/unreleased/bvl-enforce-terms.yml b/changelogs/unreleased/bvl-enforce-terms.yml deleted file mode 100644 index 1bb1ecdf623..00000000000 --- a/changelogs/unreleased/bvl-enforce-terms.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow admins to enforce accepting Terms of Service on an instance -merge_request: 18570 -author: -type: added diff --git a/changelogs/unreleased/bvl-restrict-api-git-for-terms.yml b/changelogs/unreleased/bvl-restrict-api-git-for-terms.yml deleted file mode 100644 index 49cd04b065b..00000000000 --- a/changelogs/unreleased/bvl-restrict-api-git-for-terms.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Block access to the API & git for users that did not accept enforced Terms - of Service -merge_request: 18816 -author: -type: other diff --git a/changelogs/unreleased/bvl-shared-groups-on-group-page.yml b/changelogs/unreleased/bvl-shared-groups-on-group-page.yml deleted file mode 100644 index 6c0703fd138..00000000000 --- a/changelogs/unreleased/bvl-shared-groups-on-group-page.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Show shared projects on group page -merge_request: 18390 -author: -type: fixed diff --git a/changelogs/unreleased/bw-add-console-message.yml b/changelogs/unreleased/bw-add-console-message.yml deleted file mode 100644 index 7994f7caced..00000000000 --- a/changelogs/unreleased/bw-add-console-message.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Output some useful information when running the rails console -merge_request: 18697 -author: -type: added diff --git a/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml b/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml new file mode 100644 index 00000000000..a0d787e570e --- /dev/null +++ b/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml @@ -0,0 +1,3 @@ +title: Add anchor for incoming email regex +merge_request: !18917 +type: added diff --git a/changelogs/unreleased/change-font-for-tables-inside-diff-discussions.yml b/changelogs/unreleased/change-font-for-tables-inside-diff-discussions.yml deleted file mode 100644 index f2810fab208..00000000000 --- a/changelogs/unreleased/change-font-for-tables-inside-diff-discussions.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Change font for tables inside diff discussions -merge_request: 18660 -author: George Tsiolis -type: changed diff --git a/changelogs/unreleased/collapsed-contextual-nav-update.yml b/changelogs/unreleased/collapsed-contextual-nav-update.yml new file mode 100644 index 00000000000..31a32a9e1e9 --- /dev/null +++ b/changelogs/unreleased/collapsed-contextual-nav-update.yml @@ -0,0 +1,6 @@ +--- +title: Renamed 'Overview' to 'Project' in collapsed contextual navigation at a project + level +merge_request: 18996 +author: Constance Okoghenun +type: fixed diff --git a/changelogs/unreleased/commit-branch-tag-icon-update.yml b/changelogs/unreleased/commit-branch-tag-icon-update.yml new file mode 100644 index 00000000000..136b7cbf0f4 --- /dev/null +++ b/changelogs/unreleased/commit-branch-tag-icon-update.yml @@ -0,0 +1,5 @@ +--- +title: Updated icons for branch and tag names in commit details +merge_request: 18953 +author: Constance Okoghenun +type: changed diff --git a/changelogs/unreleased/create-live-trace-only-if-job-is-complete.yml b/changelogs/unreleased/create-live-trace-only-if-job-is-complete.yml new file mode 100644 index 00000000000..f32c70cf884 --- /dev/null +++ b/changelogs/unreleased/create-live-trace-only-if-job-is-complete.yml @@ -0,0 +1,5 @@ +--- +title: Forbid to patch traces for finished jobs +merge_request: 18969 +author: +type: fixed diff --git a/changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml b/changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml deleted file mode 100644 index 3e1ac7b795d..00000000000 --- a/changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add deprecation message to dynamic milestone pages -merge_request: 17505 -author: -type: added diff --git a/changelogs/unreleased/dm-webhook-catch-blocked-url-exception.yml b/changelogs/unreleased/dm-webhook-catch-blocked-url-exception.yml deleted file mode 100644 index c4f8f7acca6..00000000000 --- a/changelogs/unreleased/dm-webhook-catch-blocked-url-exception.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Ensure web hook 'blocked URL' errors are stored in web hook logs and properly - surfaced to the user -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/docs-use-variables-deploy-policy-for-staging-and-production.yml b/changelogs/unreleased/docs-use-variables-deploy-policy-for-staging-and-production.yml deleted file mode 100644 index aa23a89a175..00000000000 --- a/changelogs/unreleased/docs-use-variables-deploy-policy-for-staging-and-production.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Add documentation about how to use variables to define deploy policies for - staging/production environments -merge_request: 18675 -author: -type: other diff --git a/changelogs/unreleased/dz-add-2fa-filter-admin-api.yml b/changelogs/unreleased/dz-add-2fa-filter-admin-api.yml deleted file mode 100644 index df479e69380..00000000000 --- a/changelogs/unreleased/dz-add-2fa-filter-admin-api.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add 2FA filter to users API for admins only -merge_request: 18503 -author: -type: changed diff --git a/changelogs/unreleased/feature-add-language-in-repository-to-api.yml b/changelogs/unreleased/feature-add-language-in-repository-to-api.yml deleted file mode 100644 index bd9bd377212..00000000000 --- a/changelogs/unreleased/feature-add-language-in-repository-to-api.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'API: add languages of project GET /projects/:id/languages' -merge_request: 17770 -author: Roger Rüttimann -type: added diff --git a/changelogs/unreleased/feature-add_target_to_tags.yml b/changelogs/unreleased/feature-add_target_to_tags.yml deleted file mode 100644 index 75816005e1f..00000000000 --- a/changelogs/unreleased/feature-add_target_to_tags.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Expose the target commit ID through the tag API -merge_request: -author: -type: added diff --git a/changelogs/unreleased/feature-display-active-sessions.yml b/changelogs/unreleased/feature-display-active-sessions.yml deleted file mode 100644 index 14cfa66953e..00000000000 --- a/changelogs/unreleased/feature-display-active-sessions.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Display active sessions and allow the user to revoke any of it -merge_request: 17867 -author: Alexis Reigel -type: added diff --git a/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml b/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml new file mode 100644 index 00000000000..d77c5b42497 --- /dev/null +++ b/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml @@ -0,0 +1,5 @@ +--- +title: Add support for variables expression pattern matching syntax +merge_request: 18902 +author: +type: added diff --git a/changelogs/unreleased/feature-runner-per-group.yml b/changelogs/unreleased/feature-runner-per-group.yml deleted file mode 100644 index 162a5fae0a4..00000000000 --- a/changelogs/unreleased/feature-runner-per-group.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow group masters to configure runners for groups -merge_request: 9646 -author: Alexis Reigel -type: added diff --git a/changelogs/unreleased/feature-show-only-groups-user-is-member-of-in-dashboard.yml b/changelogs/unreleased/feature-show-only-groups-user-is-member-of-in-dashboard.yml deleted file mode 100644 index 6e2273ed9af..00000000000 --- a/changelogs/unreleased/feature-show-only-groups-user-is-member-of-in-dashboard.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: For group dashboard, we no longer show groups which the visitor is not a member of (this applies to admins and auditors) -merge_request: 17884 -author: Roger Rüttimann -type: changed diff --git a/changelogs/unreleased/fix-assignee-name-wrap.yml b/changelogs/unreleased/fix-assignee-name-wrap.yml new file mode 100644 index 00000000000..2407288785f --- /dev/null +++ b/changelogs/unreleased/fix-assignee-name-wrap.yml @@ -0,0 +1,5 @@ +--- +title: Wrapping problem on the issues page has been fixed +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix-devops-remove-beta.yml b/changelogs/unreleased/fix-devops-remove-beta.yml new file mode 100644 index 00000000000..326003eb956 --- /dev/null +++ b/changelogs/unreleased/fix-devops-remove-beta.yml @@ -0,0 +1,5 @@ +--- +title: Removed "(Beta)" from "Auto DevOps" messages +merge_request: 18759 +author: +type: changed diff --git a/changelogs/unreleased/fix-gb-add-pipeline-builds-foreign-key.yml b/changelogs/unreleased/fix-gb-add-pipeline-builds-foreign-key.yml deleted file mode 100644 index bded7bb7cc4..00000000000 --- a/changelogs/unreleased/fix-gb-add-pipeline-builds-foreign-key.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add database foreign key constraint between pipelines and build -merge_request: 18822 -author: -type: fixed diff --git a/changelogs/unreleased/fix-gb-exclude-persisted-variables-from-environment-name.yml b/changelogs/unreleased/fix-gb-exclude-persisted-variables-from-environment-name.yml new file mode 100644 index 00000000000..92426832f30 --- /dev/null +++ b/changelogs/unreleased/fix-gb-exclude-persisted-variables-from-environment-name.yml @@ -0,0 +1,5 @@ +--- +title: Exclude CI_PIPELINE_ID from variables supported in dynamic environment name +merge_request: 19032 +author: +type: fixed diff --git a/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml b/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml new file mode 100644 index 00000000000..c2a788d6ad0 --- /dev/null +++ b/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml @@ -0,0 +1,5 @@ +--- +title: Do not allow to trigger manual actions that were skipped +merge_request: 18985 +author: +type: fixed diff --git a/changelogs/unreleased/fix-inconsistent-protected-branch-pill-baseline.yml b/changelogs/unreleased/fix-inconsistent-protected-branch-pill-baseline.yml deleted file mode 100644 index fb6dffaf226..00000000000 --- a/changelogs/unreleased/fix-inconsistent-protected-branch-pill-baseline.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fixed inconsistent protected branch pill baseline -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/fix-project-mirror-data-schema.yml b/changelogs/unreleased/fix-project-mirror-data-schema.yml deleted file mode 100644 index 107f1fe3b9c..00000000000 --- a/changelogs/unreleased/fix-project-mirror-data-schema.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Fixes database inconsistencies between Community and Enterprise Edition on - import state -merge_request: 18811 -author: -type: fixed diff --git a/changelogs/unreleased/fix-shortcut-close-screen-with-key.yml b/changelogs/unreleased/fix-shortcut-close-screen-with-key.yml deleted file mode 100644 index 9cbc856a075..00000000000 --- a/changelogs/unreleased/fix-shortcut-close-screen-with-key.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix close keyboard shortcuts dialog using the keyboard shortcut -merge_request: 18783 -author: Lars Greiss -type: fixed diff --git a/changelogs/unreleased/fix-unverified-hover-state.yml b/changelogs/unreleased/fix-unverified-hover-state.yml new file mode 100644 index 00000000000..003138f9821 --- /dev/null +++ b/changelogs/unreleased/fix-unverified-hover-state.yml @@ -0,0 +1,5 @@ +--- +title: Unverified hover state color changed to black +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix-wiki-find-page-invalid-encoding.yml b/changelogs/unreleased/fix-wiki-find-page-invalid-encoding.yml deleted file mode 100644 index f003bef8671..00000000000 --- a/changelogs/unreleased/fix-wiki-find-page-invalid-encoding.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix finding wiki pages when they have invalidly-encoded content -merge_request: 18856 -author: -type: fixed diff --git a/changelogs/unreleased/fj-42354-custom-hooks-not-triggered-by-UI-wiki-edit.yml b/changelogs/unreleased/fj-42354-custom-hooks-not-triggered-by-UI-wiki-edit.yml deleted file mode 100644 index 9fe458aba4a..00000000000 --- a/changelogs/unreleased/fj-42354-custom-hooks-not-triggered-by-UI-wiki-edit.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Triggering custom hooks by Wiki UI edit -merge_request: 18251 -author: -type: fixed diff --git a/changelogs/unreleased/fj-45057-improve-ssrf-documentation.yml b/changelogs/unreleased/fj-45057-improve-ssrf-documentation.yml deleted file mode 100644 index b923f442b26..00000000000 --- a/changelogs/unreleased/fj-45057-improve-ssrf-documentation.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Added Webhook SSRF prevention to documentation -merge_request: 18532 -author: -type: other diff --git a/changelogs/unreleased/fj-46411-fix-badge-api-endpoint-route-with-relative-url.yml b/changelogs/unreleased/fj-46411-fix-badge-api-endpoint-route-with-relative-url.yml new file mode 100644 index 00000000000..bd4e5a43352 --- /dev/null +++ b/changelogs/unreleased/fj-46411-fix-badge-api-endpoint-route-with-relative-url.yml @@ -0,0 +1,5 @@ +--- +title: Fixed badge api endpoint route when relative url is set +merge_request: 19004 +author: +type: fixed diff --git a/changelogs/unreleased/fj-46459-fix-expose-url-when-base-url-set.yml b/changelogs/unreleased/fj-46459-fix-expose-url-when-base-url-set.yml new file mode 100644 index 00000000000..16b0ee06898 --- /dev/null +++ b/changelogs/unreleased/fj-46459-fix-expose-url-when-base-url-set.yml @@ -0,0 +1,5 @@ +--- +title: Fixed bug where generated api urls didn't add the base url if set +merge_request: 19003 +author: +type: fixed diff --git a/changelogs/unreleased/fj-change-gollum-gems-to-custom-ones.yml b/changelogs/unreleased/fj-change-gollum-gems-to-custom-ones.yml deleted file mode 100644 index 53883e8d907..00000000000 --- a/changelogs/unreleased/fj-change-gollum-gems-to-custom-ones.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Replacing gollum libraries for gitlab custom libs -merge_request: 18343 -author: -type: other diff --git a/changelogs/unreleased/fl-pipelines-details-axios.yml b/changelogs/unreleased/fl-pipelines-details-axios.yml deleted file mode 100644 index 0b72e54cba3..00000000000 --- a/changelogs/unreleased/fl-pipelines-details-axios.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Replace vue resource with axios for pipelines details page -merge_request: -author: -type: other diff --git a/changelogs/unreleased/helm-add-alpine-mirrors.yml b/changelogs/unreleased/helm-add-alpine-mirrors.yml deleted file mode 100644 index 656c4f911d0..00000000000 --- a/changelogs/unreleased/helm-add-alpine-mirrors.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Increase cluster applications installer availability using alpine linux mirrors -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/ide-file-finder.yml b/changelogs/unreleased/ide-file-finder.yml deleted file mode 100644 index 252dd3a30c4..00000000000 --- a/changelogs/unreleased/ide-file-finder.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Added fuzzy file finder to web IDE -merge_request: -author: -type: added diff --git a/changelogs/unreleased/ide-improve-commit-panel.yml b/changelogs/unreleased/ide-improve-commit-panel.yml deleted file mode 100644 index f6237db3039..00000000000 --- a/changelogs/unreleased/ide-improve-commit-panel.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve interaction on WebIDE commit panel -merge_request: -author: -type: changed diff --git a/changelogs/unreleased/improve-commit-message-body-rendering.yml b/changelogs/unreleased/improve-commit-message-body-rendering.yml deleted file mode 100644 index 3fb9b03725e..00000000000 --- a/changelogs/unreleased/improve-commit-message-body-rendering.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve commit message body rendering and fix responsive compare panels -merge_request: 18725 -author: Constance Okoghenun -type: changed diff --git a/changelogs/unreleased/improve-jobs-queuing-time-metric.yml b/changelogs/unreleased/improve-jobs-queuing-time-metric.yml deleted file mode 100644 index cee8b8523fd..00000000000 --- a/changelogs/unreleased/improve-jobs-queuing-time-metric.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Partition job_queue_duration_seconds with jobs_running_for_project -merge_request: 17730 -author: -type: changed diff --git a/changelogs/unreleased/improve-quick-actions-summary-preview.yml b/changelogs/unreleased/improve-quick-actions-summary-preview.yml deleted file mode 100644 index bc75c169ad7..00000000000 --- a/changelogs/unreleased/improve-quick-actions-summary-preview.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve quick actions summary preview -merge_request: 18659 -author: George Tsiolis -type: changed diff --git a/changelogs/unreleased/increase-new-issue-metadata-form-margin.yml b/changelogs/unreleased/increase-new-issue-metadata-form-margin.yml deleted file mode 100644 index a7196f67969..00000000000 --- a/changelogs/unreleased/increase-new-issue-metadata-form-margin.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Increase new issue metadata form margin -merge_request: 18630 -author: George Tsiolis -type: fixed diff --git a/changelogs/unreleased/inform-the-user-when-there-are-no-project-import-options-available.yml b/changelogs/unreleased/inform-the-user-when-there-are-no-project-import-options-available.yml deleted file mode 100644 index c14f21fc644..00000000000 --- a/changelogs/unreleased/inform-the-user-when-there-are-no-project-import-options-available.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Inform the user when there are no project import options available -merge_request: 18716 -author: George Tsiolis -type: changed diff --git a/changelogs/unreleased/issue-25256.yml b/changelogs/unreleased/issue-25256.yml new file mode 100644 index 00000000000..e981b5f9a8f --- /dev/null +++ b/changelogs/unreleased/issue-25256.yml @@ -0,0 +1,5 @@ +--- +title: Don't trim incoming emails that create new issues +merge_request: +author: Cameron Crockett +type: fixed diff --git a/changelogs/unreleased/issue_38418.yml b/changelogs/unreleased/issue_38418.yml new file mode 100644 index 00000000000..79452b27e4b --- /dev/null +++ b/changelogs/unreleased/issue_38418.yml @@ -0,0 +1,5 @@ +--- +title: Fix issue count on sidebar +merge_request: +author: +type: other diff --git a/changelogs/unreleased/issue_43660.yml b/changelogs/unreleased/issue_43660.yml deleted file mode 100644 index d83c0ebcbb5..00000000000 --- a/changelogs/unreleased/issue_43660.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Enable prometheus monitoring by default -merge_request: -author: -type: other diff --git a/changelogs/unreleased/jivl-add-dot-system-notes.yml b/changelogs/unreleased/jivl-add-dot-system-notes.yml new file mode 100644 index 00000000000..2246bab1464 --- /dev/null +++ b/changelogs/unreleased/jivl-add-dot-system-notes.yml @@ -0,0 +1,5 @@ +--- +title: Add dot to separate system notes content +merge_request: 18864 +author: +type: changed diff --git a/changelogs/unreleased/jivl-refactor-activity-calendar.yml b/changelogs/unreleased/jivl-refactor-activity-calendar.yml deleted file mode 100644 index 0702ede4af9..00000000000 --- a/changelogs/unreleased/jivl-refactor-activity-calendar.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Refactored activity calendar -merge_request: 18469 -author: Enrico Scholz -type: changed diff --git a/changelogs/unreleased/jprovazn-commit-notes-api.yml b/changelogs/unreleased/jprovazn-commit-notes-api.yml deleted file mode 100644 index 4665d800ccf..00000000000 --- a/changelogs/unreleased/jprovazn-commit-notes-api.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add discussion API for merge requests and commits -merge_request: -author: -type: added diff --git a/changelogs/unreleased/jprovazn-fix-resolvable.yml b/changelogs/unreleased/jprovazn-fix-resolvable.yml new file mode 100644 index 00000000000..e17c409e290 --- /dev/null +++ b/changelogs/unreleased/jprovazn-fix-resolvable.yml @@ -0,0 +1,5 @@ +--- +title: Fix resolvable check if note's commit could not be found. +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/jprovazn-generic-error.yml b/changelogs/unreleased/jprovazn-generic-error.yml deleted file mode 100644 index ced3b84fe02..00000000000 --- a/changelogs/unreleased/jprovazn-generic-error.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Display only generic message on merge error to avoid exposing any potentially - sensitive or user unfriendly backend messages. -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/jprovazn-null-byte.yml b/changelogs/unreleased/jprovazn-null-byte.yml new file mode 100644 index 00000000000..4c4760ac412 --- /dev/null +++ b/changelogs/unreleased/jprovazn-null-byte.yml @@ -0,0 +1,5 @@ +--- +title: Fix filename matching when processing file or blob search results +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/jprovazn-pipeline-policy.yml b/changelogs/unreleased/jprovazn-pipeline-policy.yml new file mode 100644 index 00000000000..2997c6c8667 --- /dev/null +++ b/changelogs/unreleased/jprovazn-pipeline-policy.yml @@ -0,0 +1,6 @@ +--- +title: Allow maintainers to retry pipelines on forked projects (if allowed in merge + request) +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/jprovazn-remote-upload-destroy.yml b/changelogs/unreleased/jprovazn-remote-upload-destroy.yml new file mode 100644 index 00000000000..22e55920fa3 --- /dev/null +++ b/changelogs/unreleased/jprovazn-remote-upload-destroy.yml @@ -0,0 +1,5 @@ +--- +title: Fix deletion of Object Store uploads +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/jr-33320-lfs-settings-interface.yml b/changelogs/unreleased/jr-33320-lfs-settings-interface.yml deleted file mode 100644 index b39308f5474..00000000000 --- a/changelogs/unreleased/jr-33320-lfs-settings-interface.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Show group and project LFS settings in the interface to Owners and Masters -merge_request: 18562 -author: -type: changed diff --git a/changelogs/unreleased/jr-46209-web-ide-copy.yml b/changelogs/unreleased/jr-46209-web-ide-copy.yml deleted file mode 100644 index 87ccae6ced0..00000000000 --- a/changelogs/unreleased/jr-46209-web-ide-copy.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix outdated Web IDE welcome copy -merge_request: 18861 -author: -type: fixed diff --git a/changelogs/unreleased/jr-web-ide-shortcuts.yml b/changelogs/unreleased/jr-web-ide-shortcuts.yml new file mode 100644 index 00000000000..a895eab432a --- /dev/null +++ b/changelogs/unreleased/jr-web-ide-shortcuts.yml @@ -0,0 +1,5 @@ +--- +title: Add shortcuts to Web IDE docs and modal +merge_request: 19044 +author: +type: changed diff --git a/changelogs/unreleased/jramsay-44880-filter-pipelines-by-sha.yml b/changelogs/unreleased/jramsay-44880-filter-pipelines-by-sha.yml deleted file mode 100644 index 3654aa28ff4..00000000000 --- a/changelogs/unreleased/jramsay-44880-filter-pipelines-by-sha.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add sha filter to pipelines list API -merge_request: 18125 -author: -type: changed diff --git a/changelogs/unreleased/label-links-on-project-transfer.yml b/changelogs/unreleased/label-links-on-project-transfer.yml deleted file mode 100644 index fabedb77cb3..00000000000 --- a/changelogs/unreleased/label-links-on-project-transfer.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix label links update on project transfer -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/live-trace-v2-efficient-destroy-all.yml b/changelogs/unreleased/live-trace-v2-efficient-destroy-all.yml deleted file mode 100644 index ab22739b73d..00000000000 --- a/changelogs/unreleased/live-trace-v2-efficient-destroy-all.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Destroy build_chunks efficiently with FastDestroyAll module -merge_request: 18575 -author: -type: performance diff --git a/changelogs/unreleased/live-trace-v2.yml b/changelogs/unreleased/live-trace-v2.yml deleted file mode 100644 index 875a66bc565..00000000000 --- a/changelogs/unreleased/live-trace-v2.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: New CI Job live-trace architecture -merge_request: 18169 -author: -type: changed diff --git a/changelogs/unreleased/memoize-database-version.yml b/changelogs/unreleased/memoize-database-version.yml new file mode 100644 index 00000000000..575348a53a1 --- /dev/null +++ b/changelogs/unreleased/memoize-database-version.yml @@ -0,0 +1,5 @@ +--- +title: Memoize Gitlab::Database.version +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/migrate-restore-repo-to-gitaly.yml b/changelogs/unreleased/migrate-restore-repo-to-gitaly.yml new file mode 100644 index 00000000000..59f375de20e --- /dev/null +++ b/changelogs/unreleased/migrate-restore-repo-to-gitaly.yml @@ -0,0 +1,5 @@ +--- +title: Support restoring repositories into gitaly +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/move-board-blank-state-vue-component.yml b/changelogs/unreleased/move-board-blank-state-vue-component.yml deleted file mode 100644 index 0a278a8c009..00000000000 --- a/changelogs/unreleased/move-board-blank-state-vue-component.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Move BoardBlankState vue component -merge_request: 17666 -author: George Tsiolis -type: performance diff --git a/changelogs/unreleased/move-disussion-actions-to-the-right.yml b/changelogs/unreleased/move-disussion-actions-to-the-right.yml new file mode 100644 index 00000000000..b79c6f36585 --- /dev/null +++ b/changelogs/unreleased/move-disussion-actions-to-the-right.yml @@ -0,0 +1,5 @@ +--- +title: Move discussion actions to the right for small viewports +merge_request: 18476 +author: George Tsiolis +type: changed diff --git a/changelogs/unreleased/move-estimate-only-pane-vue-component.yml b/changelogs/unreleased/move-estimate-only-pane-vue-component.yml deleted file mode 100644 index b6c538f70b3..00000000000 --- a/changelogs/unreleased/move-estimate-only-pane-vue-component.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Move TimeTrackingEstimateOnlyPane vue component -merge_request: 18318 -author: George Tsiolis -type: performance diff --git a/changelogs/unreleased/move-help-state-vue-component.yml b/changelogs/unreleased/move-help-state-vue-component.yml deleted file mode 100644 index 6108368cde0..00000000000 --- a/changelogs/unreleased/move-help-state-vue-component.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Move TimeTrackingHelpState vue component -merge_request: 18319 -author: George Tsiolis -type: performance diff --git a/changelogs/unreleased/move-notification-service-calls-to-sidekiq.yml b/changelogs/unreleased/move-notification-service-calls-to-sidekiq.yml deleted file mode 100644 index b2517884d3c..00000000000 --- a/changelogs/unreleased/move-notification-service-calls-to-sidekiq.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Compute notification recipients in background jobs -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/move-pipeline-failed-vue-component.yml b/changelogs/unreleased/move-pipeline-failed-vue-component.yml deleted file mode 100644 index 38d42134876..00000000000 --- a/changelogs/unreleased/move-pipeline-failed-vue-component.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Move PipelineFailed vue component -merge_request: 18277 -author: George Tsiolis -type: performance diff --git a/changelogs/unreleased/move-time-tracking-spent-only-pane-vue-component.yml b/changelogs/unreleased/move-time-tracking-spent-only-pane-vue-component.yml deleted file mode 100644 index d2db0df5a04..00000000000 --- a/changelogs/unreleased/move-time-tracking-spent-only-pane-vue-component.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Move TimeTrackingSpentOnlyPane vue component -merge_request: 18710 -author: George Tsiolis -type: performance diff --git a/changelogs/unreleased/mr-conflict-notification.yml b/changelogs/unreleased/mr-conflict-notification.yml new file mode 100644 index 00000000000..d3d5f1fc373 --- /dev/null +++ b/changelogs/unreleased/mr-conflict-notification.yml @@ -0,0 +1,5 @@ +--- +title: When MR becomes unmergeable, notify and create todo for author and merge user +merge_request: 18042 +author: +type: added diff --git a/changelogs/unreleased/osw-use-cached-highlighted-content-for-discussions.yml b/changelogs/unreleased/osw-use-cached-highlighted-content-for-discussions.yml deleted file mode 100644 index 03a11a3038a..00000000000 --- a/changelogs/unreleased/osw-use-cached-highlighted-content-for-discussions.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Use persisted diff data instead fetching Git on discussions -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/performance-gb-improve-pipeline-creation-service.yml b/changelogs/unreleased/performance-gb-improve-pipeline-creation-service.yml deleted file mode 100644 index bd308f37bec..00000000000 --- a/changelogs/unreleased/performance-gb-improve-pipeline-creation-service.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve performance of a service responsible for creating a pipeline -merge_request: 18582 -author: -type: performance diff --git a/changelogs/unreleased/pipelines-index-performance.yml b/changelogs/unreleased/pipelines-index-performance.yml new file mode 100644 index 00000000000..928c2ddab72 --- /dev/null +++ b/changelogs/unreleased/pipelines-index-performance.yml @@ -0,0 +1,5 @@ +--- +title: Improve performance of project pipelines pages +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/rd-44635-error-500-when-clicking-ghost-user-or-gitlab-support-bot-in-ui.yml b/changelogs/unreleased/rd-44635-error-500-when-clicking-ghost-user-or-gitlab-support-bot-in-ui.yml deleted file mode 100644 index a08a75ceda6..00000000000 --- a/changelogs/unreleased/rd-44635-error-500-when-clicking-ghost-user-or-gitlab-support-bot-in-ui.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix missing namespace for some internal users -merge_request: 18357 -author: -type: fixed diff --git a/changelogs/unreleased/rd-45502-uploading-project-export-with-lfs-file-locks-fails.yml b/changelogs/unreleased/rd-45502-uploading-project-export-with-lfs-file-locks-fails.yml deleted file mode 100644 index e3266dda629..00000000000 --- a/changelogs/unreleased/rd-45502-uploading-project-export-with-lfs-file-locks-fails.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Don't include lfs_file_locks data in export bundle -merge_request: 18495 -author: -type: fixed diff --git a/changelogs/unreleased/refactor-move-mr-widget-ready-to-merge-vue-component.yml b/changelogs/unreleased/refactor-move-mr-widget-ready-to-merge-vue-component.yml deleted file mode 100644 index 90192fae030..00000000000 --- a/changelogs/unreleased/refactor-move-mr-widget-ready-to-merge-vue-component.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Move ReadyToMerge vue component -merge_request: 17545 -author: George Tsiolis -type: performance diff --git a/changelogs/unreleased/refactor-move-mr-widget-wip-vue-component.yml b/changelogs/unreleased/refactor-move-mr-widget-wip-vue-component.yml deleted file mode 100644 index 0f045431aae..00000000000 --- a/changelogs/unreleased/refactor-move-mr-widget-wip-vue-component.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Move WorkInProgress vue component -merge_request: 17536 -author: George Tsiolis -type: performance diff --git a/changelogs/unreleased/refactor-move-no-tracking-pane-vue-component.yml b/changelogs/unreleased/refactor-move-no-tracking-pane-vue-component.yml deleted file mode 100644 index 4bb088a1e58..00000000000 --- a/changelogs/unreleased/refactor-move-no-tracking-pane-vue-component.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Move TimeTrackingNoTrackingPane vue component -merge_request: 18676 -author: George Tsiolis -type: performance diff --git a/changelogs/unreleased/refactor-move-sidebar-time-tracking-vue-component.yml b/changelogs/unreleased/refactor-move-sidebar-time-tracking-vue-component.yml deleted file mode 100644 index 4f578bfcf26..00000000000 --- a/changelogs/unreleased/refactor-move-sidebar-time-tracking-vue-component.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Move SidebarTimeTracking vue component -merge_request: 18677 -author: George Tsiolis -type: performance diff --git a/changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml b/changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml new file mode 100644 index 00000000000..b8b2762a21d --- /dev/null +++ b/changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml @@ -0,0 +1,5 @@ +--- +title: Move SquashBeforeMerge vue component +merge_request: 18813 +author: George Tsiolis +type: performance diff --git a/changelogs/unreleased/registry-ux-improvements-remove-clipboard-prefix.yml b/changelogs/unreleased/registry-ux-improvements-remove-clipboard-prefix.yml new file mode 100644 index 00000000000..ddf7f51aa5e --- /dev/null +++ b/changelogs/unreleased/registry-ux-improvements-remove-clipboard-prefix.yml @@ -0,0 +1,5 @@ +--- +title: Remove docker pull prefix from registry clipboard feature +merge_request: 18933 +author: Lars Greiss +type: changed diff --git a/changelogs/unreleased/rename-merge-request-widget-author-component.yml b/changelogs/unreleased/rename-merge-request-widget-author-component.yml new file mode 100644 index 00000000000..15e6eafd826 --- /dev/null +++ b/changelogs/unreleased/rename-merge-request-widget-author-component.yml @@ -0,0 +1,5 @@ +--- +title: Rename merge request widget author component +merge_request: 19079 +author: George Tsiolis +type: changed diff --git a/changelogs/unreleased/rename-overview-project-sidenav.yml b/changelogs/unreleased/rename-overview-project-sidenav.yml deleted file mode 100644 index 3632ef25c00..00000000000 --- a/changelogs/unreleased/rename-overview-project-sidenav.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Renamed Overview to Project in the contextual navigation at a project level -merge_request: 18295 -author: Constance Okoghenun -type: changed diff --git a/changelogs/unreleased/restore-label-underline-color.yml b/changelogs/unreleased/restore-label-underline-color.yml deleted file mode 100644 index 2e24ece5c36..00000000000 --- a/changelogs/unreleased/restore-label-underline-color.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Restore label underline color -merge_request: 18407 -author: George Tsiolis -type: fixed diff --git a/changelogs/unreleased/restore-size-and-position-for-fork-icon.yml b/changelogs/unreleased/restore-size-and-position-for-fork-icon.yml deleted file mode 100644 index dd8dad0b17d..00000000000 --- a/changelogs/unreleased/restore-size-and-position-for-fork-icon.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix size and position for fork icon -merge_request: 18449 -author: George Tsiolis -type: changed diff --git a/changelogs/unreleased/revert-discussion-counter-height.yml b/changelogs/unreleased/revert-discussion-counter-height.yml deleted file mode 100644 index 331ff997009..00000000000 --- a/changelogs/unreleased/revert-discussion-counter-height.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Revert discussion counter height -merge_request: 18656 -author: George Tsiolis -type: changed diff --git a/changelogs/unreleased/security-45689-fix-archive-cache-bug.yml b/changelogs/unreleased/security-45689-fix-archive-cache-bug.yml deleted file mode 100644 index 0103a7fc430..00000000000 --- a/changelogs/unreleased/security-45689-fix-archive-cache-bug.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Serve archive requests with the correct file in all cases -merge_request: -author: -type: security diff --git a/changelogs/unreleased/security_issue_42029.yml b/changelogs/unreleased/security_issue_42029.yml deleted file mode 100644 index 0772e33f930..00000000000 --- a/changelogs/unreleased/security_issue_42029.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Sanitizes user name to avoid XSS attacks -merge_request: -author: -type: security diff --git a/changelogs/unreleased/sh-bump-lograge.yml b/changelogs/unreleased/sh-bump-lograge.yml deleted file mode 100644 index 65b15153a35..00000000000 --- a/changelogs/unreleased/sh-bump-lograge.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Bump lograge to 0.10.0 and remove monkey patch -merge_request: -author: -type: other diff --git a/changelogs/unreleased/sh-enforce-unique-and-not-null-project-ids-project-features.yml b/changelogs/unreleased/sh-enforce-unique-and-not-null-project-ids-project-features.yml new file mode 100644 index 00000000000..aae42b66c84 --- /dev/null +++ b/changelogs/unreleased/sh-enforce-unique-and-not-null-project-ids-project-features.yml @@ -0,0 +1,5 @@ +--- +title: Add a unique and not null constraint on the project_features.project_id column +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-backup-specific-rake-task.yml b/changelogs/unreleased/sh-fix-backup-specific-rake-task.yml new file mode 100644 index 00000000000..71b121710ee --- /dev/null +++ b/changelogs/unreleased/sh-fix-backup-specific-rake-task.yml @@ -0,0 +1,5 @@ +--- +title: Fix backup creation and restore for specific Rake tasks +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml b/changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml new file mode 100644 index 00000000000..3c51aaae896 --- /dev/null +++ b/changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml @@ -0,0 +1,5 @@ +--- +title: Fix cross-origin errors when attempting to download JavaScript attachments +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-grape-logging-status-code.yml b/changelogs/unreleased/sh-fix-grape-logging-status-code.yml new file mode 100644 index 00000000000..aabf9a84bfb --- /dev/null +++ b/changelogs/unreleased/sh-fix-grape-logging-status-code.yml @@ -0,0 +1,5 @@ +--- +title: Fix api_json.log not always reporting the right HTTP status code +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-move-delete-groups-api-async.yml b/changelogs/unreleased/sh-move-delete-groups-api-async.yml new file mode 100644 index 00000000000..1b200cac5c5 --- /dev/null +++ b/changelogs/unreleased/sh-move-delete-groups-api-async.yml @@ -0,0 +1,5 @@ +--- +title: Move API group deletion to Sidekiq +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/show-group-id-in-group-settings.yml b/changelogs/unreleased/show-group-id-in-group-settings.yml deleted file mode 100644 index b975fe8c71d..00000000000 --- a/changelogs/unreleased/show-group-id-in-group-settings.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Show group id in group settings -merge_request: 18482 -author: George Tsiolis -type: added diff --git a/changelogs/unreleased/show-runners-description-on-jobs-page.yml b/changelogs/unreleased/show-runners-description-on-jobs-page.yml deleted file mode 100644 index d9414a3d021..00000000000 --- a/changelogs/unreleased/show-runners-description-on-jobs-page.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Show Runner's description on job's page -merge_request: 17321 -author: -type: added diff --git a/changelogs/unreleased/tc-repo-verify-mails.yml b/changelogs/unreleased/tc-repo-verify-mails.yml deleted file mode 100644 index b4d3c4b1596..00000000000 --- a/changelogs/unreleased/tc-repo-verify-mails.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Small improvements to repository checks -merge_request: 18484 -author: -type: changed diff --git a/changelogs/unreleased/tz-upgrade-underscore.yml b/changelogs/unreleased/tz-upgrade-underscore.yml deleted file mode 100644 index 5dfd8154ecd..00000000000 --- a/changelogs/unreleased/tz-upgrade-underscore.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Upgrade underscore.js to 1.9.0 -merge_request: 18578 -author: -type: other diff --git a/changelogs/unreleased/unresolved-discussions-vue-component-i18n-and-tests.yml b/changelogs/unreleased/unresolved-discussions-vue-component-i18n-and-tests.yml deleted file mode 100644 index d99a9c93c0b..00000000000 --- a/changelogs/unreleased/unresolved-discussions-vue-component-i18n-and-tests.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add i18n and update specs for UnresolvedDiscussions vue component -merge_request: 17866 -author: George Tsiolis -type: performance diff --git a/changelogs/unreleased/update-environment-item-action-buttons-icons.yml b/changelogs/unreleased/update-environment-item-action-buttons-icons.yml deleted file mode 100644 index 7f06022be3e..00000000000 --- a/changelogs/unreleased/update-environment-item-action-buttons-icons.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update environment item action buttons icons -merge_request: 18632 -author: George Tsiolis -type: changed diff --git a/changelogs/unreleased/update-timeline-icon-for-description-edit.yml b/changelogs/unreleased/update-timeline-icon-for-description-edit.yml deleted file mode 100644 index 560db00e503..00000000000 --- a/changelogs/unreleased/update-timeline-icon-for-description-edit.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update timeline icon for description edit -merge_request: 18633 -author: George Tsiolis -type: changed diff --git a/changelogs/unreleased/update-wiki-modal.yml b/changelogs/unreleased/update-wiki-modal.yml new file mode 100644 index 00000000000..00f2fc4f181 --- /dev/null +++ b/changelogs/unreleased/update-wiki-modal.yml @@ -0,0 +1,5 @@ +--- +title: New design for wiki page deletion confirmation +merge_request: 18712 +author: Constance Okoghenun +type: added diff --git a/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml b/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml new file mode 100644 index 00000000000..098e4b1d5fa --- /dev/null +++ b/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml @@ -0,0 +1,5 @@ +--- +title: "Use case in-sensitive ordering by name for dashboard" +merge_request: 18553 +author: "@vedharish" +type: fixed diff --git a/changelogs/unreleased/winh-dashboard-any-milestone.yml b/changelogs/unreleased/winh-dashboard-any-milestone.yml deleted file mode 100644 index 49eecd3da2b..00000000000 --- a/changelogs/unreleased/winh-dashboard-any-milestone.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Reset milestone filter when clicking "Any Milestone" in dashboard -merge_request: 18531 -author: -type: fixed diff --git a/changelogs/unreleased/winh-dropdown-entry-unlocking.yml b/changelogs/unreleased/winh-dropdown-entry-unlocking.yml deleted file mode 100644 index fc669af1f57..00000000000 --- a/changelogs/unreleased/winh-dropdown-entry-unlocking.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove green background from unlock button in admin area -merge_request: 18288 -author: -type: changed diff --git a/changelogs/unreleased/winh-make-it-right-now.yml b/changelogs/unreleased/winh-make-it-right-now.yml new file mode 100644 index 00000000000..7b386c0b332 --- /dev/null +++ b/changelogs/unreleased/winh-make-it-right-now.yml @@ -0,0 +1,5 @@ +--- +title: Use "right now" for short time periods +merge_request: 19095 +author: +type: changed diff --git a/changelogs/unreleased/winh-new-merge-request-encoding.yml b/changelogs/unreleased/winh-new-merge-request-encoding.yml new file mode 100644 index 00000000000..f797657e660 --- /dev/null +++ b/changelogs/unreleased/winh-new-merge-request-encoding.yml @@ -0,0 +1,5 @@ +--- +title: Fix encoding of branch names on compare and new merge request page +merge_request: 19143 +author: +type: fixed diff --git a/changelogs/unreleased/zj-add-branch-mandatory.yml b/changelogs/unreleased/zj-add-branch-mandatory.yml new file mode 100644 index 00000000000..82712ce842d --- /dev/null +++ b/changelogs/unreleased/zj-add-branch-mandatory.yml @@ -0,0 +1,5 @@ +--- +title: Adding branches through the WebUI is handled by Gitaly +merge_request: +author: +type: other diff --git a/changelogs/unreleased/zj-branch-containing-sha-opt-out.yml b/changelogs/unreleased/zj-branch-containing-sha-opt-out.yml deleted file mode 100644 index 3d11ee588ae..00000000000 --- a/changelogs/unreleased/zj-branch-containing-sha-opt-out.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Detecting branchnames containing a commit uses Gitaly by default -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/zj-calculate-checksum-mandator.yml b/changelogs/unreleased/zj-calculate-checksum-mandator.yml new file mode 100644 index 00000000000..83315a3c5dd --- /dev/null +++ b/changelogs/unreleased/zj-calculate-checksum-mandator.yml @@ -0,0 +1,5 @@ +--- +title: Remove shellout implementation for Repository checksums +merge_request: +author: +type: other diff --git a/changelogs/unreleased/zj-find-license-opt-out.yml b/changelogs/unreleased/zj-find-license-opt-out.yml deleted file mode 100644 index be2656601a9..00000000000 --- a/changelogs/unreleased/zj-find-license-opt-out.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Detect repository license on Gitaly by default -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/zj-fork-opt-out.yml b/changelogs/unreleased/zj-fork-opt-out.yml deleted file mode 100644 index 56bf6b8b0f6..00000000000 --- a/changelogs/unreleased/zj-fork-opt-out.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Gitaly handles repository forks by default -merge_request: -author: -type: other diff --git a/changelogs/unreleased/zj-namespace-service-mandatory.yml b/changelogs/unreleased/zj-namespace-service-mandatory.yml deleted file mode 100644 index d890741c51b..00000000000 --- a/changelogs/unreleased/zj-namespace-service-mandatory.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Finish NamespaceService migration to Gitaly -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/zj-ref-contains-sha-mandatory.yml b/changelogs/unreleased/zj-ref-contains-sha-mandatory.yml new file mode 100644 index 00000000000..61bdce43c0e --- /dev/null +++ b/changelogs/unreleased/zj-ref-contains-sha-mandatory.yml @@ -0,0 +1,5 @@ +--- +title: Refs containting sha checks are done by Gitaly +merge_request: +author: +type: other diff --git a/changelogs/unreleased/zj-ref-exists-opt-out.yml b/changelogs/unreleased/zj-ref-exists-opt-out.yml deleted file mode 100644 index cdffecb0d0a..00000000000 --- a/changelogs/unreleased/zj-ref-exists-opt-out.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Check if a ref exists is done by Gitaly by default -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/zj-repo-checksum-opt-out.yml b/changelogs/unreleased/zj-repo-checksum-opt-out.yml deleted file mode 100644 index 98dfedf7475..00000000000 --- a/changelogs/unreleased/zj-repo-checksum-opt-out.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Compute Gitlab::Git::Repository#checksum on Gitaly by default -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/zj-repository-exist-mandatory.yml b/changelogs/unreleased/zj-repository-exist-mandatory.yml deleted file mode 100644 index 7d83446e90f..00000000000 --- a/changelogs/unreleased/zj-repository-exist-mandatory.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Repository#exists? is always executed through Gitaly -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/zj-tag-containing-sha-opt-out.yml b/changelogs/unreleased/zj-tag-containing-sha-opt-out.yml deleted file mode 100644 index 4774c7811d1..00000000000 --- a/changelogs/unreleased/zj-tag-containing-sha-opt-out.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Detecting tags containing a commit uses Gitaly by default -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/zj-workhorse-archive-mandatory.yml b/changelogs/unreleased/zj-workhorse-archive-mandatory.yml new file mode 100644 index 00000000000..3a4a351a2b9 --- /dev/null +++ b/changelogs/unreleased/zj-workhorse-archive-mandatory.yml @@ -0,0 +1,5 @@ +--- +title: Workhorse will use Gitaly to create archives +merge_request: +author: +type: other diff --git a/changelogs/unreleased/zj-workhorse-commit-patch-diff.yml b/changelogs/unreleased/zj-workhorse-commit-patch-diff.yml new file mode 100644 index 00000000000..bce68692d98 --- /dev/null +++ b/changelogs/unreleased/zj-workhorse-commit-patch-diff.yml @@ -0,0 +1,5 @@ +--- +title: Workhorse to send raw diff and patch for commits +merge_request: +author: +type: other diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 5248bd858a0..dd36700964a 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -470,6 +470,3 @@ if Rails.env.test? Settings.gitlab['default_can_create_group'] = true Settings.gitlab['default_can_create_team'] = false end - -# Force a refresh of application settings at startup -ApplicationSetting.expire diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb index eb7959e4da6..146c4b1e024 100644 --- a/config/initializers/7_prometheus_metrics.rb +++ b/config/initializers/7_prometheus_metrics.rb @@ -25,7 +25,7 @@ Sidekiq.configure_server do |config| end end -if Gitlab::Metrics.prometheus_metrics_enabled? +if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled? unless Sidekiq.server? Gitlab::Metrics::Samplers::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start end diff --git a/config/initializers/console_message.rb b/config/initializers/console_message.rb index 536ab337d85..2c46a25f365 100644 --- a/config/initializers/console_message.rb +++ b/config/initializers/console_message.rb @@ -3,7 +3,7 @@ if defined?(Rails::Console) # note that this will not print out when using `spring` justify = 15 puts "-------------------------------------------------------------------------------------" - puts " Gitlab:".ljust(justify) + "#{Gitlab::VERSION} (#{Gitlab::REVISION})" + puts " Gitlab:".ljust(justify) + "#{Gitlab::VERSION} (#{Gitlab.revision})" puts " Gitlab Shell:".ljust(justify) + Gitlab::Shell.new.version puts " #{Gitlab::Database.adapter_name}:".ljust(justify) + Gitlab::Database.version puts "-------------------------------------------------------------------------------------" diff --git a/config/initializers/deprecations.rb b/config/initializers/deprecations.rb index 2476ea9e38a..14616e726d9 100644 --- a/config/initializers/deprecations.rb +++ b/config/initializers/deprecations.rb @@ -1,5 +1,9 @@ -deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab') +if Rails.env.development? || ENV['GITLAB_LEGACY_PATH_LOG_MESSAGE'] + deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab') + + deprecator.behavior = -> (message, callstack) { + Rails.logger.warn("#{message}: #{callstack[1..20].join}") + } -if Gitlab.dev_env_or_com? ActiveSupport::Deprecation.deprecate_methods(Gitlab::GitalyClient::StorageSettings, :legacy_disk_path, deprecator: deprecator) end diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb index c60ad535fd5..80cab7273e5 100644 --- a/config/initializers/flipper.rb +++ b/config/initializers/flipper.rb @@ -1,22 +1 @@ -require 'flipper/adapters/active_record' -require 'flipper/adapters/active_support_cache_store' - -Flipper.configure do |config| - config.default do - adapter = Flipper::Adapters::ActiveRecord.new( - feature_class: Feature::FlipperFeature, gate_class: Feature::FlipperGate) - cached_adapter = Flipper::Adapters::ActiveSupportCacheStore.new( - adapter, - Rails.cache, - expires_in: 1.hour) - - Flipper.new(cached_adapter) - end -end - Feature.register_feature_groups - -unless Rails.env.test? - require 'flipper/middleware/memoizer' - Rails.application.config.middleware.use Flipper::Middleware::Memoizer -end diff --git a/config/initializers/kubeclient.rb b/config/initializers/kubeclient.rb new file mode 100644 index 00000000000..7f115268b37 --- /dev/null +++ b/config/initializers/kubeclient.rb @@ -0,0 +1,16 @@ +class Kubeclient::Client + # We need to monkey patch this method until + # https://github.com/abonas/kubeclient/pull/323 is merged + def proxy_url(kind, name, port, namespace = '') + discover unless @discovered + entity_name_plural = + if %w[services pods nodes].include?(kind.to_s) + kind.to_s + else + @entities[kind.to_s].resource_name + end + + ns_prefix = build_namespace_prefix(namespace) + rest_client["#{ns_prefix}#{entity_name_plural}/#{name}:#{port}/proxy"].url + end +end diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index b2da3b3dc19..17d09293205 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -13,7 +13,7 @@ def configure_sentry if sentry_enabled Raven.configure do |config| config.dsn = Gitlab::CurrentSettings.current_application_settings.sentry_dsn - config.release = Gitlab::REVISION + config.release = Gitlab.revision # Sanitize fields based on those sanitized from Rails. config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s) diff --git a/config/webpack.config.js b/config/webpack.config.js index 5096f35e808..d6ab32972fb 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -2,20 +2,25 @@ const fs = require('fs'); const path = require('path'); const glob = require('glob'); const webpack = require('webpack'); +const VueLoaderPlugin = require('vue-loader/lib/plugin'); const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin; const CopyWebpackPlugin = require('copy-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const ROOT_PATH = path.resolve(__dirname, '..'); +const CACHE_PATH = path.join(ROOT_PATH, 'tmp/cache'); const IS_PRODUCTION = process.env.NODE_ENV === 'production'; const IS_DEV_SERVER = process.argv.join(' ').indexOf('webpack-dev-server') !== -1; const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost'; const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808; -const DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false'; +const DEV_SERVER_LIVERELOAD = IS_DEV_SERVER && process.env.DEV_SERVER_LIVERELOAD !== 'false'; const WEBPACK_REPORT = process.env.WEBPACK_REPORT; const NO_COMPRESSION = process.env.NO_COMPRESSION; +const VUE_VERSION = require('vue/package.json').version; +const VUE_LOADER_VERSION = require('vue-loader/package.json').version; + let autoEntriesCount = 0; let watchAutoEntries = []; const defaultEntries = ['./main']; @@ -61,7 +66,7 @@ function generateEntries() { return Object.assign(manualEntries, autoEntries); } -const config = { +module.exports = { mode: IS_PRODUCTION ? 'production' : 'development', context: path.join(ROOT_PATH, 'app/assets/javascripts'), @@ -76,46 +81,43 @@ const config = { globalObject: 'this', // allow HMR and web workers to play nice }, - optimization: { - nodeEnv: false, - runtimeChunk: 'single', - splitChunks: { - maxInitialRequests: 4, - cacheGroups: { - default: false, - common: () => ({ - priority: 20, - name: 'main', - chunks: 'initial', - minChunks: autoEntriesCount * 0.9, - }), - vendors: { - priority: 10, - chunks: 'async', - test: /[\\/](node_modules|vendor[\\/]assets[\\/]javascripts)[\\/]/, - }, - commons: { - chunks: 'all', - minChunks: 2, - reuseExistingChunk: true, - }, - }, + resolve: { + extensions: ['.js'], + alias: { + '~': path.join(ROOT_PATH, 'app/assets/javascripts'), + emojis: path.join(ROOT_PATH, 'fixtures/emojis'), + empty_states: path.join(ROOT_PATH, 'app/views/shared/empty_states'), + icons: path.join(ROOT_PATH, 'app/views/shared/icons'), + images: path.join(ROOT_PATH, 'app/assets/images'), + vendor: path.join(ROOT_PATH, 'vendor/assets/javascripts'), + vue$: 'vue/dist/vue.esm.js', + spec: path.join(ROOT_PATH, 'spec/javascripts'), }, }, module: { + strictExportPresence: true, rules: [ { test: /\.js$/, - exclude: /(node_modules|vendor\/assets)/, + exclude: path => /node_modules|vendor[\\/]assets/.test(path) && !/\.vue\.js/.test(path), loader: 'babel-loader', options: { - cacheDirectory: path.join(ROOT_PATH, 'tmp/cache/babel-loader'), + cacheDirectory: path.join(CACHE_PATH, 'babel-loader'), }, }, { test: /\.vue$/, loader: 'vue-loader', + options: { + cacheDirectory: path.join(CACHE_PATH, 'vue-loader'), + cacheIdentifier: [ + process.env.NODE_ENV || 'development', + webpack.version, + VUE_VERSION, + VUE_LOADER_VERSION, + ].join('|'), + }, }, { test: /\.svg$/, @@ -147,10 +149,9 @@ const config = { }, }, { - test: /katex.min.css$/, - include: /node_modules\/katex\/dist/, + test: /.css$/, use: [ - { loader: 'style-loader' }, + 'vue-style-loader', { loader: 'css-loader', options: { @@ -175,9 +176,34 @@ const config = { ], }, ], - noParse: [/monaco-editor\/\w+\/vs\//], - strictExportPresence: true, + }, + + optimization: { + nodeEnv: false, + runtimeChunk: 'single', + splitChunks: { + maxInitialRequests: 4, + cacheGroups: { + default: false, + common: () => ({ + priority: 20, + name: 'main', + chunks: 'initial', + minChunks: autoEntriesCount * 0.9, + }), + vendors: { + priority: 10, + chunks: 'async', + test: /[\\/](node_modules|vendor[\\/]assets[\\/]javascripts)[\\/]/, + }, + commons: { + chunks: 'all', + minChunks: 2, + reuseExistingChunk: true, + }, + }, + }, }, plugins: [ @@ -197,6 +223,9 @@ const config = { }, }), + // enable vue-loader to use existing loader rules for other module types + new VueLoaderPlugin(), + // prevent pikaday from including moment.js new webpack.IgnorePlugin(/moment/, /pikaday/), @@ -228,87 +257,66 @@ const config = { }, }, ]), - ], - resolve: { - extensions: ['.js'], - alias: { - '~': path.join(ROOT_PATH, 'app/assets/javascripts'), - emojis: path.join(ROOT_PATH, 'fixtures/emojis'), - empty_states: path.join(ROOT_PATH, 'app/views/shared/empty_states'), - icons: path.join(ROOT_PATH, 'app/views/shared/icons'), - images: path.join(ROOT_PATH, 'app/assets/images'), - vendor: path.join(ROOT_PATH, 'vendor/assets/javascripts'), - vue$: 'vue/dist/vue.esm.js', - spec: path.join(ROOT_PATH, 'spec/javascripts'), - }, - }, + // compression can require a lot of compute time and is disabled in CI + IS_PRODUCTION && !NO_COMPRESSION && new CompressionPlugin(), - // sqljs requires fs - node: { - fs: 'empty', - }, -}; + // WatchForChangesPlugin + // TODO: publish this as a separate plugin + IS_DEV_SERVER && { + apply(compiler) { + compiler.hooks.emit.tapAsync('WatchForChangesPlugin', (compilation, callback) => { + const missingDeps = Array.from(compilation.missingDependencies); + const nodeModulesPath = path.join(ROOT_PATH, 'node_modules'); + const hasMissingNodeModules = missingDeps.some( + file => file.indexOf(nodeModulesPath) !== -1 + ); -if (IS_PRODUCTION) { - config.devtool = 'source-map'; + // watch for changes to missing node_modules + if (hasMissingNodeModules) compilation.contextDependencies.add(nodeModulesPath); - // compression can require a lot of compute time and is disabled in CI - if (!NO_COMPRESSION) { - config.plugins.push(new CompressionPlugin()); - } -} + // watch for changes to automatic entrypoints + watchAutoEntries.forEach(watchPath => compilation.contextDependencies.add(watchPath)); + + // report our auto-generated bundle count + console.log( + `${autoEntriesCount} entries from '/pages' automatically added to webpack output.` + ); + + callback(); + }); + }, + }, + + // enable HMR only in webpack-dev-server + DEV_SERVER_LIVERELOAD && new webpack.HotModuleReplacementPlugin(), -if (IS_DEV_SERVER) { - config.devtool = 'cheap-module-eval-source-map'; - config.devServer = { + // optionally generate webpack bundle analysis + WEBPACK_REPORT && + new BundleAnalyzerPlugin({ + analyzerMode: 'static', + generateStatsFile: true, + openAnalyzer: false, + reportFilename: path.join(ROOT_PATH, 'webpack-report/index.html'), + statsFilename: path.join(ROOT_PATH, 'webpack-report/stats.json'), + }), + ].filter(Boolean), + + devServer: { host: DEV_SERVER_HOST, port: DEV_SERVER_PORT, disableHostCheck: true, - headers: { 'Access-Control-Allow-Origin': '*' }, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': '*', + }, stats: 'errors-only', hot: DEV_SERVER_LIVERELOAD, inline: DEV_SERVER_LIVERELOAD, - }; - config.plugins.push({ - apply(compiler) { - compiler.hooks.emit.tapAsync('WatchForChangesPlugin', (compilation, callback) => { - const missingDeps = Array.from(compilation.missingDependencies); - const nodeModulesPath = path.join(ROOT_PATH, 'node_modules'); - const hasMissingNodeModules = missingDeps.some( - file => file.indexOf(nodeModulesPath) !== -1 - ); - - // watch for changes to missing node_modules - if (hasMissingNodeModules) compilation.contextDependencies.add(nodeModulesPath); - - // watch for changes to automatic entrypoints - watchAutoEntries.forEach(watchPath => compilation.contextDependencies.add(watchPath)); - - // report our auto-generated bundle count - console.log( - `${autoEntriesCount} entries from '/pages' automatically added to webpack output.` - ); - - callback(); - }); - }, - }); - if (DEV_SERVER_LIVERELOAD) { - config.plugins.push(new webpack.HotModuleReplacementPlugin()); - } -} + }, -if (WEBPACK_REPORT) { - config.plugins.push( - new BundleAnalyzerPlugin({ - analyzerMode: 'static', - generateStatsFile: true, - openAnalyzer: false, - reportFilename: path.join(ROOT_PATH, 'webpack-report/index.html'), - statsFilename: path.join(ROOT_PATH, 'webpack-report/stats.json'), - }) - ); -} + devtool: IS_PRODUCTION ? 'source-map' : 'cheap-module-eval-source-map', -module.exports = config; + // sqljs requires fs + node: { fs: 'empty' }, +}; diff --git a/db/migrate/20180503175053_ensure_missing_columns_to_project_mirror_data.rb b/db/migrate/20180503175053_ensure_missing_columns_to_project_mirror_data.rb new file mode 100644 index 00000000000..970a53d68d0 --- /dev/null +++ b/db/migrate/20180503175053_ensure_missing_columns_to_project_mirror_data.rb @@ -0,0 +1,15 @@ +class EnsureMissingColumnsToProjectMirrorData < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + add_column :project_mirror_data, :status, :string unless column_exists?(:project_mirror_data, :status) + add_column :project_mirror_data, :jid, :string unless column_exists?(:project_mirror_data, :jid) + add_column :project_mirror_data, :last_error, :text unless column_exists?(:project_mirror_data, :last_error) + end + + def down + # db/migrate/20180502122856_create_project_mirror_data.rb will remove the table + end +end diff --git a/db/migrate/20180504195842_project_name_lower_index.rb b/db/migrate/20180504195842_project_name_lower_index.rb new file mode 100644 index 00000000000..d6f25d3d4ab --- /dev/null +++ b/db/migrate/20180504195842_project_name_lower_index.rb @@ -0,0 +1,32 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class ProjectNameLowerIndex < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + INDEX_NAME = 'index_projects_on_lower_name' + + disable_ddl_transaction! + + def up + return unless Gitlab::Database.postgresql? + + disable_statement_timeout + + execute "CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON projects (LOWER(name))" + end + + def down + return unless Gitlab::Database.postgresql? + + disable_statement_timeout + + if supports_drop_index_concurrently? + execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}" + else + execute "DROP INDEX IF EXISTS #{INDEX_NAME}" + end + end +end diff --git a/db/migrate/20180511090724_add_index_on_ci_runners_runner_type.rb b/db/migrate/20180511090724_add_index_on_ci_runners_runner_type.rb new file mode 100644 index 00000000000..580f56007c7 --- /dev/null +++ b/db/migrate/20180511090724_add_index_on_ci_runners_runner_type.rb @@ -0,0 +1,15 @@ +class AddIndexOnCiRunnersRunnerType < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :ci_runners, :runner_type + end + + def down + remove_index :ci_runners, :runner_type + end +end diff --git a/db/migrate/20180517082340_add_not_null_constraints_to_project_authorizations.rb b/db/migrate/20180517082340_add_not_null_constraints_to_project_authorizations.rb new file mode 100644 index 00000000000..3b7b877232b --- /dev/null +++ b/db/migrate/20180517082340_add_not_null_constraints_to_project_authorizations.rb @@ -0,0 +1,38 @@ +class AddNotNullConstraintsToProjectAuthorizations < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def up + if Gitlab::Database.postgresql? + # One-pass version for PostgreSQL + execute <<~SQL + ALTER TABLE project_authorizations + ALTER COLUMN user_id SET NOT NULL, + ALTER COLUMN project_id SET NOT NULL, + ALTER COLUMN access_level SET NOT NULL + SQL + else + change_column_null :project_authorizations, :user_id, false + change_column_null :project_authorizations, :project_id, false + change_column_null :project_authorizations, :access_level, false + end + end + + def down + if Gitlab::Database.postgresql? + # One-pass version for PostgreSQL + execute <<~SQL + ALTER TABLE project_authorizations + ALTER COLUMN user_id DROP NOT NULL, + ALTER COLUMN project_id DROP NOT NULL, + ALTER COLUMN access_level DROP NOT NULL + SQL + else + change_column_null :project_authorizations, :user_id, true + change_column_null :project_authorizations, :project_id, true + change_column_null :project_authorizations, :access_level, true + end + end +end diff --git a/db/migrate/20180521171529_increase_mysql_text_limit_for_gpg_keys.rb b/db/migrate/20180521171529_increase_mysql_text_limit_for_gpg_keys.rb new file mode 100644 index 00000000000..df84898003f --- /dev/null +++ b/db/migrate/20180521171529_increase_mysql_text_limit_for_gpg_keys.rb @@ -0,0 +1,2 @@ +# rubocop:disable all +require_relative 'gpg_keys_limits_to_mysql' diff --git a/db/migrate/gpg_keys_limits_to_mysql.rb b/db/migrate/gpg_keys_limits_to_mysql.rb new file mode 100644 index 00000000000..780340d0564 --- /dev/null +++ b/db/migrate/gpg_keys_limits_to_mysql.rb @@ -0,0 +1,15 @@ +class IncreaseMysqlTextLimitForGpgKeys < ActiveRecord::Migration + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def up + return unless Gitlab::Database.mysql? + + change_column :gpg_keys, :key, :text, limit: 16.megabytes - 1 + end + + def down + # no-op + end +end diff --git a/db/optional_migrations/composite_primary_keys.rb b/db/optional_migrations/composite_primary_keys.rb new file mode 100644 index 00000000000..0fd3fca52dd --- /dev/null +++ b/db/optional_migrations/composite_primary_keys.rb @@ -0,0 +1,63 @@ +# This migration adds a primary key constraint to tables +# that only have a composite unique key. +# +# This is not strictly relevant to Rails (v4 does not +# support composite primary keys). However this becomes +# useful for e.g. PostgreSQL's logical replication (pglogical) +# which requires all tables to have a primary key constraint. +# +# In that sense, the migration is optional and not strictly needed. +class CompositePrimaryKeysMigration < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + Index = Struct.new(:table, :name, :columns) + + TABLES = [ + Index.new(:issue_assignees, 'index_issue_assignees_on_issue_id_and_user_id', %i(issue_id user_id)), + Index.new(:user_interacted_projects, 'index_user_interacted_projects_on_project_id_and_user_id', %i(project_id user_id)), + Index.new(:merge_request_diff_files, 'index_merge_request_diff_files_on_mr_diff_id_and_order', %i(merge_request_diff_id relative_order)), + Index.new(:merge_request_diff_commits, 'index_merge_request_diff_commits_on_mr_diff_id_and_order', %i(merge_request_diff_id relative_order)), + Index.new(:project_authorizations, 'index_project_authorizations_on_user_id_project_id_access_level', %i(user_id project_id access_level)), + Index.new(:push_event_payloads, 'index_push_event_payloads_on_event_id', %i(event_id)), + Index.new(:schema_migrations, 'unique_schema_migrations', %(version)), + ] + + disable_ddl_transaction! + + def up + return unless Gitlab::Database.postgresql? + + disable_statement_timeout + TABLES.each do |index| + add_primary_key(index) + end + end + + def down + return unless Gitlab::Database.postgresql? + + disable_statement_timeout + TABLES.each do |index| + remove_primary_key(index) + end + end + + private + def add_primary_key(index) + execute "ALTER TABLE #{index.table} ADD PRIMARY KEY USING INDEX #{index.name}" + end + + def remove_primary_key(index) + temp_index_name = "#{index.name[0..58]}_old" + rename_index index.table, index.name, temp_index_name if index_exists_by_name?(index.table, index.name) + + # re-create unique key index + add_concurrent_index index.table, index.columns, unique: true, name: index.name + + # This also drops the `temp_index_name` as this is owned by the constraint + execute "ALTER TABLE #{index.table} DROP CONSTRAINT IF EXISTS #{temp_index_name}" + end +end + diff --git a/db/post_migrate/20180511174224_add_unique_constraint_to_project_features_project_id.rb b/db/post_migrate/20180511174224_add_unique_constraint_to_project_features_project_id.rb new file mode 100644 index 00000000000..88a9f5f8256 --- /dev/null +++ b/db/post_migrate/20180511174224_add_unique_constraint_to_project_features_project_id.rb @@ -0,0 +1,43 @@ +class AddUniqueConstraintToProjectFeaturesProjectId < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + class ProjectFeature < ActiveRecord::Base + self.table_name = 'project_features' + + include EachBatch + end + + def up + remove_duplicates + + add_concurrent_index :project_features, :project_id, unique: true, name: 'index_project_features_on_project_id_unique' + remove_concurrent_index_by_name :project_features, 'index_project_features_on_project_id' + rename_index :project_features, 'index_project_features_on_project_id_unique', 'index_project_features_on_project_id' + end + + def down + rename_index :project_features, 'index_project_features_on_project_id', 'index_project_features_on_project_id_old' + add_concurrent_index :project_features, :project_id + remove_concurrent_index_by_name :project_features, 'index_project_features_on_project_id_old' + end + + private + + def remove_duplicates + features = ProjectFeature + .select('MAX(id) AS max, COUNT(id), project_id') + .group(:project_id) + .having('COUNT(id) > 1') + + features.each do |feature| + ProjectFeature + .where(project_id: feature['project_id']) + .where('id <> ?', feature['max']) + .each_batch { |batch| batch.delete_all } + end + end +end diff --git a/db/post_migrate/20180512061621_add_not_null_constraint_to_project_features_project_id.rb b/db/post_migrate/20180512061621_add_not_null_constraint_to_project_features_project_id.rb new file mode 100644 index 00000000000..5a6d6ff4a10 --- /dev/null +++ b/db/post_migrate/20180512061621_add_not_null_constraint_to_project_features_project_id.rb @@ -0,0 +1,21 @@ +class AddNotNullConstraintToProjectFeaturesProjectId < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + class ProjectFeature < ActiveRecord::Base + include EachBatch + + self.table_name = 'project_features' + end + + def up + ProjectFeature.where(project_id: nil).delete_all + + change_column_null :project_features, :project_id, false + end + + def down + change_column_null :project_features, :project_id, true + end +end diff --git a/db/post_migrate/20180514161336_remove_gemnasium_service.rb b/db/post_migrate/20180514161336_remove_gemnasium_service.rb new file mode 100644 index 00000000000..6d7806e8daa --- /dev/null +++ b/db/post_migrate/20180514161336_remove_gemnasium_service.rb @@ -0,0 +1,15 @@ +class RemoveGemnasiumService < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + disable_statement_timeout + + execute("DELETE FROM services WHERE type='GemnasiumService';") + end + + def down + # noop + end +end diff --git a/db/schema.rb b/db/schema.rb index 95adccf0d5c..37d336b9928 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: 20180508135515) do +ActiveRecord::Schema.define(version: 20180521171529) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -503,6 +503,7 @@ ActiveRecord::Schema.define(version: 20180508135515) do add_index "ci_runners", ["contacted_at"], name: "index_ci_runners_on_contacted_at", using: :btree add_index "ci_runners", ["is_shared"], name: "index_ci_runners_on_is_shared", using: :btree add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree + add_index "ci_runners", ["runner_type"], name: "index_ci_runners_on_runner_type", using: :btree add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree create_table "ci_stages", force: :cascade do |t| @@ -1448,9 +1449,9 @@ ActiveRecord::Schema.define(version: 20180508135515) do add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree create_table "project_authorizations", id: false, force: :cascade do |t| - t.integer "user_id" - t.integer "project_id" - t.integer "access_level" + t.integer "user_id", null: false + t.integer "project_id", null: false + t.integer "access_level", null: false end add_index "project_authorizations", ["project_id"], name: "index_project_authorizations_on_project_id", using: :btree @@ -1493,7 +1494,7 @@ ActiveRecord::Schema.define(version: 20180508135515) do add_index "project_deploy_tokens", ["project_id", "deploy_token_id"], name: "index_project_deploy_tokens_on_project_id_and_deploy_token_id", unique: true, using: :btree create_table "project_features", force: :cascade do |t| - t.integer "project_id" + t.integer "project_id", null: false t.integer "merge_requests_access_level" t.integer "issues_access_level" t.integer "wiki_access_level" @@ -1504,7 +1505,7 @@ ActiveRecord::Schema.define(version: 20180508135515) do t.integer "repository_access_level", default: 20, null: false end - add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", using: :btree + add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", unique: true, using: :btree create_table "project_group_links", force: :cascade do |t| t.integer "project_id", null: false diff --git a/doc/README.md b/doc/README.md index c929ba7a59e..ff8dd3fab8a 100644 --- a/doc/README.md +++ b/doc/README.md @@ -241,7 +241,7 @@ GitLab.com is hosted, managed, and administered by GitLab, Inc., with and teams: Free, Bronze, Silver, and Gold. GitLab.com subscriptions grants access -to the same features available in GitLab self-hosted, **expect +to the same features available in GitLab self-hosted, **except [administration](administration/index.md) tools and settings**: - GitLab.com Free includes the same features available in Core diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index ad8ffc46559..957f17e3ea3 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -1,7 +1,7 @@ # NFS You can view information and options set for each of the mounted NFS file -systems by running `sudo nfsstat -m`. +systems by running `nfsstat -m` and `cat /etc/fstab`. ## NFS Server features diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index 69600cad25c..411a0fae93f 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -1,7 +1,7 @@ # GitLab Prometheus metrics >**Note:** -Available since [Omnibus GitLab 9.3][29118]. Currently experimental. For +Available since [Omnibus GitLab 9.3][29118]. For installations from source you'll have to configure it yourself. To enable the GitLab Prometheus metrics: @@ -24,7 +24,7 @@ server, because the embedded server configuration is overwritten once every ## Metrics available -In this experimental phase, only a few metrics are available: +The following metrics are available: | Metric | Type | Since | Description | |:--------------------------------- |:--------- |:----- |:----------- | diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md index 3d24812c66a..f47add48345 100644 --- a/doc/administration/monitoring/prometheus/index.md +++ b/doc/administration/monitoring/prometheus/index.md @@ -120,7 +120,7 @@ To disable the monitoring of Kubernetes: ## GitLab Prometheus metrics -> Introduced as an experimental feature in GitLab 9.3. +> Introduced in GitLab 9.3. GitLab monitors its own internal service metrics, and makes them available at the `/-/metrics` endpoint. Unlike other exporters, this endpoint requires authentication as it is available on the same URL and port as user traffic. diff --git a/doc/api/README.md b/doc/api/README.md index e777fc63d2b..194907accc7 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -33,6 +33,7 @@ following locations: - [Jobs](jobs.md) - [Keys](keys.md) - [Labels](labels.md) +- [Markdown](markdown.md) - [Merge Requests](merge_requests.md) - [Project milestones](milestones.md) - [Group milestones](group_milestones.md) diff --git a/doc/api/group_milestones.md b/doc/api/group_milestones.md index 21d3ac73000..152929b7614 100644 --- a/doc/api/group_milestones.md +++ b/doc/api/group_milestones.md @@ -22,7 +22,7 @@ Parameters: | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | | `iids[]` | Array[integer] | optional | Return only the milestones having the given `iid` | -| `state` | string | optional | Return only `active` or `closed` milestones` | +| `state` | string | optional | Return only `active` or `closed` milestones | | `search` | string | optional | Return only milestones with a title or description matching the provided string | ```bash diff --git a/doc/api/groups.md b/doc/api/groups.md index 923fd662a5b..96842ef330f 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -487,6 +487,9 @@ Parameters: - `id` (required) - The ID or path of a user group +This will queue a background job to delete all projects in the group. The +response will be a 202 Accepted if the user has authorization. + ## Search for group Get all groups that match your string in their name or path. diff --git a/doc/api/issues.md b/doc/api/issues.md index 7479c1d2f93..5613cb6d915 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -38,8 +38,8 @@ GET /issues?my_reaction_emoji=star | `state` | string | no | Return all issues or just those that are `opened` or `closed` | | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels | | `milestone` | string | no | The milestone title | -| `scope` | string | no | Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all`. Defaults to `created-by-me` _([Introduced][ce-13004] in GitLab 9.5)_ | -| `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned-to-me`. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | +| `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | | `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | | `iids[]` | Array[integer] | no | Return only the issues having the given `iid` | @@ -152,7 +152,7 @@ GET /groups/:id/issues?my_reaction_emoji=star | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels | | `iids[]` | Array[integer] | no | Return only the issues having the given `iid` | | `milestone` | string | no | The milestone title | -| `scope` | string | no | Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all` _([Introduced][ce-13004] in GitLab 9.5)_ | +| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | | `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | @@ -266,7 +266,7 @@ GET /projects/:id/issues?my_reaction_emoji=star | `state` | string | no | Return all issues or just those that are `opened` or `closed` | | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels | | `milestone` | string | no | The milestone title | -| `scope` | string | no | Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all` _([Introduced][ce-13004] in GitLab 9.5)_ | +| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | | `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | @@ -467,7 +467,7 @@ POST /projects/:id/issues | `description` | string | no | The description of an issue | | `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. | | `assignee_ids` | Array[integer] | no | The ID of the users to assign issue | -| `milestone_id` | integer | no | The ID of a milestone to assign issue | +| `milestone_id` | integer | no | The global ID of a milestone to assign issue | | `labels` | string | no | Comma-separated label names for an issue | | `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) | | `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | @@ -548,7 +548,7 @@ PUT /projects/:id/issues/:issue_iid | `description` | string | no | The description of an issue | | `confidential` | boolean | no | Updates an issue to be confidential | | `assignee_ids` | Array[integer] | no | The ID of the user(s) to assign the issue to. Set to `0` or provide an empty value to unassign all assignees. | -| `milestone_id` | integer | no | The ID of a milestone to assign the issue to. Set to `0` or provide an empty value to unassign a milestone.| +| `milestone_id` | integer | no | The global ID of a milestone to assign the issue to. Set to `0` or provide an empty value to unassign a milestone.| | `labels` | string | no | Comma-separated label names for an issue. Set to an empty string to unassign all labels. | | `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it | | `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) | @@ -1254,3 +1254,4 @@ Example response: [ce-13004]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13004 [ce-14016]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14016 [ce-17042]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17042 +[ce-18935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18935 diff --git a/doc/api/jobs.md b/doc/api/jobs.md index db4fe2f6880..e4e48edd9a7 100644 --- a/doc/api/jobs.md +++ b/doc/api/jobs.md @@ -82,7 +82,7 @@ Example of response "artifacts_file": null, "finished_at": "2015-12-24T17:54:24.921Z", "id": 6, - "name": "spinach:other", + "name": "rspec:other", "pipeline": { "id": 6, "ref": "master", @@ -196,7 +196,7 @@ Example of response "artifacts_file": null, "finished_at": "2015-12-24T17:54:24.921Z", "id": 6, - "name": "spinach:other", + "name": "rspec:other", "pipeline": { "id": 6, "ref": "master", diff --git a/doc/api/markdown.md b/doc/api/markdown.md new file mode 100644 index 00000000000..f406838e887 --- /dev/null +++ b/doc/api/markdown.md @@ -0,0 +1,29 @@ +# Markdown API + +> [Introduced][ce-18926] in GitLab 11.0. + +Available only in APIv4. + +## Render an arbitrary Markdown document + +``` +POST /api/v4/markdown +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | ------------- | ------------------------------------------ | +| `text` | string | yes | The markdown text to render | +| `gfm` | boolean | no (optional) | Render text using GitLab Flavored Markdown. Default is `false` | +| `project` | string | no (optional) | Use `project` as a context when creating references using GitLab Flavored Markdown. [Authentication](README.html#authentication) is required if a project is not public. | + +```bash +curl --header Content-Type:application/json --data '{"text":"Hello world! :tada:", "gfm":true, "project":"group_example/project_example"}' https://gitlab.example.com/api/v4/markdown +``` + +Response example: + +```json +{ "html": "<p dir=\"auto\">Hello world! <gl-emoji title=\"party popper\" data-name=\"tada\" data-unicode-version=\"6.0\">🎉</gl-emoji></p>" } +``` + +[ce-18926]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18926 diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index b9a4f661777..4e34831422a 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -28,7 +28,7 @@ GET /merge_requests?milestone=release GET /merge_requests?labels=bug,reproduced GET /merge_requests?author_id=5 GET /merge_requests?my_reaction_emoji=star -GET /merge_requests?scope=assigned-to-me +GET /merge_requests?scope=assigned_to_me ``` Parameters: @@ -45,8 +45,8 @@ Parameters: | `created_before` | datetime | no | Return merge requests created on or before the given time | | `updated_after` | datetime | no | Return merge requests updated on or after the given time | | `updated_before` | datetime | no | Return merge requests updated on or before the given time | -| `scope` | string | no | Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`. Defaults to `created-by-me` | -| `author_id` | integer | no | Returns merge requests created by the given user `id`. Combine with `scope=all` or `scope=assigned-to-me` | +| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead. | +| `author_id` | integer | no | Returns merge requests created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me` | | `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` | | `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | | `source_branch` | string | no | Return merge requests with the given source branch | @@ -164,7 +164,7 @@ Parameters: | `created_before` | datetime | no | Return merge requests created on or before the given time | | `updated_after` | datetime | no | Return merge requests updated on or after the given time | | `updated_before` | datetime | no | Return merge requests updated on or before the given time | -| `scope` | string | no | Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all` _([Introduced][ce-13060] in GitLab 9.5)_ | +| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13060] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ | | `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ | | `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ | @@ -539,7 +539,7 @@ POST /projects/:id/merge_requests | `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 | +| `milestone_id` | integer | no | The global ID of a milestone | | `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | | `allow_maintainer_to_push` | boolean | no | Whether or not a maintainer of the target project can push to the source branch | @@ -622,7 +622,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid | `target_branch` | string | no | The target branch | | `title` | string | no | Title of MR | | `assignee_id` | integer | no | The ID of the user to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. | -| `milestone_id` | integer | no | The ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.| +| `milestone_id` | integer | no | The global ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.| | `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. | | `description` | string | no | Description of MR | | `state_event` | string | no | New state (close/reopen) | @@ -1460,3 +1460,4 @@ Example response: [ce-13060]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13060 [ce-14016]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14016 [ce-15454]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15454 +[ce-18935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18935 diff --git a/doc/api/projects.md b/doc/api/projects.md index fe21dfe23f9..79cf5e1cc10 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -66,6 +66,7 @@ GET /projects "ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git", "http_url_to_repo": "http://example.com/diaspora/diaspora-client.git", "web_url": "http://example.com/diaspora/diaspora-client", + "readme_url": "http://example.com/diaspora/diaspora-client/blob/master/README.md", "tag_list": [ "example", "disapora client" @@ -135,6 +136,7 @@ GET /projects "ssh_url_to_repo": "git@example.com:brightbox/puppet.git", "http_url_to_repo": "http://example.com/brightbox/puppet.git", "web_url": "http://example.com/brightbox/puppet", + "readme_url": "http://example.com/brightbox/puppet/blob/master/README.md", "tag_list": [ "example", "puppet" @@ -252,6 +254,7 @@ GET /users/:user_id/projects "ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git", "http_url_to_repo": "http://example.com/diaspora/diaspora-client.git", "web_url": "http://example.com/diaspora/diaspora-client", + "readme_url": "http://example.com/diaspora/diaspora-client/blob/master/README.md", "tag_list": [ "example", "disapora client" @@ -321,6 +324,7 @@ GET /users/:user_id/projects "ssh_url_to_repo": "git@example.com:brightbox/puppet.git", "http_url_to_repo": "http://example.com/brightbox/puppet.git", "web_url": "http://example.com/brightbox/puppet", + "readme_url": "http://example.com/brightbox/puppet/blob/master/README.md", "tag_list": [ "example", "puppet" @@ -420,6 +424,7 @@ GET /projects/:id "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", "web_url": "http://example.com/diaspora/diaspora-project-site", + "readme_url": "http://example.com/diaspora/diaspora-project-site/blob/master/README.md", "tag_list": [ "example", "disapora project" @@ -710,6 +715,7 @@ Example responses: "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", "web_url": "http://example.com/diaspora/diaspora-project-site", + "readme_url": "http://example.com/diaspora/diaspora-project-site/blob/master/README.md", "tag_list": [ "example", "disapora project" @@ -788,6 +794,7 @@ Example response: "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", "web_url": "http://example.com/diaspora/diaspora-project-site", + "readme_url": "http://example.com/diaspora/diaspora-project-site/blob/master/README.md", "tag_list": [ "example", "disapora project" @@ -865,6 +872,7 @@ Example response: "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", "web_url": "http://example.com/diaspora/diaspora-project-site", + "readme_url": "http://example.com/diaspora/diaspora-project-site/blob/master/README.md", "tag_list": [ "example", "disapora project" @@ -966,6 +974,7 @@ Example response: "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", "web_url": "http://example.com/diaspora/diaspora-project-site", + "readme_url": "http://example.com/diaspora/diaspora-project-site/blob/master/README.md", "tag_list": [ "example", "disapora project" @@ -1061,6 +1070,7 @@ Example response: "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git", "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", "web_url": "http://example.com/diaspora/diaspora-project-site", + "readme_url": "http://example.com/diaspora/diaspora-project-site/blob/master/README.md", "tag_list": [ "example", "disapora project" @@ -1421,4 +1431,3 @@ GET /projects/:id/snapshot | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | | `wiki` | boolean | no | Whether to download the wiki, rather than project, repository | - diff --git a/doc/api/services.md b/doc/api/services.md index ec632125325..f23303ef836 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -405,6 +405,13 @@ GET /projects/:id/services/flowdock Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities. +CAUTION: **Warning:** +Gemnasium service integration has been deprecated in GitLab 11.0. Gemnasium has been +[acquired by GitLab](https://about.gitlab.com/press/releases/2018-01-30-gemnasium-acquisition.html) +in January 2018 and since May 15, 2018, the service provided by Gemnasium is no longer available. +You can [migrate from Gemnasium to GitLab](https://docs.gitlab.com/ee/user/project/import/gemnasium.html) +to keep monitoring your dependencies. + ### Create/Edit Gemnasium service Set Gemnasium service for a project. diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 517e25f00f7..3a491f0073c 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -252,6 +252,7 @@ including predefined, secure variables and `.gitlab-ci.yml` [`variables`](yaml/README.md#variables). You however cannot use variables defined under `script` or on the Runner's side. There are other variables that are unsupported in environment name context: +- `CI_PIPELINE_ID` - `CI_JOB_ID` - `CI_JOB_TOKEN` - `CI_BUILD_ID` diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md index d1aa783cc9c..cc19e090964 100644 --- a/doc/ci/examples/code_climate.md +++ b/doc/ci/examples/code_climate.md @@ -5,10 +5,10 @@ GitLab CI and Docker. First, you need GitLab Runner with [docker-in-docker executor][dind]. -Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `codequality`: +Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `code_quality`: ```yaml -codequality: +code_quality: image: docker:stable variables: DOCKER_DRIVER: overlay2 @@ -23,20 +23,27 @@ codequality: --volume /var/run/docker.sock:/var/run/docker.sock "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code artifacts: - paths: [codeclimate.json] + paths: [gl-code-quality-report.json] ``` -The above example will create a `codequality` job in your CI/CD pipeline which +The above example will create a `code_quality` job in your CI/CD pipeline which will scan your source code for code quality issues. The report will be saved as an artifact that you can later download and analyze. TIP: **Tip:** Starting with [GitLab Starter][ee] 9.3, this information will be automatically extracted and shown right in the merge request widget. To do -so, the CI/CD job must be named `codequality` and the artifact path must be -`codeclimate.json`. +so, the CI/CD job must be named `code_quality` and the artifact path must be +`gl-code-quality-report.json`. [Learn more on code quality diffs in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html). +CAUTION: **Caution:** +Code Quality was previously using `codeclimate` and `codequality` for job name and +`codeclimate.json` for the artifact name. While these old names +are still maintained they have been deprecated with GitLab 11.0 and may be removed +in next major release, GitLab 12.0. You are advised to update your current `.gitlab-ci.yml` +configuration to reflect that change. + [cli]: https://github.com/codeclimate/codeclimate [dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor [ee]: https://about.gitlab.com/products/ diff --git a/doc/ci/examples/container_scanning.md b/doc/ci/examples/container_scanning.md index a9501f6c577..92ff90507ee 100644 --- a/doc/ci/examples/container_scanning.md +++ b/doc/ci/examples/container_scanning.md @@ -7,10 +7,10 @@ for Vulnerability Static Analysis for containers. All you need is a GitLab Runner with the Docker executor (the shared Runners on GitLab.com will work fine). You can then add a new job to `.gitlab-ci.yml`, -called `sast:container`: +called `container_scanning`: ```yaml -sast:container: +container_scanning: image: docker:stable variables: DOCKER_DRIVER: overlay2 @@ -34,12 +34,12 @@ sast:container: - retries=0 - echo "Waiting for clair daemon to start" - while( ! wget -T 10 -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; echo -n "." ; if [ $retries -eq 10 ] ; then echo " Timeout, aborting." ; exit 1 ; fi ; retries=$(($retries+1)) ; done - - ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-sast-container-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true + - ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-container-scanning-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true artifacts: - paths: [gl-sast-container-report.json] + paths: [gl-container-scanning-report.json] ``` -The above example will create a `sast:container` job in your CI/CD pipeline, pull +The above example will create a `container_scanning` job in your CI/CD pipeline, pull the image from the [Container Registry](../../user/project/container_registry.md) (whose name is defined from the two `CI_APPLICATION_` variables) and scan it for possible vulnerabilities. The report will be saved as an artifact that you @@ -52,8 +52,15 @@ in our case its named `clair-whitelist.yml`. TIP: **Tip:** Starting with [GitLab Ultimate][ee] 10.4, this information will be automatically extracted and shown right in the merge request widget. To do -so, the CI/CD job must be named `sast:container` and the artifact path must be -`gl-sast-container-report.json`. +so, the CI/CD job must be named `container_scanning` and the artifact path must be +`gl-container-scanning-report.json`. [Learn more on container scanning results shown in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/container_scanning.html). +CAUTION: **Caution:** +Container Scanning was previously using `sast:container` for job name and +`gl-sast-container-report.json` for the artifact name. While these old names +are still maintained they have been deprecated with GitLab 11.0 and may be removed +in next major release, GitLab 12.0. You are advised to update your current `.gitlab-ci.yml` +configuration to reflect that change. + [ee]: https://about.gitlab.com/products/ diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index fec0ff87326..47e658f610e 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -151,7 +151,8 @@ The next step is to configure a Runner so that it picks the pending jobs. In GitLab, Runners run the jobs that you define in `.gitlab-ci.yml`. A Runner can be a virtual machine, a VPS, a bare-metal machine, a docker container or even a cluster of containers. GitLab and the Runners communicate through an API, -so the only requirement is that the Runner's machine has [Internet] access. +so the only requirement is that the Runner's machine has network access to the +GitLab server. A Runner can be specific to a certain project or serve multiple projects in GitLab. If it serves all projects it's called a _Shared Runner_. @@ -226,4 +227,3 @@ CI with various languages. [enabled]: ../enable_or_disable_ci.md [stages]: ../yaml/README.md#stages [pipeline]: ../pipelines.md -[internet]: https://about.gitlab.com/images/theinternet.png diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 42367bf13f7..aedf7958c8a 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -530,6 +530,16 @@ Below you can find supported syntax reference: `$STAGING` value needs to a string, with length higher than zero. Variable that contains only whitespace characters is not an empty variable. +1. Pattern matching _(added in 11.0)_ + + > Example: `$VARIABLE =~ /^content.*/` + + It is possible perform pattern matching against a variable and regular + expression. Expression like this evaluates to truth if matches are found. + + Pattern matching is case-sensitive by default. Use `i` flag modifier, like + `/pattern/i` to make a pattern case-insensitive. + ### Unsupported predefined variables Because GitLab evaluates variables before creating jobs, we do not support a @@ -543,6 +553,7 @@ We do not support variables containing tokens because of security reasons. You can find a full list of unsupported variables below: +- `CI_PIPELINE_ID` - `CI_JOB_ID` - `CI_JOB_TOKEN` - `CI_BUILD_ID` diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 2a17a51d7f8..3e77a6f58b7 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -344,10 +344,11 @@ job: kubernetes: active ``` -Example of using variables expressions: +Examples of using variables expressions: ```yaml deploy: + script: cap staging deploy only: refs: - branches @@ -356,6 +357,16 @@ deploy: - $STAGING ``` +Another use case is exluding jobs depending on a commit message _(added in 11.0)_: + +```yaml +end-to-end: + script: rake test:end-to-end + except: + variables: + - $CI_COMMIT_MESSAGE =~ /skip-end-to-end-tests/ +``` + Learn more about variables expressions on [a separate page][variables-expressions]. ## `tags` diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 7165b8062a7..d03b7fa23ca 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -29,6 +29,10 @@ There are a few rules to get your merge request accepted: to ask one of the [Merge request coaches][team]. 1. The reviewer will assign the merge request to a maintainer once the reviewer is satisfied with the state of the merge request. +1. Keep in mind that maintainers are also going to perform a final code review. + The ideal scenario is that the reviewer has already addressed any concerns + the maintainer would have found, and the maintainer only has to perform the + merge, but be prepared for further review comments. For more guidance, see [CONTRIBUTING.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md). @@ -207,3 +211,4 @@ Largely based on the [thoughtbot code review guide]. [projects]: https://about.gitlab.com/handbook/engineering/projects/ [team]: https://about.gitlab.com/team/ [build handbook]: https://about.gitlab.com/handbook/build/handbook/build#how-to-work-with-build +[^1]: Please note that specs other than JavaScript specs are considered backend code. diff --git a/doc/development/database_debugging.md b/doc/development/database_debugging.md index 32f392f1303..9c31265e417 100644 --- a/doc/development/database_debugging.md +++ b/doc/development/database_debugging.md @@ -11,7 +11,7 @@ Available `RAILS_ENV` - `production` (generally not for your main GDK db, but you may need this for e.g. omnibus) - `development` (this is your main GDK db) - - `test` (used for tests like rspec and spinach) + - `test` (used for tests like rspec) ## Nuke everything and start over diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index 4873090a2d4..057a4094aed 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -53,6 +53,9 @@ stub_licensed_features(variable_environment_scope: true) EE-specific comments should not be backported to CE. +**Note:** This is only meant as a workaround, we should follow up and +resolve this soon. + ### Detection of EE-only files For each commit (except on `master`), the `ee-files-location-check` CI job tries @@ -105,11 +108,14 @@ is applied not only to models. Here's a list of other examples: - `ee/app/services/foo/create_service.rb` - `ee/app/validators/foo_attr_validator.rb` - `ee/app/workers/foo_worker.rb` +- `ee/app/views/foo.html.haml` +- `ee/app/views/foo/_bar.html.haml` This works because for every path that are present in CE's eager-load/auto-load paths, we add the same `ee/`-prepended path in [`config/application.rb`]. +This also applies to views. -[`config/application.rb`]: https://gitlab.com/gitlab-org/gitlab-ee/blob/d278b76d6600a0e27d8019a0be27971ba23ab640/config/application.rb#L41-51 +[`config/application.rb`]: https://gitlab.com/gitlab-org/gitlab-ee/blob/925d3d4ebc7a2c72964ce97623ae41b8af12538d/config/application.rb#L42-52 ### EE features based on CE features @@ -359,9 +365,37 @@ Blocks of code that are EE-specific should be moved to partials. This avoids conflicts with big chunks of HAML code that that are not fun to resolve when you add the indentation to the equation. -EE-specific views should be placed in `ee/app/views/ee/`, using extra +EE-specific views should be placed in `ee/app/views/`, using extra sub-directories if appropriate. +Instead of using regular `render`, we should use `render_if_exists`, which +will not render anything if it cannot find the specific partial. We use this +so that we could put `render_if_exists` in CE, keeping code the same between +CE and EE. + +Also, it should search for the EE partial first, and then CE partial, and +then if nothing found, render nothing. + +This has two uses: + +- CE renders nothing, and EE renders its EE partial. +- CE renders its CE partial, and EE renders its EE partial, while the view + file stays the same. + +The advantages of this: + +- Minimal code difference between CE and EE. +- Very clear hints about where we're extending EE views while reading CE codes. +- Whenever we want to show something different in CE, we could just add CE + partials. Same applies the other way around. If we just use + `render_if_exists`, it would be very easy to change the content in EE. + +The disadvantage of this: + +- Slightly more work while developing EE features, because now we need to + port `render_if_exists` to CE. +- If we have typos in the partial name, it would be silently ignored. + ### Code in `lib/` Place EE-specific logic in the top-level `EE` module namespace. Namespace the diff --git a/doc/development/fe_guide/development_process.md b/doc/development/fe_guide/development_process.md index 5504a6421d5..d240dbe8b02 100644 --- a/doc/development/fe_guide/development_process.md +++ b/doc/development/fe_guide/development_process.md @@ -22,9 +22,9 @@ Please use your best judgement when to use it and please contribute new points t - [ ] Are all [departments](https://about.gitlab.com/handbook/engineering/#engineering-teams) that are needed from your perspective already involved in the issue? (For example is UX missing?) - [ ] Is the specification complete? Are you missing decisions? How about error handling/defaults/edge cases? Take your time to understand the needed implementation and go through its flow. - [ ] Are all necessary UX specifications available that you will need in order to implement? Are there new UX components/patterns in the designs? Then contact the UI component team early on. How should error messages or validation be handled? -- [ ] **Library usage** Use Vuex as soon as you have even a medium state to manage, use Vue router if you need to have different views internally and want to link from the outside. Check what libraries we already have for which occassions. +- [ ] **Library usage** Use Vuex as soon as you have even a medium state to manage, use Vue router if you need to have different views internally and want to link from the outside. Check what libraries we already have for which occasions. - [ ] **Plan your implementation:** - - [ ] **Architecture plan:** Create a plan aligned with GitLab's architecture, how you are going to do the implementation, for example Vue application setup and its components (through [onion skinning](https://gitlab.com/gitlab-org/gitlab-ce/issues/35873#note_39994091)), Store structure and data flow, which existing Vue components can you reuse. Its a good idea to go through your plan with another engineer to refine it. + - [ ] **Architecture plan:** Create a plan aligned with GitLab's architecture, how you are going to do the implementation, for example Vue application setup and its components (through [onion skinning](https://gitlab.com/gitlab-org/gitlab-ce/issues/35873#note_39994091)), Store structure and data flow, which existing Vue components can you reuse. It's a good idea to go through your plan with another engineer to refine it. - [ ] **Backend:** The best way is to kickoff the implementation in a call and discuss with the assigned Backend engineer what you will need from the backend and also when. Can you reuse existing API's? How is the performance with the planned architecture? Maybe create together a JSON mock object to already start with development. - [ ] **Communication:** It also makes sense to have for bigger features an own slack channel (normally called #f_{feature_name}) and even weekly demo calls with all people involved. - [ ] **Dependency Plan:** Are there big dependencies in the plan between you and others, then maybe create an execution diagram to show what is blocking which part and the order of the different parts. @@ -56,7 +56,7 @@ Please use your best judgement when to use it and please contribute new points t - [ ] If you have multiple MR's then also smoke test against the final merge. - [ ] Are there any big changes on how and especially how frequently we use the API then let production know about it - [ ] Smoke test of the RC on dev., staging., canary deployments and .com -- [ ] Follow up on issues that came out of the review. Create isssues for discovered edge cases that should be covered in future iterations. +- [ ] Follow up on issues that came out of the review. Create issues for discovered edge cases that should be covered in future iterations. --- diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md index 04dfe418dbe..284b4b53334 100644 --- a/doc/development/fe_guide/style_guide_js.md +++ b/doc/development/fe_guide/style_guide_js.md @@ -632,7 +632,7 @@ Useful links: // good <span title="tooltip text">Foo</span> - $('span').tooltip('fixTitle'); + $('span').tooltip('_fixTitle'); ``` ### The Javascript/Vue Accord diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md index 6a89bfc7721..858b03c60bf 100644 --- a/doc/development/fe_guide/vuex.md +++ b/doc/development/fe_guide/vuex.md @@ -37,12 +37,13 @@ import state from './state'; Vue.use(Vuex); -export default new Vuex.Store({ +export const createStore = () => new Vuex.Store({ actions, getters, mutations, state, }); +export default createStore(); ``` ### `state.js` @@ -68,10 +69,10 @@ Often we need to provide data from haml to our Vue application. Let's store it i You can use `mapState` to access state properties in the components. ### `actions.js` -An action is a playload of information to send data from our application to our store. +An action is a payload of information to send data from our application to our store. An action is usually composed by a `type` and a `payload` and they describe what happened. -Enforcing that every change is described as an action lets us have a clear understanting of what is going on in the app. +Enforcing that every change is described as an action lets us have a clear understanding of what is going on in the app. In this file, we will write the actions that will call the respective mutations: @@ -87,7 +88,7 @@ In this file, we will write the actions that will call the respective mutations: export const fetchUsers = ({ state, dispatch }) => { dispatch('requestUsers'); - axios.get(state.endoint) + axios.get(state.endpoint) .then(({ data }) => dispatch('receiveUsersSuccess', data)) .catch((error) => { dispatch('receiveUsersError', error) @@ -102,7 +103,7 @@ In this file, we will write the actions that will call the respective mutations: export const addUser = ({ state, dispatch }, user) => { dispatch('requestAddUser'); - axios.post(state.endoint, user) + axios.post(state.endpoint, user) .then(({ data }) => dispatch('receiveAddUserSuccess', data)) .catch((error) => dispatch('receiveAddUserError', error)); } @@ -126,7 +127,7 @@ The component MUST only dispatch the `fetchNamespace` action. Actions namespaced The `fetch` action will be responsible to dispatch `requestNamespace`, `receiveNamespaceSuccess` and `receiveNamespaceError` By following this pattern we guarantee: -1. All aplications follow the same pattern, making it easier for anyone to maintain the code +1. All applications follow the same pattern, making it easier for anyone to maintain the code 1. All data in the application follows the same lifecycle pattern 1. Actions are contained and human friendly 1. Unit tests are easier @@ -149,7 +150,7 @@ import { mapActions } from 'vuex'; }; ``` -#### `mutations.js` +### `mutations.js` The mutations specify how the application state changes in response to actions sent to the store. The only way to change state in a Vuex store should be by committing a mutation. @@ -175,7 +176,7 @@ Remember that actions only describe that something happened, they don't describe state.isLoading = false; }, [types.REQUEST_ADD_USER](state, user) { - state.isAddingUser = true; + state.isAddingUser = true; }, [types.RECEIVE_ADD_USER_SUCCESS](state, user) { state.isAddingUser = false; @@ -183,12 +184,12 @@ Remember that actions only describe that something happened, they don't describe }, [types.REQUEST_ADD_USER_ERROR](state, error) { state.isAddingUser = true; - state.errorAddingUser = error∂; + state.errorAddingUser = error; }, }; ``` -#### `getters.js` +### `getters.js` Sometimes we may need to get derived state based on store state, like filtering for a specific prop. Using a getter will also cache the result based on dependencies due to [how computed props work](https://vuejs.org/v2/guide/computed.html#Computed-Caching-vs-Methods) This can be done through the `getters`: @@ -213,7 +214,7 @@ import { mapGetters } from 'vuex'; }; ``` -#### `mutations_types.js` +### `mutations_types.js` From [vuex mutations docs][vuex-mutations]: > It is a commonly seen pattern to use constants for mutation types in various Flux implementations. This allows the code to take advantage of tooling like linters, and putting all constants in a single file allows your collaborators to get an at-a-glance view of what mutations are possible in the entire application. @@ -289,7 +290,7 @@ export default { ``` ### Vuex Gotchas -1. Do not call a mutation directly. Always use an action to commit a mutation. Doing so will keep consistency through out the application. From Vuex docs: +1. Do not call a mutation directly. Always use an action to commit a mutation. Doing so will keep consistency throughout the application. From Vuex docs: > why don't we just call store.commit('action') directly? Well, remember that mutations must be synchronous? Actions aren't. We can perform asynchronous operations inside an action. @@ -320,10 +321,11 @@ In order to write unit tests for those components, we need to include the store ```javascript //component_spec.js import Vue from 'vue'; -import store from './store'; +import { createStore } from './store'; import component from './component.vue' describe('component', () => { + let store; let vm; let Component; @@ -340,9 +342,11 @@ describe('component', () => { name: 'Foo', age: '30', }; + + store = createStore(); // populate the store - store.dipatch('addUser', user); + store.dispatch('addUser', user); vm = new Component({ store, @@ -352,6 +356,18 @@ describe('component', () => { }); ``` +#### Testing Vuex actions and getters +Because we're currently using [`babel-plugin-rewire`](https://github.com/speedskater/babel-plugin-rewire), you may encounter the following error when testing your Vuex actions and getters: +`[vuex] actions should be function or object with "handler" function` + +To prevent this error from happening, you need to export an empty function as `default`: +``` +// getters.js or actions.js + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; +``` + [vuex-docs]: https://vuex.vuejs.org [vuex-structure]: https://vuex.vuejs.org/en/structure.html [vuex-mutations]: https://vuex.vuejs.org/en/mutations.html diff --git a/doc/development/query_recorder.md b/doc/development/query_recorder.md index 12e90101139..26d3355e94d 100644 --- a/doc/development/query_recorder.md +++ b/doc/development/query_recorder.md @@ -2,7 +2,7 @@ QueryRecorder is a tool for detecting the [N+1 queries problem](http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations) from tests. -> Implemented in [spec/support/query_recorder.rb](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/support/query_recorder.rb) via [9c623e3e](https://gitlab.com/gitlab-org/gitlab-ce/commit/9c623e3e5d7434f2e30f7c389d13e5af4ede770a) +> Implemented in [spec/support/query_recorder.rb](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/support/helpers/query_recorder.rb) via [9c623e3e](https://gitlab.com/gitlab-org/gitlab-ce/commit/9c623e3e5d7434f2e30f7c389d13e5af4ede770a) As a rule, merge requests [should not increase query counts](merge_request_performance_guidelines.md#query-counts). If you find yourself adding something like `.includes(:author, :assignee)` to avoid having `N+1` queries, consider using QueryRecorder to enforce this with a test. Without this, a new feature which causes an additional model to be accessed will silently reintroduce the problem. diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md index fdfa1f10402..31addcaf675 100644 --- a/doc/development/rake_tasks.md +++ b/doc/development/rake_tasks.md @@ -65,12 +65,11 @@ To make sure that indices still fit. You could find great details in: ## Run tests In order to run the test you can use the following commands: -- `rake spinach` to run the spinach suite - `rake spec` to run the rspec suite - `rake karma` to run the karma test suite - `rake gitlab:test` to run all the tests -Note: Both `rake spinach` and `rake spec` takes significant time to pass. +Note: `rake spec` takes significant time to pass. Instead of running full test suite locally you can save a lot of time by running a single test or directory related to your changes. After you submit merge request CI will run full test suite for you. Green CI status in the merge request means @@ -82,12 +81,10 @@ files it can find, also the ones in `/tmp` To run a single test file you can use: - `bin/rspec spec/controllers/commit_controller_spec.rb` for a rspec test -- `bin/spinach features/project/issues/milestones.feature` for a spinach test To run several tests inside one directory: - `bin/rspec spec/requests/api/` for the rspec tests if you want to test API only -- `bin/spinach features/profile/` for the spinach tests if you want to test only profile pages ### Speed-up tests, rake tasks, and migrations diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index 61fa5459b91..a76a5096b69 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -12,8 +12,7 @@ Here are some things to keep in mind regarding test performance: - `FactoryBot.build(...)` and `.build_stubbed` are faster than `.create`. - Don't `create` an object when `build`, `build_stubbed`, `attributes_for`, `spy`, or `double` will do. Database persistence is slow! -- Don't mark a feature as requiring JavaScript (through `@javascript` in - Spinach or `:js` in RSpec) unless it's _actually_ required for the test +- Don't mark a feature as requiring JavaScript (through `:js` in RSpec) unless it's _actually_ required for the test to be valid. Headless browser testing is slow! [parallelization]: ci.md#test-suite-parallelization-on-the-ci @@ -134,11 +133,24 @@ really fast since: - gitlab-shell and Gitaly setup are skipped - Test repositories setup are skipped -Note that in some cases, you might have to add some `require_dependency 'foo'` -in your file under test since Rails autoloading is not available in these cases. +`fast_spec_helper` also support autoloading classes that are located inside the +`lib/` directory. It means that as long as your class / module is using only +code from the `lib/` directory you will not need to explicitly load any +dependencies. `fast_spec_helper` also loads all ActiveSupport extensions, +including core extensions that are commonly used in the Rails environment. -This shouldn't be a problem since explicitely listing dependencies should be -considered a good practice anyway. +Note that in some cases, you might still have to load some dependencies using +`require_dependency` when a code is using gems or a dependency is not located +in `lib/`. + +For example, if you want to test your code that is calling the +`Gitlab::UntrustedRegexp` class, which under the hood uses `re2` library, you +should either add `require_dependency 're2'` to files in your library that +need `re2` gem, to make this requirement explicit, or you can add it to the +spec itself, but the former is preferred. + +It takes around one second to load tests that are using `fast_spec_helper` +instead of 30+ seconds in case of a regular `spec_helper`. ### `let` variables diff --git a/doc/development/testing_guide/ci.md b/doc/development/testing_guide/ci.md index e90de55068d..0d8e150e090 100644 --- a/doc/development/testing_guide/ci.md +++ b/doc/development/testing_guide/ci.md @@ -24,8 +24,7 @@ Our current CI parallelization setup is as follows: uploaded to S3. After that, the next pipeline will use the up-to-date -`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file. The same strategy -is used for Spinach tests as well. +`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file. ### Monitoring diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md index 0d0d511582b..3b2b9c8c947 100644 --- a/doc/development/testing_guide/frontend_testing.md +++ b/doc/development/testing_guide/frontend_testing.md @@ -280,26 +280,6 @@ describe "Admin::AbuseReports", :js do end ``` -### Spinach errors due to missing JavaScript - -NOTE: **Note:** Since we are discouraging the use of Spinach when writing new -feature tests, you shouldn't ever need to use this. This information is kept -available for legacy purposes only. - -In Spinach, the JavaScript driver is enabled differently. In the `*.feature` -file for the failing spec, add the `@javascript` flag above the Scenario: - -``` -@javascript -Scenario: Developer can approve merge request - Given I am a "Shop" developer - And I visit project "Shop" merge requests page - And merge request 'Bug NS-04' must be approved - And I click link "Bug NS-04" - When I click link "Approve" - Then I should see approved merge request "Bug NS-04" -``` - [jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html [jasmine-jquery]: https://github.com/velesin/jasmine-jquery [karma]: http://karma-runner.github.io/ diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md index 74d09eb91ff..0cd63a54b55 100644 --- a/doc/development/testing_guide/index.md +++ b/doc/development/testing_guide/index.md @@ -72,21 +72,6 @@ Everything you should know about how to run end-to-end tests using --- -## Spinach (feature) tests - -GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426) -for its feature/integration tests in September 2012. - -As of March 2016, we are [trying to avoid adding new Spinach -tests](https://gitlab.com/gitlab-org/gitlab-ce/issues/14121) going forward, -opting for [RSpec feature](#features-integration) specs. - -Adding new Spinach scenarios is acceptable _only if_ the new scenario requires -no more than one new `step` definition. If more than that is required, the -test should be re-implemented using RSpec instead. - ---- - [Return to Development documentation](../README.md) [^1]: /ci/yaml/README.html#dependencies diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md index 51794f7f4df..07ced36f0c1 100644 --- a/doc/development/testing_guide/testing_levels.md +++ b/doc/development/testing_guide/testing_levels.md @@ -81,7 +81,6 @@ possible). | Tests path | Testing engine | Notes | | ---------- | -------------- | ----- | | `spec/features/` | [Capybara] + [RSpec] | If your spec has the `:js` metadata, the browser driver will be [Poltergeist], otherwise it's using [RackTest]. | -| `features/` | Spinach | Spinach tests are deprecated, [you shouldn't add new Spinach tests](#spinach-feature-tests). | ### Consider **not** writing a system test! diff --git a/doc/downgrade_ee_to_ce/README.md b/doc/downgrade_ee_to_ce/README.md index f656057e3da..236408762e3 100644 --- a/doc/downgrade_ee_to_ce/README.md +++ b/doc/downgrade_ee_to_ce/README.md @@ -15,9 +15,9 @@ Kerberos and Atlassian Crowd are only available on the Enterprise Edition, so you should disable these mechanisms before downgrading and you should provide alternative authentication methods to your users. -### Remove Jenkins CI Service entries from the database +### Remove Service Integration entries from the database -The `JenkinsService` class is only available on the Enterprise Edition codebase, +The `JenkinsService` and `GithubService` classes are only available in the Enterprise Edition codebase, so if you downgrade to the Community Edition, you'll come across the following error: @@ -30,20 +30,31 @@ column if you didn't intend it to be used for storing the inheritance class or o use another column for that information.) ``` +or + +``` +Completed 500 Internal Server Error in 497ms (ActiveRecord: 32.2ms) + +ActionView::Template::Error (The single-table inheritance mechanism failed to locate the subclass: 'GithubService'. This +error is raised because the column 'type' is reserved for storing the class in case of inheritance. Please rename this +column if you didn't intend it to be used for storing the inheritance class or overwrite Service.inheritance_column to +use another column for that information.) +``` + All services are created automatically for every project you have, so in order to avoid getting this error, you need to remove all instances of the -`JenkinsService` from your database: +`JenkinsService` and `GithubService` from your database: **Omnibus Installation** ``` -$ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService']).delete_all" +$ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all" ``` **Source Installation** ``` -$ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService']).delete_all" production +$ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all" production ``` ### Secret variables environment scopes diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md index 0a093c9ec32..2aab225fcdb 100644 --- a/doc/install/kubernetes/gitlab_runner_chart.md +++ b/doc/install/kubernetes/gitlab_runner_chart.md @@ -1,6 +1,6 @@ # GitLab Runner Helm Chart > **Note:** -These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). +These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/gitlab-runner/issues). The `gitlab-runner` Helm chart deploys a GitLab Runner instance into your Kubernetes cluster. @@ -25,7 +25,7 @@ For more information on available GitLab Helm Charts, please see our [overview]( Create a `values.yaml` file for your GitLab Runner configuration. See [Helm docs](https://github.com/kubernetes/helm/blob/master/docs/chart_template_guide/values_files.md) for information on how your values file will override the defaults. -The default configuration can always be found in the [values.yaml](https://gitlab.com/charts/charts.gitlab.io/blob/master/charts/gitlab-runner/values.yaml) in the chart repository. +The default configuration can always be found in the [values.yaml](https://gitlab.com/charts/gitlab-runner/blob/master/values.yaml) in the chart repository. ### Required configuration @@ -39,7 +39,7 @@ Unless you need to specify additional configuration, you are [ready to install]( ### Other configuration -The rest of the configuration is [documented in the `values.yaml`](https://gitlab.com/charts/charts.gitlab.io/blob/master/charts/gitlab-runner/values.yaml) in the chart repository. +The rest of the configuration is [documented in the `values.yaml`](https://gitlab.com/charts/gitlab-runner/blob/master/values.yaml) in the chart repository. Here is a snippet of the important settings: diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 1f2b4d9d3d9..1f399a8a3f7 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -27,8 +27,8 @@ Please see the [installation from source guide](installation.md) and the [instal ### Non-Unix operating systems such as Windows -GitLab is developed for Unix operating systems. -GitLab does **not** run on Windows and we have no plans of supporting it in the near future. +GitLab is developed for Unix operating systems. +It does **not** run on Windows, and we have no plans to support it in the near future. For the latest development status view this [issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/46567). Please consider using a virtual machine to run GitLab. ## Ruby versions diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 785cc32d590..5faae7ca2d6 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -31,8 +31,8 @@ sudo apt-get install -y rsync >**Note:** In GitLab 9.2 the timestamp format was changed from `EPOCH_YYYY_MM_DD` to -`EPOCH_YYYY_MM_DD_GitLab version`, for example `1493107454_2017_04_25` -would become `1493107454_2017_04_25_9.1.0`. +`EPOCH_YYYY_MM_DD_GitLab_version`, for example `1493107454_2018_04_25` +would become `1493107454_2018_04_25_10.6.4-ce`. The backup archive will be saved in `backup_path`, which is specified in the `config/gitlab.yml` file. @@ -41,8 +41,8 @@ identifies the time at which each backup was created, plus the GitLab version. The timestamp is needed if you need to restore GitLab and multiple backups are available. -For example, if the backup name is `1493107454_2017_04_25_9.1.0_gitlab_backup.tar`, -then the timestamp is `1493107454_2017_04_25_9.1.0`. +For example, if the backup name is `1493107454_2018_04_25_10.6.4-ce_gitlab_backup.tar`, +then the timestamp is `1493107454_2018_04_25_10.6.4-ce`. ### Creating a backup of the GitLab system @@ -64,6 +64,13 @@ If you are running GitLab within a Docker container, you can run the backup from docker exec -t <container name> gitlab-rake gitlab:backup:create ``` +If you are using the gitlab-omnibus helm chart on a Kubernetes cluster, you can +run the backup task on the gitlab application pod using kubectl + +``` +kubectl exec -it <gitlab-gitlab pod> gitlab-rake gitlab:backup:create +``` + Example output: ``` @@ -567,7 +574,7 @@ First make sure your backup tar file is in the backup directory described in the `/var/opt/gitlab/backups`. ```shell -sudo cp 1493107454_2017_04_25_9.1.0_gitlab_backup.tar /var/opt/gitlab/backups/ +sudo cp 11493107454_2018_04_25_10.6.4-ce_gitlab_backup.tar /var/opt/gitlab/backups/ ``` Stop the processes that are connected to the database. Leave the rest of GitLab @@ -585,7 +592,7 @@ restore: ```shell # This command will overwrite the contents of your GitLab database! -sudo gitlab-rake gitlab:backup:restore BACKUP=1493107454_2017_04_25_9.1.0 +sudo gitlab-rake gitlab:backup:restore BACKUP=1493107454_2018_04_25_10.6.4-ce ``` Next, restore `/etc/gitlab/gitlab-secrets.json` if necessary as mentioned above. @@ -601,6 +608,34 @@ If there is a GitLab version mismatch between your backup tar file and the insta version of GitLab, the restore command will abort with an error. Install the [correct GitLab version](https://packages.gitlab.com/gitlab/) and try again. +### Restore for Docker image and gitlab-omnibus helm chart + +For GitLab installations using docker image or the gitlab-omnibus helm chart on +a Kubernetes cluster, restore task expects the restore directories to be empty. +However, with docker and Kubernetes volume mounts, some system level directories +may be created at the volume roots, like `lost+found` directory found in Linux +operating systems. These directories are usually owned by `root`, which can +cause access permission errors since the restore rake task runs as `git` user. +So, to restore a GitLab installation, users have to confirm the restore target +directories are empty. + +For both these installation types, the backup tarball has to be available in the +backup location (default location is `/var/opt/gitlab/backups`). + +For docker installations, the restore task can be run from host using the +command + +``` +docker exec -it <name of container> gitlab-rake gitlab:backup:restore +``` + +Similarly, for gitlab-omnibus helm chart, the restore task can be run on the +gitlab application pod using kubectl + +``` +kubectl exec -it <gitlab-gitlab pod> gitlab-rake gitlab:backup:restore +``` + ## Alternative backup strategies If your GitLab server contains a lot of Git repository data you may find the GitLab backup script to be too slow. diff --git a/doc/topics/autodevops/img/rollout_enabled.png b/doc/topics/autodevops/img/rollout_enabled.png Binary files differnew file mode 100644 index 00000000000..d6e7d790cf2 --- /dev/null +++ b/doc/topics/autodevops/img/rollout_enabled.png diff --git a/doc/topics/autodevops/img/rollout_staging_disabled.png b/doc/topics/autodevops/img/rollout_staging_disabled.png Binary files differnew file mode 100644 index 00000000000..71e36b440f0 --- /dev/null +++ b/doc/topics/autodevops/img/rollout_staging_disabled.png diff --git a/doc/topics/autodevops/img/rollout_staging_enabled.png b/doc/topics/autodevops/img/rollout_staging_enabled.png Binary files differnew file mode 100644 index 00000000000..d0d1d356627 --- /dev/null +++ b/doc/topics/autodevops/img/rollout_staging_enabled.png diff --git a/doc/topics/autodevops/img/staging_enabled.png b/doc/topics/autodevops/img/staging_enabled.png Binary files differnew file mode 100644 index 00000000000..0ef1a67d641 --- /dev/null +++ b/doc/topics/autodevops/img/staging_enabled.png diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 5254e6e3d9a..efec365042a 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -1,7 +1,5 @@ # Auto DevOps -DANGER: Auto DevOps is currently in **Beta** and _not recommended for production use_. - > [Introduced][ce-37115] in GitLab 10.0. Auto DevOps automatically detects, builds, tests, deploys, and monitors your @@ -222,8 +220,8 @@ tests, it's up to you to add them. ### Auto Code Quality -Auto Code Quality uses the open source -[`codeclimate` image](https://hub.docker.com/r/codeclimate/codeclimate/) to run +Auto Code Quality uses the +[Code Quality image](https://gitlab.com/gitlab-org/security-products/codequality) to run static analysis and other code checks on the current code. The report is created, and is uploaded as an artifact which you can later download and check out. @@ -496,6 +494,16 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac | `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-variables-environment-variables). Set it to use a custom database name. | | `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142` | | `STAGING_ENABLED` | From GitLab 10.8, this variable can be used to define a [deploy policy for staging and production environments](#deploy-policy-for-staging-and-production-environments). | +| `CANARY_ENABLED` | From GitLab 11.0, this variable can be used to define a [deploy policy for canary environments](#deploy-policy-for-canary-environments). | +| `INCREMENTAL_ROLLOUT_ENABLED`| From GitLab 10.8, this variable can be used to enable an [incremental rollout](#incremental-rollout-to-production) of your application for the production environment. | +| `TEST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `test` job. If the variable is present, the job will not be created. | +| `CODEQUALITY_DISABLED` | From GitLab 11.0, this variable can be used to disable the `codequality` job. If the variable is present, the job will not be created. | +| `SAST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `sast` job. If the variable is present, the job will not be created. | +| `DEPENDENCY_SCANNING_DISABLED` | From GitLab 11.0, this variable can be used to disable the `dependency_scanning` job. If the variable is present, the job will not be created. | +| `CONTAINER_SCANNING_DISABLED` | From GitLab 11.0, this variable can be used to disable the `sast:container` job. If the variable is present, the job will not be created. | +| `REVIEW_DISABLED` | From GitLab 11.0, this variable can be used to disable the `review` and the manual `review:stop` job. If the variable is present, these jobs will not be created. | +| `DAST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `dast` job. If the variable is present, the job will not be created. | +| `PERFORMANCE_DISABLED` | From GitLab 11.0, this variable can be used to disable the `performance` job. If the variable is present, the job will not be created. | TIP: **Tip:** Set up the replica variables using a @@ -578,6 +586,72 @@ If `STAGING_ENABLED` is defined in your project (e.g., set `STAGING_ENABLED` to to a `staging` environment, and a `production_manual` job will be created for you when you're ready to manually deploy to production. +#### Deploy policy for canary environments **[PREMIUM]** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ci-yml/merge_requests/171) +in GitLab 11.0. + +A [canary environment](https://docs.gitlab.com/ee/user/project/canary_deployments.html) can be used +before any changes are deployed to production. + +If `CANARY_ENABLED` is defined in your project (e.g., set `CANARY_ENABLED` to +`1` as a secret variable) then two manual jobs will be created: + +- `canary` which will deploy the application to the canary environment +- `production_manual` which is to be used by you when you're ready to manually + deploy to production. + +#### Incremental rollout to production **[PREMIUM]** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5415) in GitLab 10.8. + +When you have a new version of your app to deploy in production, you may want +to use an incremental rollout to replace just a few pods with the latest code. +This will allow you to first check how the app is behaving, and later manually +increasing the rollout up to 100%. + +If `INCREMENTAL_ROLLOUT_ENABLED` is defined in your project (e.g., set +`INCREMENTAL_ROLLOUT_ENABLED` to `1` as a secret variable), then instead of the +standard `production` job, 4 different +[manual jobs](../../ci/pipelines.md#manual-actions-from-the-pipeline-graph) +will be created: + +1. `rollout 10%` +1. `rollout 25%` +1. `rollout 50%` +1. `rollout 100%` + +The percentage is based on the `REPLICAS` variable and defines the number of +pods you want to have for your deployment. If you say `10`, and then you run +the `10%` rollout job, there will be `1` new pod + `9` old ones. + +To start a job, click on the play icon next to the job's name. You are not +required to go from `10%` to `100%`, you can jump to whatever job you want. +You can also scale down by running a lower percentage job, just before hitting +`100%`. Once you get to `100%`, you cannot scale down, and you'd have to roll +back by redeploying the old version using the +[rollback button](../../ci/environments.md#rolling-back-changes) in the +environment page. + +Below, you can see how the pipeline will look if the rollout or staging +variables are defined. + +- **Without `INCREMENTAL_ROLLOUT_ENABLED` and without `STAGING_ENABLED`** + + ![Staging and rollout disabled](img/rollout_staging_disabled.png) + +- **Without `INCREMENTAL_ROLLOUT_ENABLED` and with `STAGING_ENABLED`** + + ![Staging enabled](img/staging_enabled.png) + +- **With `INCREMENTAL_ROLLOUT_ENABLED` and without `STAGING_ENABLED`** + + ![Rollout enabled](img/rollout_enabled.png) + +- **With `INCREMENTAL_ROLLOUT_ENABLED` and with `STAGING_ENABLED`** + + ![Rollout and staging enabled](img/rollout_staging_enabled.png) + ## Currently supported languages NOTE: **Note:** diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md index 15567715c98..61c04f3d9bb 100644 --- a/doc/topics/autodevops/quick_start_guide.md +++ b/doc/topics/autodevops/quick_start_guide.md @@ -1,7 +1,5 @@ # Auto DevOps: quick start guide -DANGER: Auto DevOps is currently in **Beta** and _not recommended for production use_. - > [Introduced][ce-37115] in GitLab 10.0. This is a step-by-step guide to deploying a project hosted on GitLab.com to @@ -23,6 +21,10 @@ page](https://gitlab.com/auto-devops-examples/minimal-ruby-app) and press the **Fork** button. Soon you should have a project under your namespace with the necessary files. +You can also start a new project from a +[GitLab project template](https://gitlab.com/gitlab-org/project-templates) if +you want to use a different language. + ## Setup your own cluster on Google Kubernetes Engine If you do not already have a Google Cloud account, create one at @@ -124,10 +126,10 @@ Next, a pipeline needs to be triggered. Since the test project doesn't have a manually visit `https://gitlab.com/<username>/minimal-ruby-app/pipelines/new`, where `<username>` is your username. -This will create a new pipeline with several jobs: `build`, `test`, `codequality`, +This will create a new pipeline with several jobs: `build`, `test`, `code_quality`, and `production`. The `build` job will create a Docker image with your new change and push it to the Container Registry. The `test` job will test your -changes, whereas the `codequality` job will run static analysis on your changes. +changes, whereas the `code_quality` job will run static analysis on your changes. Finally, the `production` job will deploy your changes to a production application. Once the deploy job succeeds you should be able to see your application by diff --git a/doc/university/README.md b/doc/university/README.md index 55865ac23e8..aa68c841f92 100644 --- a/doc/university/README.md +++ b/doc/university/README.md @@ -28,7 +28,6 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project #### 1.1. Version Control and Git 1. [Version Control Systems](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.g72f2e4906_2_29) -1. [Operating Systems and How Git Works](https://drive.google.com/a/gitlab.com/file/d/0B41DBToSSIG_OVYxVFJDOGI3Vzg/view?usp=sharing) 1. [Code School: An Introduction to Git](https://www.codeschool.com/account/courses/try-git) #### 1.2. GitLab Basics diff --git a/doc/user/admin_area/settings/img/enforce_terms.png b/doc/user/admin_area/settings/img/enforce_terms.png Binary files differindex e5f0a2683b5..e5f0a2683b5 100755..100644 --- a/doc/user/admin_area/settings/img/enforce_terms.png +++ b/doc/user/admin_area/settings/img/enforce_terms.png diff --git a/doc/user/admin_area/settings/img/respond_to_terms.png b/doc/user/admin_area/settings/img/respond_to_terms.png Binary files differindex d0d086c3498..d0d086c3498 100755..100644 --- a/doc/user/admin_area/settings/img/respond_to_terms.png +++ b/doc/user/admin_area/settings/img/respond_to_terms.png diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index 7baccb796c6..0c1cd113686 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -75,7 +75,6 @@ Shared Runners on GitLab.com run in [autoscale mode] and powered by Google Cloud Platform and DigitalOcean. Autoscaling means reduced waiting times to spin up CI/CD jobs, and isolated VMs for each project, thus maximizing security. - They're free to use for public open source projects and limited to 2000 CI minutes per month per group for private projects. Read about all [GitLab.com plans](https://about.gitlab.com/pricing/). @@ -90,6 +89,10 @@ ephemeral instances with 3.75GB of RAM, CoreOS and the latest Docker Engine installed. Instances provide 1 vCPU and 25GB of HDD disk space. The default region of the VMs is US East1. +Jobs handled by the shared Runners on GitLab.com (`shared-runners-manager-X.gitlab.com`), +**will be timed out after 3 hours**, regardless of the timeout configured in a +project. Check the issues [4010] and [4070] for the reference. + Below are the shared Runners settings. | Setting | GitLab.com | Default | @@ -340,3 +343,5 @@ High Performance TCP/HTTP Load Balancer: [mailgun]: https://www.mailgun.com/ "Mailgun website" [sidekiq]: http://sidekiq.org/ "Sidekiq website" [unicorn-worker-killer]: https://rubygems.org/gems/unicorn-worker-killer "unicorn-worker-killer" +[4010]: https://gitlab.com/gitlab-com/infrastructure/issues/4010 "Find a good value for maximum timeout for Shared Runners" +[4070]: https://gitlab.com/gitlab-com/infrastructure/issues/4070 "Configure per-runner timeout for shared-runners-manager-X on GitLab.com" diff --git a/doc/user/project/bulk_editing.md b/doc/user/project/bulk_editing.md new file mode 100644 index 00000000000..324a7fa6603 --- /dev/null +++ b/doc/user/project/bulk_editing.md @@ -0,0 +1,17 @@ +# Bulk Editing + +>**Note:** +- A permission level of `Reporter` or higher is required in order to manage +issues. +- A permission level of `Developer` or higher is required in order to manage +merge requests. + +Fields across multiple issues or merge requests can be updated simutaneously by using the bulk edit feature. + +>**Note:** +- Bulk editing of issues and merge requests is only available at the project level. + +To access the feature, navigate to either the issue or merge request list for the project and click 'Edit Issues' or 'Edit Merge Requests'. This will cause a sidebar to be shown on the right-hand side of the screen, where the available, editable fields are displayed. Checkboxes will also appear to the left-hand side of each issue or merge request, ready to be selected. + +Once all items have been selected, choose the appropriate fields and their values from the sidebar and click 'Update All' to apply these changes. + diff --git a/doc/user/project/deploy_tokens/index.md b/doc/user/project/deploy_tokens/index.md index 7a8b3c75690..c09d5aeba8e 100644 --- a/doc/user/project/deploy_tokens/index.md +++ b/doc/user/project/deploy_tokens/index.md @@ -76,7 +76,7 @@ pull images from your Container Registry. > [Introduced][ce-18414] in GitLab 10.8. There's a special case when it comes to Deploy Tokens, if a user creates one -named `gitlab-deploy-token`, the name and token of the Deploy Token will be +named `gitlab-deploy-token`, the username and token of the Deploy Token will be automatically exposed to the CI/CD jobs as environment variables: `CI_DEPLOY_USER` and `CI_DEPLOY_PASSWORD`, respectively. diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md index 5933bcedc8b..4d5b2c97291 100644 --- a/doc/user/project/integrations/jira.md +++ b/doc/user/project/integrations/jira.md @@ -111,7 +111,7 @@ in the table below. | ----- | ----------- | | `Web URL` | The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., `https://jira.example.com`. | | `JIRA API URL` | The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. | -| `Username` | The user name created in [configuring JIRA step](#configuring-jira). | +| `Username` | The user name created in [configuring JIRA step](#configuring-jira). Using the email address will cause `401 unauthorized`. | | `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | | `Transition ID` | This is the ID of a transition that moves issues to the desired state. **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** | diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md index 9496d6f2ce0..074eeb729e3 100644 --- a/doc/user/project/integrations/project_services.md +++ b/doc/user/project/integrations/project_services.md @@ -34,7 +34,7 @@ Click on the service links to see further configuration instructions and details | [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients | | External Wiki | Replaces the link to the internal wiki with a link to an external wiki | | Flowdock | Flowdock is a collaboration web app for technical teams | -| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | +| Gemnasium _(Has been deprecated in GitLab 11.0)_ | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | | [HipChat](hipchat.md) | Private group chat and IM | | [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | | [JIRA](jira.md) | JIRA issue tracker | diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md index be4436749f9..bf17731c523 100644 --- a/doc/user/project/issues/index.md +++ b/doc/user/project/issues/index.md @@ -151,3 +151,7 @@ or Bugzilla. ### Issue's API Read through the [API documentation](../../../api/issues.md). + +### Bulk editing issues + +Find out about [bulk editing issues](../../project/bulk_editing.md). diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index a6c0fd49c45..5932f5a2bc1 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -233,6 +233,10 @@ all your changes will be available to preview by anyone with the Review Apps lin [Read more about Review Apps.](../../../ci/review_apps/index.md) +## Bulk editing merge requests + +Find out about [bulk editing merge requests](../../project/bulk_editing.md). + ## Tips Here are some tips that will help you be more efficient with merge requests in diff --git a/doc/user/project/merge_requests/maintainer_access.md b/doc/user/project/merge_requests/maintainer_access.md index c9763a3fe02..89f71e16a50 100644 --- a/doc/user/project/merge_requests/maintainer_access.md +++ b/doc/user/project/merge_requests/maintainer_access.md @@ -16,3 +16,5 @@ source project, and only lasts while the merge request is open. Enable this functionality while creating a merge request: ![Enable maintainer edits](./img/allow_maintainer_push.png) + +[ce-17395]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17395 diff --git a/doc/user/project/merge_requests/resolve_conflicts.md b/doc/user/project/merge_requests/resolve_conflicts.md index 68c49054e47..ecbc8534eea 100644 --- a/doc/user/project/merge_requests/resolve_conflicts.md +++ b/doc/user/project/merge_requests/resolve_conflicts.md @@ -29,7 +29,7 @@ The merge conflict resolution editor allows for more complex merge conflicts, which require the user to manually modify a file in order to resolve a conflict, to be solved right form the GitLab interface. Use the **Edit inline** button to open the editor. Once you're sure about your changes, hit the -**Commit conflict resolution** button. +**Commit to source branch** button. ![Merge conflict editor](img/merge_conflict_editor.png) diff --git a/doc/user/project/web_ide/img/commit_changes.png b/doc/user/project/web_ide/img/commit_changes.png Binary files differindex b6fcbf699aa..a5364c12760 100644 --- a/doc/user/project/web_ide/img/commit_changes.png +++ b/doc/user/project/web_ide/img/commit_changes.png diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md index b7064b83c4e..105d8a6ab61 100644 --- a/doc/user/project/web_ide/index.md +++ b/doc/user/project/web_ide/index.md @@ -13,15 +13,27 @@ and from merge requests. ![Open Web IDE](img/open_web_ide.png) -## Commit changes +## File finder -Changed files are shown on the right in the commit panel. All changes are -automatically staged. To commit your changes, add a commit message and click -the 'Commit Button'. +> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18323) [GitLab Core][ce] 10.8. + +The file finder allows you to quickly open files in the current branch by +searching. The file finder is launched using the keyboard shortcut `Command-p`, +`Control-p`, or `t` (when editor is not in focus). Type the filename or +file path fragments to start seeing results. + +## Stage and commit changes + +After making your changes, click the Commit button in the bottom left to +review the list of changed files. Click on each file to review the changes and +click the tick icon to stage the file. + +Once you have staged some changes, you can add a commit message and commit the +staged changes. Unstaged changes will not be commited. ![Commit changes](img/commit_changes.png) -## Comparing changes +## Reviewing changes Before you commit your changes, you can compare them with the previous commit by switching to the review mode or selecting the file from the staged files @@ -30,4 +42,5 @@ list. An additional review mode is available when you open a merge request, which shows you a preview of the merge request diff if you commit your changes. +[ce]: https://about.gitlab.com/pricing/ [ee]: https://about.gitlab.com/pricing/ diff --git a/doc/user/snippets.md b/doc/user/snippets.md index 2170b079f62..8397c0b00ef 100644 --- a/doc/user/snippets.md +++ b/doc/user/snippets.md @@ -27,3 +27,36 @@ Personal snippets are not related to any project and can be created completely i You can download the raw content of a snippet. By default snippets will be downloaded with Linux-style line endings (`LF`). If you want to preserve the original line endings you need to add a parameter `line_ending=raw` (eg. `https://gitlab.com/snippets/SNIPPET_ID/raw?line_ending=raw`). In case a snippet was created using the GitLab web interface the original line ending is Windows-like (`CRLF`). + +## Embedded Snippets + +> Introduced in GitLab 10.8. + +Public snippets can not only be shared, but also embedded on any website. This +allows to reuse a GitLab snippet in multiple places and any change to the source +is automatically reflected in the embedded snippet. + +To embed a snippet, first make sure that: + +- The project is public (if it's a project snippet) +- The snippet is public +- In **Project > Settings > Permissions**, the snippets permissions are + set to **Everyone with access** + +Once the above conditions are met, the "Embed" section will appear in your snippet +where you can simply click on the "Copy to clipboard" button. This copies a one-line +script that you can add to any website or blog post. + +Here's how an example code looks like: + +```html +<script src="https://gitlab.com/namespace/project/snippets/SNIPPET_ID.js"></script> +``` + +Here's how an embedded snippet looks like: + +<script src="https://gitlab.com/gitlab-org/gitlab-ce/snippets/1717978.js"></script> + +Embedded snippets are displayed with a header that shows the file name if defined, +the snippet size, a link to GitLab, and the actual snippet content. Actions in +the header allow users to see the snippet in raw format and download it. diff --git a/doc/workflow/merge_requests.md b/doc/workflow/merge_requests.md index a68bb8b27ca..dc6da1938f3 100644 --- a/doc/workflow/merge_requests.md +++ b/doc/workflow/merge_requests.md @@ -1 +1 @@ -This document was moved to [user/project/merge_requests](../user/project/merge_requests.md). +This document was moved to [user/project/merge_requests/index.md](../user/project/merge_requests/index.md). diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index f1501c81b27..fc592b99860 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -106,6 +106,10 @@ by yourself (except when an issue is due). You will only receive automatic notifications when somebody else comments or adds changes to the ones that you've created or mentions you. +If a merge request becomes unmergeable, its author will be notified about the cause. +If a user has also set the merge request to automatically merge once pipeline succeeds, +then that user will also be notified. + ### Email Headers Notification emails include headers that provide extra content about the notification received: diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md index 2e1bd6bfe5c..b2f1cbec204 100644 --- a/doc/workflow/shortcuts.md +++ b/doc/workflow/shortcuts.md @@ -11,7 +11,7 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?' | <kbd>f</kbd> | Focus filter | | <kbd>p</kbd> + <kbd>b</kbd> | Show/hide the Performance Bar | | <kbd>?</kbd> | Show/hide this dialog | -| <kbd>⌘</kbd> + <kbd>shift</kbd> + <kbd>p</kbd> | Toggle markdown preview | +| <kbd>Cmd</kbd>/<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>p</kbd> | Toggle markdown preview | | <kbd>↑</kbd> | Edit last comment (when focused on an empty textarea) | ## Project Files Browsing @@ -46,15 +46,19 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?' | Keyboard Shortcut | Description | | ----------------- | ----------- | | <kbd>g</kbd> + <kbd>p</kbd> | Go to the project's home page | -| <kbd>g</kbd> + <kbd>e</kbd> | Go to the project's activity feed | +| <kbd>g</kbd> + <kbd>v</kbd> | Go to the project's activity feed | | <kbd>g</kbd> + <kbd>f</kbd> | Go to files | | <kbd>g</kbd> + <kbd>c</kbd> | Go to commits | -| <kbd>g</kbd> + <kbd>b</kbd> | Go to jobs | +| <kbd>g</kbd> + <kbd>j</kbd> | Go to jobs | | <kbd>g</kbd> + <kbd>n</kbd> | Go to network graph | -| <kbd>g</kbd> + <kbd>g</kbd> | Go to repository charts | +| <kbd>g</kbd> + <kbd>d</kbd> | Go to repository charts | | <kbd>g</kbd> + <kbd>i</kbd> | Go to issues | +| <kbd>g</kbd> + <kbd>b</kbd> | Go to issue boards | | <kbd>g</kbd> + <kbd>m</kbd> | Go to merge requests | +| <kbd>g</kbd> + <kbd>e</kbd> | Go to environments | +| <kbd>g</kbd> + <kbd>k</kbd> | Go to kubernetes | | <kbd>g</kbd> + <kbd>s</kbd> | Go to snippets | +| <kbd>g</kbd> + <kbd>w</kbd> | Go to wiki | | <kbd>t</kbd> | Go to finding file | | <kbd>i</kbd> | New issue | @@ -66,8 +70,8 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?' | <kbd>→</kbd> or <kbd>l</kbd> | Scroll right | | <kbd>↑</kbd> or <kbd>k</kbd> | Scroll up | | <kbd>↓</kbd> or <kbd>j</kbd> | Scroll down | -| <kbd>shift</kbd> + <kbd>↑</kbd> or <kbd>shift</kbd> + <kbd>k</kbd> | Scroll to top | -| <kbd>shift</kbd> + <kbd>↓</kbd> or <kbd>shift</kbd> + <kbd>j</kbd> | Scroll to bottom | +| <kbd>Shift</kbd> + <kbd>↑</kbd> or <kbd>Shift</kbd> + <kbd>k</kbd> | Scroll to top | +| <kbd>Shift</kbd> + <kbd>↓</kbd> or <kbd>Shift</kbd> + <kbd>j</kbd> | Scroll to bottom | ## Issues and Merge Requests @@ -84,3 +88,9 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?' | Keyboard Shortcut | Description | | ----------------- | ----------- | | <kbd>e</kbd> | Edit wiki page| + +## Web IDE + +| Keyboard Shortcut | Description | +| ----------------- | ----------- | +| <kbd>⌘</kbd> + <kbd>p</kbd> | Go to file | diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md index f13d29884d4..762bf616268 100644 --- a/doc/workflow/todos.md +++ b/doc/workflow/todos.md @@ -31,6 +31,9 @@ A Todo appears in your Todos dashboard when: - you are `@mentioned` in a comment on a commit, - a job in the CI pipeline running for your merge request failed, but this job is not allowed to fail. +- a merge request becomes unmergeable, and you are either: + - the author, or + - have set it to automatically merge once pipeline succeeds. ### Directly addressed Todos diff --git a/features/project/commits/diff_comments.feature b/features/project/commits/diff_comments.feature deleted file mode 100644 index 35687aac9ea..00000000000 --- a/features/project/commits/diff_comments.feature +++ /dev/null @@ -1,96 +0,0 @@ -@project_commits -Feature: Project Commits Diff Comments - Background: - Given I sign in as a user - And I own project "Shop" - And I visit project commit page - - @javascript - Scenario: I can comment on a commit diff - Given I leave a diff comment like "Typo, please fix" - Then I should see a diff comment saying "Typo, please fix" - - @javascript - Scenario: I can add a diff comment with a single emoji - Given I open a diff comment form - And I write a diff comment like ":smile:" - Then I should see a diff comment with an emoji image - - @javascript - Scenario: I get a temporary form for the first comment on a diff line - Given I open a diff comment form - Then I should see a temporary diff comment form - - @javascript - Scenario: I have a cancel button on the diff form - Given I open a diff comment form - Then I should see the cancel comment button - - @javascript - Scenario: I can cancel a diff form - Given I open a diff comment form - And I cancel the diff comment - Then I should not see the diff comment form - - @javascript - Scenario: I can't open a second form for a diff line - Given I open a diff comment form - And I open a diff comment form - Then I should only see one diff form - - @javascript - Scenario: I can have multiple forms - Given I open a diff comment form - And I write a diff comment like ":-1: I don't like this" - And I open another diff comment form - Then I should see a diff comment form with ":-1: I don't like this" - And I should see an empty diff comment form - - @javascript - Scenario: I can preview multiple forms separately - Given I preview a diff comment text like "Should fix it :smile:" - And I preview another diff comment text like "DRY this up" - Then I should see two separate previews - - @javascript - Scenario: I have a reply button in discussions - Given I leave a diff comment like "Typo, please fix" - Then I should see a discussion reply button - - @javascript - Scenario: I can preview with text - Given I open a diff comment form - And I write a diff comment like ":-1: I don't like this" - Then The diff comment preview tab should display rendered Markdown - - @javascript - Scenario: I preview a diff comment - Given I preview a diff comment text like "Should fix it :smile:" - Then I should see the diff comment preview - And I should not see the diff comment text field - - @javascript - Scenario: I can edit after preview - Given I preview a diff comment text like "Should fix it :smile:" - Then I should see the diff comment write tab - - @javascript - Scenario: The form gets removed after posting - Given I preview a diff comment text like "Should fix it :smile:" - And I submit the diff comment - Then I should not see the diff comment form - And I should see a discussion reply button - - @javascript - Scenario: I can add a comment on a side-by-side commit diff (left side) - Given I open a diff comment form - And I click side-by-side diff button - When I leave a diff comment in a parallel view on the left side like "Old comment" - Then I should see a diff comment on the left side saying "Old comment" - - @javascript - Scenario: I can add a comment on a side-by-side commit diff (right side) - Given I open a diff comment form - And I click side-by-side diff button - When I leave a diff comment in a parallel view on the right side like "New comment" - Then I should see a diff comment on the right side saying "New comment" diff --git a/features/project/forked_merge_requests.feature b/features/project/forked_merge_requests.feature deleted file mode 100644 index 9809b0ea0fe..00000000000 --- a/features/project/forked_merge_requests.feature +++ /dev/null @@ -1,51 +0,0 @@ -Feature: Project Forked Merge Requests - Background: - Given I sign in as a user - And I am a member of project "Shop" - And I have a project forked off of "Shop" called "Forked Shop" - - @javascript - Scenario: I submit new unassigned merge request to a forked project - Given I visit project "Forked Shop" merge requests page - And I click link "New Merge Request" - And I fill out a "Merge Request On Forked Project" merge request - And I submit the merge request - Then I should see merge request "Merge Request On Forked Project" - - # TODO: Improve it so it does not fail randomly - # - #@javascript - #Scenario: I can edit a forked merge request - #Given I visit project "Forked Shop" merge requests page - #And I click link "New Merge Request" - #And I fill out a "Merge Request On Forked Project" merge request - #And I submit the merge request - #And I should see merge request "Merge Request On Forked Project" - #And I click link edit "Merge Request On Forked Project" - #Then I see the edit page prefilled for "Merge Request On Forked Project" - #And I update the merge request title - #And I save the merge request - #Then I should see the edited merge request - - Scenario: I cannot submit an invalid merge request - Given I visit project "Forked Shop" merge requests page - And I click link "New Merge Request" - And I fill out an invalid "Merge Request On Forked Project" merge request - Then I should see validation errors - - @javascript - Scenario: Merge request should target fork repository by default - Given I visit project "Forked Shop" merge requests page - And I click link "New Merge Request" - Then the target repository should be the original repository - - @javascript - Scenario: I see the users in the target project for a new merge request - Given I sign in as an admin - And I have a project forked off of "Shop" called "Forked Shop" - Then I visit project "Forked Shop" merge requests page - And I click link "New Merge Request" - And I fill out a "Merge Request On Forked Project" merge request - When I click "Assign to" dropdown" - Then I should see the target project ID in the input selector - And I should see the users from the target project ID diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb deleted file mode 100644 index 97bcca7730b..00000000000 --- a/features/steps/group/members.rb +++ /dev/null @@ -1,68 +0,0 @@ -class Spinach::Features::GroupMembers < Spinach::FeatureSteps - include WaitForRequests - include SharedAuthentication - include SharedPaths - include SharedGroup - include SharedUser - - step 'I should see user "John Doe" in team list' do - expect(group_members_list).to have_content("John Doe") - end - - step 'I should not see user "Mary Jane" in team list' do - expect(group_members_list).not_to have_content("Mary Jane") - end - - step 'I click on the "Remove User From Group" button for "John Doe"' do - find(:css, '.project-members-page li', text: "John Doe").find(:css, 'a.btn-remove').click - # poltergeist always confirms popups. - end - - step 'I click on the "Remove User From Group" button for "Mary Jane"' do - find(:css, 'li', text: "Mary Jane").find(:css, 'a.btn-remove').click - # poltergeist always confirms popups. - end - - step 'I should not see the "Remove User From Group" button for "John Doe"' do - expect(find(:css, '.project-members-page li', text: "John Doe")).not_to have_selector(:css, 'a.btn-remove') - # poltergeist always confirms popups. - end - - step 'I should not see the "Remove User From Group" button for "Mary Jane"' do - expect(find(:css, 'li', text: "Mary Jane")).not_to have_selector(:css, 'a.btn-remove') - # poltergeist always confirms popups. - end - - step 'I change the "Mary Jane" role to "Developer"' do - member = mary_jane_member - - page.within "#group_member_#{member.id}" do - click_button member.human_access - - page.within '.dropdown-menu' do - click_link 'Developer' - end - - wait_for_requests - end - end - - step 'I should see "Mary Jane" as "Developer"' do - member = mary_jane_member - - page.within "#group_member_#{member.id}" do - expect(page).to have_content "Developer" - end - end - - private - - def mary_jane_member - user = User.find_by(name: "Mary Jane") - owned_group.members.find_by(user_id: user.id) - end - - def group_members_list - find(".panel .content-list") - end -end diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb deleted file mode 100644 index f8eb0f01de8..00000000000 --- a/features/steps/profile/notifications.rb +++ /dev/null @@ -1,20 +0,0 @@ -class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - - step 'I visit profile notifications page' do - visit profile_notifications_path - end - - step 'I should see global notifications settings' do - expect(page).to have_content "Notifications" - end - - step 'I select Mention setting from dropdown' do - first(:link, "On mention").click - end - - step 'I should see Notification saved message' do - expect(page).to have_content 'On mention' - end -end diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb deleted file mode 100644 index 3ecd4c8b672..00000000000 --- a/features/steps/project/commits/branches.rb +++ /dev/null @@ -1,32 +0,0 @@ -class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - - step 'I click link "All"' do - click_link "All" - end - - step 'I click link "Protected"' do - click_link "Protected" - end - - step 'I click new branch link' do - click_link "New branch" - end - - step 'I submit new branch form with invalid name' do - fill_in 'branch_name', with: '1.0 stable' - page.find("body").click # defocus the branch_name input - select_branch('master') - click_button 'Create branch' - end - - def select_branch(branch_name) - find('.git-revision-dropdown-toggle').click - - page.within '#new-branch-form .dropdown-menu' do - click_link branch_name - end - end -end diff --git a/features/steps/project/commits/comments.rb b/features/steps/project/commits/comments.rb deleted file mode 100644 index 3d4d8ad6368..00000000000 --- a/features/steps/project/commits/comments.rb +++ /dev/null @@ -1,6 +0,0 @@ -class Spinach::Features::ProjectCommitsComments < Spinach::FeatureSteps - include SharedAuthentication - include SharedNote - include SharedPaths - include SharedProject -end diff --git a/features/steps/project/commits/diff_comments.rb b/features/steps/project/commits/diff_comments.rb deleted file mode 100644 index b9d8cf2c5a5..00000000000 --- a/features/steps/project/commits/diff_comments.rb +++ /dev/null @@ -1,6 +0,0 @@ -class Spinach::Features::ProjectCommitsDiffComments < Spinach::FeatureSteps - include SharedAuthentication - include SharedDiffNote - include SharedPaths - include SharedProject -end diff --git a/features/steps/project/create.rb b/features/steps/project/create.rb deleted file mode 100644 index 60fa232672e..00000000000 --- a/features/steps/project/create.rb +++ /dev/null @@ -1,23 +0,0 @@ -class Spinach::Features::ProjectCreate < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedUser - - step 'fill project form with valid data' do - fill_in 'project_path', with: 'Empty' - page.within '#content-body' do - click_button "Create project" - end - end - - step 'I should see project page' do - expect(page).to have_content "Empty" - expect(current_path).to eq project_path(Project.last) - end - - step 'I should see empty project instructions' do - expect(page).to have_content "git init" - expect(page).to have_content "git remote" - expect(page).to have_content Project.last.url_to_repo - end -end diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb deleted file mode 100644 index 82b931b2246..00000000000 --- a/features/steps/project/forked_merge_requests.rb +++ /dev/null @@ -1,139 +0,0 @@ -class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedNote - include SharedPaths - include Select2Helper - include WaitForRequests - include ProjectForksHelper - - step 'I am a member of project "Shop"' do - @project = ::Project.find_by(name: "Shop") - @project ||= create(:project, :repository, name: "Shop") - @project.add_reporter(@user) - end - - step 'I have a project forked off of "Shop" called "Forked Shop"' do - @forked_project = fork_project(@project, @user, - namespace: @user.namespace, - repository: true) - end - - step 'I click link "New Merge Request"' do - page.within '#content-body' do - page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request') - end - end - - step 'I should see merge request "Merge Request On Forked Project"' do - expect(@project.merge_requests.size).to be >= 1 - @merge_request = @project.merge_requests.last - expect(current_path).to eq project_merge_request_path(@project, @merge_request) - expect(@merge_request.title).to eq "Merge Request On Forked Project" - expect(@merge_request.source_project).to eq @forked_project - expect(@merge_request.source_branch).to eq "fix" - expect(@merge_request.target_branch).to eq "master" - expect(page).to have_content @forked_project.full_path - expect(page).to have_content @project.full_path - expect(page).to have_content @merge_request.source_branch - expect(page).to have_content @merge_request.target_branch - - wait_for_requests - end - - step 'I fill out a "Merge Request On Forked Project" merge request' do - expect(page).to have_content('Source branch') - expect(page).to have_content('Target branch') - - first('.js-source-project').click - first('.dropdown-source-project a', text: @forked_project.full_path) - - first('.js-target-project').click - first('.dropdown-target-project a', text: @project.full_path) - - first('.js-source-branch').click - wait_for_requests - first('.js-source-branch-dropdown .dropdown-content a', text: 'fix').click - - click_button "Compare branches and continue" - - expect(page).to have_css("h3.page-title", text: "New Merge Request") - - page.within 'form#new_merge_request' do - fill_in "merge_request_title", with: "Merge Request On Forked Project" - end - end - - step 'I submit the merge request' do - click_button "Submit merge request" - end - - step 'I update the merge request title' do - fill_in "merge_request_title", with: "An Edited Forked Merge Request" - end - - step 'I save the merge request' do - click_button "Save changes" - end - - step 'I should see the edited merge request' do - expect(page).to have_content "An Edited Forked Merge Request" - expect(@project.merge_requests.size).to be >= 1 - @merge_request = @project.merge_requests.last - expect(current_path).to eq project_merge_request_path(@project, @merge_request) - expect(@merge_request.source_project).to eq @forked_project - expect(@merge_request.source_branch).to eq "fix" - expect(@merge_request.target_branch).to eq "master" - expect(page).to have_content @forked_project.full_path - expect(page).to have_content @project.full_path - expect(page).to have_content @merge_request.source_branch - expect(page).to have_content @merge_request.target_branch - end - - step 'I should see last push widget' do - expect(page).to have_content "You pushed to new_design" - expect(page).to have_link "Create Merge Request" - end - - step 'I click link edit "Merge Request On Forked Project"' do - find("#edit_merge_request").click - end - - step 'I see the edit page prefilled for "Merge Request On Forked Project"' do - expect(current_path).to eq edit_project_merge_request_path(@project, @merge_request) - expect(page).to have_content "Edit merge request #{@merge_request.to_reference}" - expect(find("#merge_request_title").value).to eq "Merge Request On Forked Project" - end - - step 'I fill out an invalid "Merge Request On Forked Project" merge request' do - expect(find_by_id("merge_request_source_project_id", visible: false).value).to eq @forked_project.id.to_s - expect(find_by_id("merge_request_target_project_id", visible: false).value).to eq @project.id.to_s - expect(find_by_id("merge_request_source_branch", visible: false).value).to eq nil - expect(find_by_id("merge_request_target_branch", visible: false).value).to eq "master" - click_button "Compare branches" - end - - step 'I should see validation errors' do - expect(page).to have_content "You must select source and target branch" - end - - step 'the target repository should be the original repository' do - expect(find_by_id("merge_request_target_project_id").value).to eq "#{@project.id}" - end - - step 'I click "Assign to" dropdown"' do - click_button 'Assignee' - end - - step 'I should see the target project ID in the input selector' do - expect(find('.js-assignee-search')["data-project-id"]).to eq "#{@project.id}" - end - - step 'I should see the users from the target project ID' do - page.within '.dropdown-menu-user' do - expect(page).to have_content 'Unassigned' - expect(page).to have_content current_user.name - expect(page).to have_content @project.users.first.name - end - end -end diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb deleted file mode 100644 index b467af53c98..00000000000 --- a/features/steps/project/issues/filter_labels.rb +++ /dev/null @@ -1,61 +0,0 @@ -class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - include Select2Helper - - step 'I should see "Bugfix1" in issues list' do - page.within ".issues-list" do - expect(page).to have_content "Bugfix1" - end - end - - step 'I should see "Bugfix2" in issues list' do - page.within ".issues-list" do - expect(page).to have_content "Bugfix2" - end - end - - step 'I should not see "Bugfix2" in issues list' do - page.within ".issues-list" do - expect(page).not_to have_content "Bugfix2" - end - end - - step 'I should not see "Feature1" in issues list' do - page.within ".issues-list" do - expect(page).not_to have_content "Feature1" - end - end - - step 'I click "dropdown close button"' do - page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click - sleep 2 - end - - step 'I click link "feature"' do - page.within ".labels-filter" do - click_link "feature" - end - end - - step 'project "Shop" has issue "Bugfix1" with labels: "bug", "feature"' do - project = Project.find_by(name: "Shop") - issue = create(:issue, title: "Bugfix1", project: project) - issue.labels << project.labels.find_by(title: 'bug') - issue.labels << project.labels.find_by(title: 'feature') - end - - step 'project "Shop" has issue "Bugfix2" with labels: "bug", "enhancement"' do - project = Project.find_by(name: "Shop") - issue = create(:issue, title: "Bugfix2", project: project) - issue.labels << project.labels.find_by(title: 'bug') - issue.labels << project.labels.find_by(title: 'enhancement') - end - - step 'project "Shop" has issue "Feature1" with labels: "feature"' do - project = Project.find_by(name: "Shop") - issue = create(:issue, title: "Feature1", project: project) - issue.labels << project.labels.find_by(title: 'feature') - end -end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb deleted file mode 100644 index baa78c23203..00000000000 --- a/features/steps/project/issues/issues.rb +++ /dev/null @@ -1,175 +0,0 @@ -class Spinach::Features::ProjectIssues < Spinach::FeatureSteps - include SharedAuthentication - include SharedIssuable - include SharedProject - include SharedNote - include SharedPaths - include SharedMarkdown - include SharedUser - - step 'I should not see "Release 0.3" in issues' do - expect(page).not_to have_content "Release 0.3" - end - - step 'I click link "Closed"' do - find('.issues-state-filters [data-state="closed"] span', text: 'Closed').click - end - - step 'I should see "Release 0.3" in issues' do - expect(page).to have_content "Release 0.3" - end - - step 'I should not see "Release 0.4" in issues' do - expect(page).not_to have_content "Release 0.4" - end - - step 'I click link "All"' do - find('.issues-state-filters [data-state="all"] span', text: 'All').click - # Waits for load - expect(find('.issues-state-filters > .active')).to have_content 'All' - end - - step 'I should see issue "Tweet control"' do - expect(page).to have_content "Tweet control" - end - - step 'I click "author" dropdown' do - page.find('.js-author-search').click - sleep 1 - end - - step 'I see current user as the first user' do - expect(page).to have_selector('.dropdown-content', visible: true) - users = page.all('.dropdown-menu-author .dropdown-content li a') - expect(users[0].text).to eq 'Any Author' - expect(users[1].text).to eq "#{current_user.name} #{current_user.to_reference}" - end - - step 'I click link "500 error on profile"' do - click_link "500 error on profile" - end - - step 'I should see label \'bug\' with issue' do - page.within '.issuable-show-labels' do - expect(page).to have_content 'bug' - end - end - - step 'I fill in issue search with "Re"' do - filter_issue "Re" - end - - step 'I fill in issue search with "Bu"' do - filter_issue "Bu" - end - - step 'I fill in issue search with ".3"' do - filter_issue ".3" - end - - step 'I fill in issue search with "Something"' do - filter_issue "Something" - end - - step 'I fill in issue search with ""' do - filter_issue "" - end - - step 'project "Shop" has milestone "v2.2"' do - milestone = create(:milestone, title: "v2.2", project: project) - - 3.times { create(:issue, project: project, milestone: milestone) } - end - - step 'project "Shop" has milestone "v3.0"' do - milestone = create(:milestone, title: "v3.0", project: project) - - 3.times { create(:issue, project: project, milestone: milestone) } - end - - When 'I select milestone "v3.0"' do - select "v3.0", from: "milestone_id" - end - - step 'I should see selected milestone with title "v3.0"' do - issues_milestone_selector = "#issue_milestone_id_chzn > a" - expect(find(issues_milestone_selector)).to have_content("v3.0") - end - - When 'I select first assignee from "Shop" project' do - first_assignee = project.users.first - select first_assignee.name, from: "assignee_id" - end - - step 'I should see first assignee from "Shop" as selected assignee' do - issues_assignee_selector = "#issue_assignee_id_chzn > a" - - assignee_name = project.users.first.name - expect(find(issues_assignee_selector)).to have_content(assignee_name) - end - - step 'The list should be sorted by "Least popular"' do - page.within '.issues-list' do - page.within 'li.issue:nth-child(1)' do - expect(page).to have_content 'Tweet control' - expect(page).to have_content '1 2' - end - - page.within 'li.issue:nth-child(2)' do - expect(page).to have_content 'Release 0.4' - expect(page).to have_content '2 1' - end - - page.within 'li.issue:nth-child(3)' do - expect(page).to have_content 'Bugfix' - expect(page).not_to have_content '0 0' - end - end - end - - When 'I visit empty project page' do - project = Project.find_by(name: 'Empty Project') - visit project_path(project) - end - - When "I visit project \"Community\" issues page" do - project = Project.find_by(name: 'Community') - visit project_issues_path(project) - end - - step 'project \'Shop\' has issue \'Bugfix1\' with description: \'Description for issue1\'' do - create(:issue, title: 'Bugfix1', description: 'Description for issue1', project: project) - end - - step 'project \'Shop\' has issue \'Feature1\' with description: \'Feature submitted for issue1\'' do - create(:issue, title: 'Feature1', description: 'Feature submitted for issue1', project: project) - end - - step 'I fill in issue search with \'Description for issue1\'' do - filter_issue 'Description for issue' - end - - step 'I fill in issue search with \'issue1\'' do - filter_issue 'issue1' - end - - step 'I fill in issue search with \'Rock and roll\'' do - filter_issue 'Rock and roll' - end - - step 'I should see \'Bugfix1\' in issues' do - expect(page).to have_content 'Bugfix1' - end - - step 'I should see \'Feature1\' in issues' do - expect(page).to have_content 'Feature1' - end - - step 'I should not see \'Bugfix1\' in issues' do - expect(page).not_to have_content 'Bugfix1' - end - - def filter_issue(text) - fill_in 'issuable_search', with: text - end -end diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb deleted file mode 100644 index 30927306a4f..00000000000 --- a/features/steps/project/issues/milestones.rb +++ /dev/null @@ -1,20 +0,0 @@ -class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - include SharedMarkdown - - step 'project "Shop" has milestone "v2.2"' do - project = Project.find_by(name: "Shop") - milestone = create(:milestone, - title: "v2.2", - project: project, - description: "# Description header" - ) - 3.times { create(:issue, project: project, milestone: milestone) } - end - - When 'I click link "All Issues"' do - click_link 'All Issues' - end -end diff --git a/features/steps/project/issues/references.rb b/features/steps/project/issues/references.rb deleted file mode 100644 index 69e8b5cbde5..00000000000 --- a/features/steps/project/issues/references.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Spinach::Features::ProjectIssuesReferences < Spinach::FeatureSteps - include SharedAuthentication - include SharedIssuable - include SharedNote - include SharedProject - include SharedUser -end diff --git a/features/steps/project/merge_requests/references.rb b/features/steps/project/merge_requests/references.rb deleted file mode 100644 index ab2ae6847a2..00000000000 --- a/features/steps/project/merge_requests/references.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Spinach::Features::ProjectMergeRequestsReferences < Spinach::FeatureSteps - include SharedAuthentication - include SharedIssuable - include SharedNote - include SharedProject - include SharedUser -end diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb deleted file mode 100644 index afaad4b255e..00000000000 --- a/features/steps/project/source/browse_files.rb +++ /dev/null @@ -1,435 +0,0 @@ -# coding: utf-8 -class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - include RepoHelpers - include WaitForRequests - - step "I don't have write access" do - @project = create(:project, :repository, name: "Other Project", path: "other-project") - @project.add_reporter(@user) - visit project_tree_path(@project, root_ref) - end - - step 'I should see files from repository' do - expect(page).to have_content "VERSION" - expect(page).to have_content ".gitignore" - expect(page).to have_content "LICENSE" - end - - step 'I should see files from repository for "6d39438"' do - expect(current_path).to eq project_tree_path(@project, "6d39438") - expect(page).to have_content ".gitignore" - expect(page).to have_content "LICENSE" - end - - step 'I see the ".gitignore"' do - expect(page).to have_content '.gitignore' - end - - step 'I don\'t see the ".gitignore"' do - expect(page).not_to have_content '.gitignore' - end - - step 'I click on ".gitignore" file in repo' do - click_link ".gitignore" - end - - step 'I should see its content' do - wait_for_requests - expect(page).to have_content old_gitignore_content - end - - step 'I should see its new content' do - wait_for_requests - expect(page).to have_content new_gitignore_content - end - - step 'I click link "Raw"' do - click_link 'Open raw' - end - - step 'I should see raw file content' do - expect(source).to eq '' # Body is filled in by gitlab-workhorse - end - - step 'I click button "Edit"' do - find('.js-edit-blob').click - end - - step 'I cannot see the edit button' do - expect(page).not_to have_link 'edit' - end - - step 'I click button "Fork"' do - click_link 'Fork' - end - - step 'I edit code' do - expect(page).to have_selector('.file-editor') - set_new_content - end - - step 'I fill the new file name' do - fill_in :file_name, with: new_file_name - end - - step 'I fill the new branch name' do - fill_in :branch_name, with: 'new_branch_name', visible: true - end - - step 'I fill the new file name with a new directory' do - fill_in :file_name, with: new_file_name_with_directory - end - - step 'I fill the commit message' do - fill_in :commit_message, with: 'New commit message', visible: true - end - - step 'I click link "Diff"' do - click_link 'Preview changes' - end - - step 'I click on "Commit changes"' do - click_button 'Commit changes' - end - - step 'I click on "Changes" tab' do - click_link 'Changes' - end - - step 'I click on "Create directory"' do - click_button 'Create directory' - end - - step 'I click on "Delete"' do - click_on 'Delete' - end - - step 'I click on "Delete file"' do - click_button 'Delete file' - end - - step 'I click on "Replace"' do - click_on "Replace" - end - - step 'I click on "Replace file"' do - click_button 'Replace file' - end - - step 'I see diff' do - expect(page).to have_css '.line_holder.new' - end - - step 'I click on "New file" link in repo' do - find('.add-to-tree').click - click_link 'New file' - expect(page).to have_selector('.file-editor') - end - - step 'I click on "Upload file" link in repo' do - find('.add-to-tree').click - click_link 'Upload file' - end - - step 'I click on "New directory" link in repo' do - find('.add-to-tree').click - click_link 'New directory' - end - - step 'I fill the new directory name' do - fill_in :dir_name, with: new_dir_name - end - - step 'I fill an existing directory name' do - fill_in :dir_name, with: 'files' - end - - step 'I can see new file page' do - expect(page).to have_content "New File" - expect(page).to have_content "Commit message" - end - - step 'I click on "Upload file"' do - click_button 'Upload file' - end - - step 'I can see the new commit message' do - expect(page).to have_content "New commit message" - end - - step 'I upload a new text file' do - drop_in_dropzone test_text_file - end - - step 'I fill the upload file commit message' do - page.within('#modal-upload-blob') do - fill_in :commit_message, with: 'New commit message' - end - end - - step 'I replace it with a text file' do - drop_in_dropzone test_text_file - end - - step 'I fill the replace file commit message' do - page.within('#modal-upload-blob') do - fill_in :commit_message, with: 'Replacement file commit message' - end - end - - step 'I can see the replacement commit message' do - expect(page).to have_content "Replacement file commit message" - end - - step 'I can see the new text file' do - expect(page).to have_content "Lorem ipsum dolor sit amet" - expect(page).to have_content "Sed ut perspiciatis unde omnis" - end - - step 'I click on files directory' do - click_link 'files' - end - - step 'I click on History link' do - click_link 'History' - end - - step 'I see Browse dir link' do - expect(page).to have_link 'Browse Directory' - expect(page).not_to have_link 'Browse Code' - end - - step 'I click on readme file' do - page.within '.tree-table' do - click_link 'README.md' - end - end - - step 'I see Browse file link' do - expect(page).to have_link 'Browse File' - expect(page).not_to have_link 'Browse Files' - end - - step 'I see Browse code link' do - expect(page).to have_link 'Browse Files' - expect(page).not_to have_link 'Browse Directory' - end - - step 'I click on Permalink' do - click_link 'Permalink' - end - - step 'I am redirected to the files URL' do - expect(current_path).to eq project_tree_path(@project, 'master') - end - - step 'I am redirected to the ".gitignore"' do - expect(current_path).to eq(project_blob_path(@project, 'master/.gitignore')) - end - - step 'I am redirected to the permalink URL' do - expect(current_path).to( - eq(project_blob_path(@project, - @project.repository.commit.sha + - '/.gitignore')) - ) - end - - step 'I am redirected to the new file' do - expect(current_path).to eq( - project_blob_path(@project, 'master/' + new_file_name)) - end - - step 'I am redirected to the new file with directory' do - expect(current_path).to eq( - project_blob_path(@project, 'master/' + new_file_name_with_directory)) - end - - step 'I am redirected to the new merge request page' do - expect(current_path).to eq(project_new_merge_request_path(@project)) - end - - step "I am redirected to the fork's new merge request page" do - fork = @user.fork_of(@project) - expect(current_path).to eq(project_new_merge_request_path(fork)) - end - - step 'I am redirected to the root directory' do - expect(current_path).to eq( - project_tree_path(@project, 'master')) - end - - step "I don't see the permalink link" do - expect(page).not_to have_link('permalink') - end - - step 'I see "Unable to create directory"' do - expect(page).to have_content('A directory with this name already exists') - end - - step 'I see "Path can contain only..."' do - expect(page).to have_content('Path can contain only') - end - - step 'I see a commit error message' do - expect(page).to have_content('Your changes could not be committed') - end - - step "I switch ref to 'test'" do - first('.js-project-refs-dropdown').click - - page.within '.project-refs-form' do - click_link "'test'" - end - end - - step "I switch ref to fix" do - first('.js-project-refs-dropdown').click - - page.within '.project-refs-form' do - click_link 'fix' - end - end - - step "I see the ref 'test' has been selected" do - expect(page).to have_selector '.dropdown-toggle-text', text: "'test'" - end - - step "I visit the 'test' tree" do - visit project_tree_path(@project, "'test'") - end - - step "I visit the fix tree" do - visit project_tree_path(@project, "fix/.testdir") - end - - step 'I see the commit data' do - expect(page).to have_css('.tree-commit-link', visible: true) - expect(page).not_to have_content('Loading commit data...') - end - - step 'I see the commit data for a directory with a leading dot' do - expect(page).to have_css('.tree-commit-link', visible: true) - expect(page).not_to have_content('Loading commit data...') - end - - step 'I click on "files/lfs/lfs_object.iso" file in repo' do - allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(true) - visit project_tree_path(@project, "lfs") - click_link 'files' - click_link "lfs" - click_link "lfs_object.iso" - end - - step 'I should see download link and object size' do - expect(page).to have_content 'Download (1.5 MB)' - end - - step 'I should not see lfs pointer details' do - expect(page).not_to have_content 'version https://git-lfs.github.com/spec/v1' - expect(page).not_to have_content 'oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' - expect(page).not_to have_content 'size 1575078' - end - - step 'I should see buttons for allowed commands' do - page.within '.content' do - expect(page).to have_link 'Download' - expect(page).to have_content 'History' - expect(page).to have_content 'Permalink' - expect(page).not_to have_content 'Edit' - expect(page).not_to have_content 'Blame' - expect(page).to have_content 'Delete' - expect(page).to have_content 'Replace' - end - end - - step 'I should see a Fork/Cancel combo' do - expect(page).to have_link 'Fork' - expect(page).to have_button 'Cancel' - end - - step 'I should see a notice about a new fork having been created' do - expect(page).to have_content "You're not allowed to make changes to this project directly. A fork of this project has been created that you can make changes in, so you can submit a merge request." - end - - # SVG files - step 'I upload a new SVG file' do - drop_in_dropzone test_svg_file - end - - step 'I visit the SVG file' do - visit project_blob_path(@project, 'new_branch_name/logo_sample.svg') - end - - step 'I can see the new rendered SVG image' do - expect(page).to have_css('.file-content img') - end - - private - - def set_new_content - find('#editor') - execute_script("ace.edit('editor').setValue('#{new_gitignore_content}')") - end - - # Content of the gitignore file on the seed repository. - def old_gitignore_content - '*.rbc' - end - - # Constant value that differs from the content - # of the gitignore of the seed repository. - def new_gitignore_content - old_gitignore_content + 'a' - end - - # Constant value that is a valid filename and - # not a filename present at root of the seed repository. - def new_file_name - 'not_a_file.md' - end - - # Constant value that is a valid filename with directory and - # not a filename present at root of the seed repository. - def new_file_name_with_directory - 'foo/bar/baz.txt' - end - - # Constant value that is a valid directory and - # not a directory present at root of the seed repository. - def new_dir_name - 'new_dir/subdir' - end - - def drop_in_dropzone(file_path) - # Generate a fake input selector - page.execute_script <<-JS - var fakeFileInput = window.$('<input/>').attr( - {id: 'fakeFileInput', type: 'file'} - ).appendTo('body'); - JS - # Attach the file to the fake input selector with Capybara - attach_file("fakeFileInput", file_path) - # Add the file to a fileList array and trigger the fake drop event - page.execute_script <<-JS - var fileList = [$('#fakeFileInput')[0].files[0]]; - var e = jQuery.Event('drop', { dataTransfer : { files : fileList } }); - $('.dropzone')[0].dropzone.listeners[0].events.drop(e); - JS - end - - def test_text_file - File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt') - end - - def test_image_file - File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') - end - - def test_svg_file - File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg') - end -end diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb deleted file mode 100644 index 104d024fee2..00000000000 --- a/features/steps/shared/active_tab.rb +++ /dev/null @@ -1,32 +0,0 @@ -module SharedActiveTab - include Spinach::DSL - include WaitForRequests - - after do - wait_for_requests if javascript_test? - end - - def ensure_active_main_tab(content) - expect(find('.sidebar-top-level-items > li.active')).to have_content(content) - end - - def ensure_active_sub_tab(content) - expect(find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)')).to have_content(content) - end - - def ensure_active_sub_nav(content) - expect(find('.layout-nav .controls li.active')).to have_content(content) - end - - step 'no other main tabs should be active' do - expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1) - end - - step 'no other sub tabs should be active' do - expect(page).to have_selector('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)', count: 1) - end - - step 'no other sub navs should be active' do - expect(page).to have_selector('.layout-nav .controls li.active', count: 1) - end -end diff --git a/features/steps/shared/admin.rb b/features/steps/shared/admin.rb deleted file mode 100644 index ac0a1764147..00000000000 --- a/features/steps/shared/admin.rb +++ /dev/null @@ -1,11 +0,0 @@ -module SharedAdmin - include Spinach::DSL - - step 'there are projects in system' do - 2.times { create(:project, :repository) } - end - - step 'system has users' do - 2.times { create(:user) } - end -end diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb deleted file mode 100644 index 27dd391b83d..00000000000 --- a/features/steps/shared/authentication.rb +++ /dev/null @@ -1,63 +0,0 @@ -require Rails.root.join('features', 'support', 'login_helpers') - -module SharedAuthentication - include Spinach::DSL - include LoginHelpers - - step 'I sign in as a user' do - sign_out(@user) if @user - - @user = create(:user) - sign_in(@user) - end - - step 'I sign in via the UI' do - gitlab_sign_in(create(:user)) - end - - step 'I sign in as an admin' do - sign_out(@user) if @user - - @user = create(:admin) - sign_in(@user) - end - - step 'I should be redirected to sign in page' do - expect(current_path).to eq new_user_session_path - end - - step "I logout directly" do - gitlab_sign_out - end - - def current_user - @user || User.reorder(nil).first - end - - private - - def gitlab_sign_in(user) - visit new_user_session_path - - fill_in "user_login", with: user.email - fill_in "user_password", with: "12345678" - check 'user_remember_me' - click_button "Sign in" - - @user = user - end - - def gitlab_sign_out - return unless @user - - if Capybara.current_driver == Capybara.javascript_driver - find('.header-user-dropdown-toggle').click - click_link 'Sign out' - expect(page).to have_button('Sign in') - else - sign_out(@user) - end - - @user = nil - end -end diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb deleted file mode 100644 index aa32528a7ca..00000000000 --- a/features/steps/shared/diff_note.rb +++ /dev/null @@ -1,237 +0,0 @@ -module SharedDiffNote - include Spinach::DSL - include RepoHelpers - include WaitForRequests - - after do - wait_for_requests if javascript_test? - end - - step 'I cancel the diff comment' do - page.within(diff_file_selector) do - find(".js-close-discussion-note-form").click - end - end - - step 'I delete a diff comment' do - find('.note').hover - find(".js-note-delete").click - end - - step 'I haven\'t written any diff comment text' do - page.within(diff_file_selector) do - fill_in "note[note]", with: "" - end - end - - step 'I leave a diff comment like "Typo, please fix"' do - page.within(diff_file_selector) do - click_diff_line(sample_commit.line_code) - - page.within("form[data-line-code='#{sample_commit.line_code}']") do - fill_in "note[note]", with: "Typo, please fix" - find(".js-comment-button").click - end - end - end - - step 'I leave a diff comment in a parallel view on the left side like "Old comment"' do - click_parallel_diff_line(sample_commit.del_line_code, 'old') - page.within("#{diff_file_selector} form[data-line-code='#{sample_commit.del_line_code}']") do - fill_in "note[note]", with: "Old comment" - find(".js-comment-button").click - end - end - - step 'I leave a diff comment in a parallel view on the right side like "New comment"' do - click_parallel_diff_line(sample_commit.line_code, 'new') - page.within("#{diff_file_selector} form[data-line-code='#{sample_commit.line_code}']") do - fill_in "note[note]", with: "New comment" - find(".js-comment-button").click - end - end - - step 'I preview a diff comment text like "Should fix it :smile:"' do - page.within(diff_file_selector) do - click_diff_line(sample_commit.line_code) - - page.within("form[data-line-code='#{sample_commit.line_code}']") do - fill_in "note[note]", with: "Should fix it :smile:" - find('.js-md-preview-button').click - end - end - end - - step 'I preview another diff comment text like "DRY this up"' do - page.within(diff_file_selector) do - click_diff_line(sample_commit.del_line_code) - - page.within("form[data-line-code='#{sample_commit.del_line_code}']") do - fill_in "note[note]", with: "DRY this up" - find('.js-md-preview-button').click - end - end - end - - step 'I open a diff comment form' do - page.within(diff_file_selector) do - click_diff_line(sample_commit.line_code) - end - end - - step 'I open another diff comment form' do - page.within(diff_file_selector) do - click_diff_line(sample_commit.del_line_code) - end - end - - step 'I write a diff comment like ":-1: I don\'t like this"' do - page.within(diff_file_selector) do - fill_in "note[note]", with: ":-1: I don\'t like this" - end - end - - step 'I write a diff comment like ":smile:"' do - page.within(diff_file_selector) do - click_diff_line(sample_commit.line_code) - - page.within("form[data-line-code='#{sample_commit.line_code}']") do - fill_in 'note[note]', with: ':smile:' - click_button('Comment') - end - end - end - - step 'I submit the diff comment' do - page.within(diff_file_selector) do - click_button("Comment") - end - end - - step 'I should not see the diff comment form' do - page.within(diff_file_selector) do - expect(page).not_to have_css("form.new_note") - end - end - - step 'The diff comment preview tab should say there is nothing to do' do - page.within(diff_file_selector) do - find('.js-md-preview-button').click - expect(find('.js-md-preview')).to have_content('Nothing to preview.') - end - end - - step 'I should not see the diff comment text field' do - page.within(diff_file_selector) do - expect(find('.js-note-text')).not_to be_visible - end - end - - step 'I should only see one diff form' do - page.within(diff_file_selector) do - expect(page).to have_css("form.new-note", count: 1) - end - end - - step 'I should see a diff comment form with ":-1: I don\'t like this"' do - page.within(diff_file_selector) do - expect(page).to have_field("note[note]", with: ":-1: I don\'t like this") - end - end - - step 'I should see a diff comment saying "Typo, please fix"' do - page.within("#{diff_file_selector} .note") do - expect(page).to have_content("Typo, please fix") - end - end - - step 'I should see a diff comment on the left side saying "Old comment"' do - page.within("#{diff_file_selector} .notes_content.parallel.old") do - expect(page).to have_content("Old comment") - end - end - - step 'I should see a diff comment on the right side saying "New comment"' do - page.within("#{diff_file_selector} .notes_content.parallel.new") do - expect(page).to have_content("New comment") - end - end - - step 'I should see a discussion reply button' do - page.within(diff_file_selector) do - expect(page).to have_button('Reply...') - end - end - - step 'I should see a temporary diff comment form' do - page.within(diff_file_selector) do - expect(page).to have_css(".js-temp-notes-holder form.new-note") - end - end - - step 'I should see an empty diff comment form' do - page.within(diff_file_selector) do - expect(page).to have_field("note[note]", with: "") - end - end - - step 'I should see the cancel comment button' do - page.within("#{diff_file_selector} form") do - expect(page).to have_css(".js-close-discussion-note-form", text: "Cancel") - end - end - - step 'I should see the diff comment preview' do - page.within("#{diff_file_selector} form") do - expect(page).to have_css('.js-md-preview', visible: true) - end - end - - step 'I should see the diff comment write tab' do - page.within(diff_file_selector) do - expect(page).to have_css('.js-md-write-button', visible: true) - end - end - - step 'The diff comment preview tab should display rendered Markdown' do - page.within(diff_file_selector) do - find('.js-md-preview-button').click - expect(find('.js-md-preview')).to have_css('gl-emoji', visible: true) - end - end - - step 'I should see two separate previews' do - page.within(diff_file_selector) do - expect(page).to have_css('.js-md-preview', visible: true, count: 2) - expect(page).to have_content('Should fix it') - expect(page).to have_content('DRY this up') - end - end - - step 'I should see a diff comment with an emoji image' do - page.within("#{diff_file_selector} .note") do - expect(page).to have_xpath("//gl-emoji[@data-name='smile']") - end - end - - step 'I click side-by-side diff button' do - find('#parallel-diff-btn').click - end - - step 'I see side-by-side diff button' do - expect(page).to have_content "Side-by-side" - end - - def diff_file_selector - '.diff-file:nth-of-type(1)' - end - - def click_diff_line(code) - find(".line_holder[id='#{code}'] button").click - end - - def click_parallel_diff_line(code, line_type) - find(".line_holder.parallel td[id='#{code}']").find(:xpath, 'preceding-sibling::*[1][self::td]').hover - find(".line_holder.parallel button[data-line-code='#{code}']").click - end -end diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb deleted file mode 100644 index 0126ce39c5a..00000000000 --- a/features/steps/shared/group.rb +++ /dev/null @@ -1,46 +0,0 @@ -module SharedGroup - include Spinach::DSL - - step 'current user is developer of group "Owned"' do - is_member_of(current_user.name, "Owned", Gitlab::Access::DEVELOPER) - end - - step '"John Doe" is guest of group "Guest"' do - is_member_of("John Doe", "Guest", Gitlab::Access::GUEST) - end - - step '"Mary Jane" is owner of group "Owned"' do - is_member_of("Mary Jane", "Owned", Gitlab::Access::OWNER) - end - - step '"Mary Jane" is guest of group "Owned"' do - is_member_of("Mary Jane", "Owned", Gitlab::Access::GUEST) - end - - step '"Mary Jane" is guest of group "Guest"' do - is_member_of("Mary Jane", "Guest", Gitlab::Access::GUEST) - end - - step 'I should see group "TestGroup"' do - expect(page).to have_content "TestGroup" - end - - step 'I should not see group "TestGroup"' do - expect(page).not_to have_content "TestGroup" - end - - protected - - def is_member_of(username, groupname, role) - user = User.find_by(name: username) || create(:user, name: username) - group = Group.find_by(name: groupname) || create(:group, name: groupname) - group.add_user(user, role) - project ||= create(:project, :repository, namespace: group) - create(:closed_issue_event, project: project) - project.add_master(user) - end - - def owned_group - @owned_group ||= Group.find_by(name: "Owned") - end -end diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb deleted file mode 100644 index cc6fd48935f..00000000000 --- a/features/steps/shared/issuable.rb +++ /dev/null @@ -1,78 +0,0 @@ -module SharedIssuable - include Spinach::DSL - - def edit_issuable - find('.js-issuable-edit', visible: true).click - end - - step 'I leave a comment referencing issue "Community issue"' do - leave_reference_comment( - issuable: Issue.find_by(title: 'Community issue'), - from_project_name: 'Enterprise' - ) - end - - step 'I click link "Edit" for the merge request' do - edit_issuable - end - - step 'I sort the list by "Least popular"' do - find('button.dropdown-toggle').click - - page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do - click_link 'Least popular' - end - end - - step 'I click link "Next" in the sidebar' do - page.within '.issuable-sidebar' do - click_link 'Next' - end - end - - def create_issuable_for_project(project_name:, title:, type: :issue) - project = Project.find_by(name: project_name) - - attrs = { - title: title, - author: project.users.first, - description: '# Description header' - } - - case type - when :issue - attrs[:project] = project - when :merge_request - attrs.merge!( - source_project: project, - target_project: project, - source_branch: 'fix', - target_branch: 'master' - ) - end - - create(type, attrs) - end - - def leave_reference_comment(issuable:, from_project_name:) - project = Project.find_by(name: from_project_name) - - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "##{issuable.to_reference(project)}" - click_button 'Comment' - end - end - - def visible_note(issuable:, from_project_name:, user_name:) - project = Project.find_by(name: from_project_name) - - expect(page).to have_content(user_name) - expect(page).to have_content("mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}") - end - - def expect_sidebar_content(content) - page.within '.issuable-sidebar' do - expect(page).to have_content content - end - end -end diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb deleted file mode 100644 index 65118f07ca2..00000000000 --- a/features/steps/shared/markdown.rb +++ /dev/null @@ -1,11 +0,0 @@ -module SharedMarkdown - include Spinach::DSL - - step 'I should not see the Markdown preview' do - expect(find('.gfm-form .js-md-preview')).not_to be_visible - end - - step 'I haven\'t written any description text' do - find('.gfm-form').fill_in 'Description', with: '' - end -end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb deleted file mode 100644 index 4a6dee3c7b8..00000000000 --- a/features/steps/shared/note.rb +++ /dev/null @@ -1,21 +0,0 @@ -module SharedNote - include Spinach::DSL - include WaitForRequests - - after do - wait_for_requests if javascript_test? - end - - step 'I haven\'t written any comment text' do - page.within(".js-main-target-form") do - fill_in "note[note]", with: "" - end - end - - step 'The comment preview tab should say there is nothing to do' do - page.within(".js-main-target-form") do - find('.js-md-preview-button').click - expect(find('.js-md-preview')).to have_content('Nothing to preview.') - end - end -end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb deleted file mode 100644 index a6bf7008955..00000000000 --- a/features/steps/shared/paths.rb +++ /dev/null @@ -1,421 +0,0 @@ -module SharedPaths - include Spinach::DSL - include RepoHelpers - include DashboardHelper - include WaitForRequests - - step 'I visit new project page' do - visit new_project_path - end - - step 'I visit login page' do - visit new_user_session_path - end - - # ---------------------------------------- - # User - # ---------------------------------------- - - step 'I visit user "John Doe" page' do - visit user_path("john_doe") - end - - # ---------------------------------------- - # Group - # ---------------------------------------- - - step 'I visit group "Owned" page' do - visit group_path(Group.find_by(name: "Owned")) - end - - step 'I visit group "Owned" activity page' do - visit activity_group_path(Group.find_by(name: "Owned")) - end - - step 'I visit group "Owned" issues page' do - visit issues_group_path(Group.find_by(name: "Owned")) - end - - step 'I visit group "Owned" merge requests page' do - visit merge_requests_group_path(Group.find_by(name: "Owned")) - end - - step 'I visit group "Owned" milestones page' do - visit group_milestones_path(Group.find_by(name: "Owned")) - end - - step 'I visit group "Owned" members page' do - visit group_group_members_path(Group.find_by(name: "Owned")) - end - - step 'I visit group "Owned" projects page' do - visit projects_group_path(Group.find_by(name: "Owned")) - end - - step 'I visit group "Guest" page' do - visit group_path(Group.find_by(name: "Guest")) - end - - step 'I visit group "Guest" issues page' do - visit issues_group_path(Group.find_by(name: "Guest")) - end - - step 'I visit group "Guest" merge requests page' do - visit merge_requests_group_path(Group.find_by(name: "Guest")) - end - - step 'I visit group "Guest" members page' do - visit group_group_members_path(Group.find_by(name: "Guest")) - end - - step 'I visit group "Guest" settings page' do - visit edit_group_path(Group.find_by(name: "Guest")) - end - - # ---------------------------------------- - # Dashboard - # ---------------------------------------- - - step 'I visit dashboard page' do - visit dashboard_projects_path - end - - step 'I visit dashboard activity page' do - visit activity_dashboard_path - end - - step 'I visit dashboard projects page' do - visit projects_dashboard_path - end - - step 'I visit dashboard issues page' do - visit assigned_issues_dashboard_path - end - - step 'I visit dashboard search page' do - visit search_path - end - - step 'I visit dashboard help page' do - visit help_path - end - - step 'I visit dashboard groups page' do - visit dashboard_groups_path - end - - step 'I should be redirected to the dashboard groups page' do - expect(current_path).to eq dashboard_groups_path - end - - step 'I visit dashboard starred projects page' do - visit starred_dashboard_projects_path - end - - # ---------------------------------------- - # Profile - # ---------------------------------------- - - step 'I visit profile page' do - visit profile_path - end - - step 'I visit profile applications page' do - visit applications_profile_path - end - - step 'I visit profile password page' do - visit edit_profile_password_path - end - - step 'I visit profile account page' do - visit profile_account_path - end - - step 'I visit profile SSH keys page' do - visit profile_keys_path - end - - step 'I visit profile preferences page' do - visit profile_preferences_path - end - - step 'I visit Authentication log page' do - visit audit_log_profile_path - end - - # ---------------------------------------- - # Admin - # ---------------------------------------- - - step 'I visit admin page' do - visit admin_root_path - end - - step 'I visit abuse reports page' do - visit admin_abuse_reports_path - end - - step 'I visit admin projects page' do - visit admin_projects_path - end - - step 'I visit admin users page' do - visit admin_users_path - end - - step 'I visit admin logs page' do - visit admin_logs_path - end - - step 'I visit admin messages page' do - visit admin_broadcast_messages_path - end - - step 'I visit admin hooks page' do - visit admin_hooks_path - end - - step 'I visit admin Resque page' do - visit admin_background_jobs_path - end - - step 'I visit admin teams page' do - visit admin_teams_path - end - - step 'I visit spam logs page' do - visit admin_spam_logs_path - end - - # ---------------------------------------- - # Generic Project - # ---------------------------------------- - - step "I visit my project's settings page" do - visit edit_project_path(@project) - end - - step 'I visit a binary file in the repo' do - visit project_blob_path(@project, - File.join(root_ref, 'files/images/logo-black.png')) - end - - step "I visit my project's commits page" do - visit project_commits_path(@project, root_ref, { limit: 5 }) - end - - step "I visit my project's commits page for a specific path" do - visit project_commits_path(@project, root_ref + "/files/ruby/regex.rb", { limit: 5 }) - end - - step 'I visit my project\'s commits stats page' do - visit stats_project_repository_path(@project) - end - - step "I visit my project's graph page" do - # Stub Graph max_size to speed up test (10 commits vs. 650) - Network::Graph.stub(max_count: 10) - - visit project_network_path(@project, root_ref) - end - - step "I visit my project's issues page" do - visit project_issues_path(@project) - end - - step "I visit my project's merge requests page" do - visit project_merge_requests_path(@project) - end - - step "I visit my project's members page" do - visit project_project_members_path(@project) - end - - step "I visit my project's wiki page" do - visit project_wiki_path(@project, :home) - end - - step 'I visit project hooks page' do - visit project_settings_integrations_path(@project) - end - - step 'I visit project find file page' do - visit project_find_file_path(@project, root_ref) - end - - # ---------------------------------------- - # "Shop" Project - # ---------------------------------------- - - step 'I visit project "Shop" page' do - visit project_path(project) - end - - step 'I visit project "Forked Shop" merge requests page' do - visit project_merge_requests_path(@forked_project) - end - - step 'I visit edit project "Shop" page' do - visit edit_project_path(project) - end - - step 'I visit compare refs page' do - visit project_compare_index_path(@project) - end - - step 'I visit project commits page' do - visit project_commits_path(@project, root_ref, { limit: 5 }) - end - - step 'I visit project commits page for stable branch' do - visit project_commits_path(@project, 'stable', { limit: 5 }) - end - - step 'I visit blob file from repo' do - visit project_blob_path(@project, File.join(sample_commit.id, sample_blob.path)) - end - - step 'I visit ".gitignore" file in repo' do - visit project_blob_path(@project, File.join(root_ref, '.gitignore')) - end - - step 'I am on the new file page' do - expect(current_path).to eq(project_create_blob_path(@project, root_ref)) - end - - step 'I am on the ".gitignore" edit file page' do - expect(current_path).to eq( - project_edit_blob_path(@project, File.join(root_ref, '.gitignore'))) - end - - step 'I visit project source page for "6d39438"' do - visit project_tree_path(@project, "6d39438") - end - - step 'I visit project source page for' \ - ' "6d394385cf567f80a8fd85055db1ab4c5295806f"' do - visit project_tree_path(@project, - '6d394385cf567f80a8fd85055db1ab4c5295806f') - end - - step 'I visit project tags page' do - visit project_tags_path(@project) - end - - step 'I visit project commit page' do - visit project_commit_path(@project, sample_commit.id) - end - - step 'I visit issue page "Release 0.4"' do - issue = Issue.find_by(title: "Release 0.4") - visit project_issue_path(issue.project, issue) - end - - step 'I visit project "Forum" labels page' do - project = Project.find_by(name: 'Forum') - visit project_labels_path(project) - end - - step 'I visit project "Shop" new label page' do - project = Project.find_by(name: 'Shop') - visit new_project_label_path(project) - end - - step 'I visit project "Forum" new label page' do - project = Project.find_by(name: 'Forum') - visit new_project_label_path(project) - end - - step 'I visit merge request page "Bug NS-04"' do - visit merge_request_path("Bug NS-04") - wait_for_requests - end - - step 'I visit merge request page "Bug NS-07"' do - visit merge_request_path("Bug NS-07") - wait_for_requests - end - - step 'I visit merge request page "Bug NS-08"' do - visit merge_request_path("Bug NS-08") - wait_for_requests - end - - step 'I visit merge request page "Bug CO-01"' do - mr = MergeRequest.find_by(title: "Bug CO-01") - visit project_merge_request_path(mr.target_project, mr) - wait_for_requests - end - - step 'I visit forked project "Shop" merge requests page' do - visit project_merge_requests_path(project) - end - - step 'I visit project "Shop" team page' do - visit project_project_members_path(project) - end - - step 'I visit project wiki page' do - visit project_wiki_path(@project, :home) - end - - # ---------------------------------------- - # Visibility Projects - # ---------------------------------------- - - step 'I visit project "Community" source page' do - project = Project.find_by(name: 'Community') - visit project_tree_path(project, root_ref) - end - - step 'I visit project "Internal" page' do - project = Project.find_by(name: "Internal") - visit project_path(project) - end - - step 'I visit project "Enterprise" page' do - project = Project.find_by(name: "Enterprise") - visit project_path(project) - end - - # ---------------------------------------- - # Empty Projects - # ---------------------------------------- - - step "I should not see command line instructions" do - expect(page).not_to have_css('.empty_wrapper') - end - - # ---------------------------------------- - # Public Projects - # ---------------------------------------- - step 'I visit the public groups area' do - visit explore_groups_path - end - - # ---------------------------------------- - # Snippets - # ---------------------------------------- - - step 'I visit project "Shop" snippets page' do - visit project_snippets_path(project) - end - - step 'I visit snippets page' do - visit explore_snippets_path - end - - def root_ref - @project.repository.root_ref - end - - def project - Project.find_by!(name: 'Shop') - end - - def merge_request_path(title) - mr = MergeRequest.find_by(title: title) - project_merge_request_path(mr.target_project, mr) - end -end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb deleted file mode 100644 index dbfb90fcc48..00000000000 --- a/features/steps/shared/project.rb +++ /dev/null @@ -1,109 +0,0 @@ -module SharedProject - include Spinach::DSL - - # Create a project without caring about what it's called - step "I own a project" do - @project = create(:project, :repository, namespace: @user.namespace) - @project.add_master(@user) - end - - step "I own a project in some group namespace" do - @group = create(:group, name: 'some group') - @project = create(:project, namespace: @group) - @project.add_master(@user) - end - - # Create a specific project called "Shop" - step 'I own project "Shop"' do - @project = Project.find_by(name: "Shop") - @project ||= create(:project, :repository, name: "Shop", namespace: @user.namespace) - @project.add_master(@user) - end - - def current_project - @project ||= Project.first - end - - # ---------------------------------------- - # Visibility of archived project - # ---------------------------------------- - - step 'I should not see project "Archive"' do - project = Project.find_by(name: "Archive") - expect(page).not_to have_content project.full_name - end - - step 'I should see project "Archive"' do - project = Project.find_by(name: "Archive") - expect(page).to have_content project.full_name - end - - # ---------------------------------------- - # Visibility level - # ---------------------------------------- - - step 'I should see project "Enterprise"' do - expect(page).to have_content "Enterprise" - end - - step 'I should not see project "Enterprise"' do - expect(page).not_to have_content "Enterprise" - end - - step 'internal project "Internal"' do - create(:project, :internal, :repository, name: 'Internal') - end - - step 'I should see project "Internal"' do - page.within '.js-projects-list-holder' do - expect(page).to have_content "Internal" - end - end - - step 'I should not see project "Internal"' do - page.within '.js-projects-list-holder' do - expect(page).not_to have_content "Internal" - end - end - - step 'I should see project "Community"' do - expect(page).to have_content "Community" - end - - step 'I should not see project "Community"' do - expect(page).not_to have_content "Community" - end - - step '"John Doe" owns private project "Enterprise"' do - user_owns_project( - user_name: 'John Doe', - project_name: 'Enterprise' - ) - end - - step '"John Doe" owns internal project "Internal"' do - user_owns_project( - user_name: 'John Doe', - project_name: 'Internal', - visibility: :internal - ) - end - - step 'public empty project "Empty Public Project"' do - create :project_empty_repo, :public, name: "Empty Public Project" - end - - step 'project "Shop" has labels: "bug", "feature", "enhancement"' do - project = Project.find_by(name: "Shop") - create(:label, project: project, title: 'bug') - create(:label, project: project, title: 'feature') - create(:label, project: project, title: 'enhancement') - end - - def user_owns_project(user_name:, project_name:, visibility: :private) - user = user_exists(user_name, username: user_name.gsub(/\s/, '').underscore) - project = Project.find_by(name: project_name) - project ||= create(:project, visibility, name: project_name, namespace: user.namespace) - project.add_master(user) - end -end diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb deleted file mode 100644 index 5a516ee33bc..00000000000 --- a/features/steps/shared/project_tab.rb +++ /dev/null @@ -1,66 +0,0 @@ -require_relative 'active_tab' - -module SharedProjectTab - include Spinach::DSL - include SharedActiveTab - - step 'the active main tab should be Project' do - ensure_active_main_tab('Overview') - end - - step 'the active main tab should be Repository' do - ensure_active_main_tab('Repository') - end - - step 'the active main tab should be Issues' do - ensure_active_main_tab('Issues') - end - - step 'the active sub tab should be Members' do - ensure_active_sub_tab('Members') - end - - step 'the active main tab should be Merge Requests' do - ensure_active_main_tab('Merge Requests') - end - - step 'the active main tab should be Snippets' do - ensure_active_main_tab('Snippets') - end - - step 'the active main tab should be Wiki' do - ensure_active_main_tab('Wiki') - end - - step 'the active main tab should be Members' do - ensure_active_main_tab('Members') - end - - step 'the active main tab should be Settings' do - ensure_active_main_tab('Settings') - end - - step 'the active sub tab should be Graph' do - ensure_active_sub_tab('Graph') - end - - step 'the active sub tab should be Files' do - ensure_active_sub_tab('Files') - end - - step 'the active sub tab should be Commits' do - ensure_active_sub_tab('Commits') - end - - step 'the active sub tab should be Home' do - ensure_active_sub_tab('Details') - end - - step 'the active sub tab should be Activity' do - ensure_active_sub_tab('Activity') - end - - step 'the active sub tab should be Charts' do - ensure_active_sub_tab('Charts') - end -end diff --git a/features/steps/shared/shortcuts.rb b/features/steps/shared/shortcuts.rb deleted file mode 100644 index a75a8474d26..00000000000 --- a/features/steps/shared/shortcuts.rb +++ /dev/null @@ -1,18 +0,0 @@ -module SharedShortcuts - include Spinach::DSL - - step 'I press "g" and "p"' do - find('body').native.send_key('g') - find('body').native.send_key('p') - end - - step 'I press "g" and "i"' do - find('body').native.send_key('g') - find('body').native.send_key('i') - end - - step 'I press "g" and "m"' do - find('body').native.send_key('g') - find('body').native.send_key('m') - end -end diff --git a/features/steps/shared/sidebar_active_tab.rb b/features/steps/shared/sidebar_active_tab.rb deleted file mode 100644 index 07fff16e867..00000000000 --- a/features/steps/shared/sidebar_active_tab.rb +++ /dev/null @@ -1,31 +0,0 @@ -module SharedSidebarActiveTab - include Spinach::DSL - - step 'no other main tabs should be active' do - expect(page).to have_selector('.nav-sidebar li.active', count: 1) - end - - def ensure_active_main_tab(content) - expect(find('.nav-sidebar li.active')).to have_content(content) - end - - step 'the active main tab should be Home' do - ensure_active_main_tab('Projects') - end - - step 'the active main tab should be Groups' do - ensure_active_main_tab('Groups') - end - - step 'the active main tab should be Projects' do - ensure_active_main_tab('Projects') - end - - step 'the active main tab should be Issues' do - ensure_active_main_tab('Issues') - end - - step 'the active main tab should be Merge Requests' do - ensure_active_main_tab('Merge Requests') - end -end diff --git a/features/steps/shared/user.rb b/features/steps/shared/user.rb deleted file mode 100644 index 9cadc91769d..00000000000 --- a/features/steps/shared/user.rb +++ /dev/null @@ -1,41 +0,0 @@ -module SharedUser - include Spinach::DSL - - step 'User "John Doe" exists' do - user_exists("John Doe", { username: "john_doe" }) - end - - step 'User "Mary Jane" exists' do - user_exists("Mary Jane", { username: "mary_jane" }) - end - - step 'gitlab user "Mike"' do - create(:user, name: "Mike") - end - - protected - - def user_exists(name, options = {}) - User.find_by(name: name) || create(:user, { name: name, admin: false }.merge(options)) - end - - step 'I have no ssh keys' do - @user.keys.delete_all - end - - step 'I click on "Personal projects" tab' do - page.within '.nav-links' do - click_link 'Personal projects' - end - - expect(page).to have_css('.tab-content #projects.active') - end - - step 'I click on "Contributed projects" tab' do - page.within '.nav-links' do - click_link 'Contributed projects' - end - - expect(page).to have_css('.tab-content #contributed.active') - end -end diff --git a/features/support/capybara.rb b/features/support/capybara.rb deleted file mode 100644 index 8879c9ab650..00000000000 --- a/features/support/capybara.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'capybara-screenshot/spinach' - -# Give CI some extra time -timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30 - -Capybara.register_driver :chrome do |app| - capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( - # This enables access to logs with `page.driver.manage.get_log(:browser)` - loggingPrefs: { - browser: "ALL", - client: "ALL", - driver: "ALL", - server: "ALL" - } - ) - - options = Selenium::WebDriver::Chrome::Options.new - options.add_argument("window-size=1240,1400") - - # Chrome won't work properly in a Docker container in sandbox mode - options.add_argument("no-sandbox") - - # Run headless by default unless CHROME_HEADLESS specified - options.add_argument("headless") unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i - - # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252 - options.add_argument("disable-dev-shm-usage") if ENV['CI'] || ENV['CI_SERVER'] - - Capybara::Selenium::Driver.new( - app, - browser: :chrome, - desired_capabilities: capabilities, - options: options - ) -end - -Capybara.javascript_driver = :chrome -Capybara.default_max_wait_time = timeout -Capybara.ignore_hidden_elements = false - -# Keep only the screenshots generated from the last failing test suite -Capybara::Screenshot.prune_strategy = :keep_last_run -# From https://github.com/mattheworiordan/capybara-screenshot/issues/84#issuecomment-41219326 -Capybara::Screenshot.register_driver(:chrome) do |driver, path| - driver.browser.save_screenshot(path) -end - -Spinach.hooks.before_run do - TestEnv.eager_load_driver_server -end diff --git a/features/support/db_cleaner.rb b/features/support/db_cleaner.rb deleted file mode 100644 index 31c922d23c3..00000000000 --- a/features/support/db_cleaner.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'database_cleaner' - -DatabaseCleaner[:active_record].strategy = :deletion - -Spinach.hooks.before_scenario do - DatabaseCleaner.start -end - -Spinach.hooks.after_scenario do - DatabaseCleaner.clean -end diff --git a/features/support/env.rb b/features/support/env.rb deleted file mode 100644 index 8fa2fcb6e3e..00000000000 --- a/features/support/env.rb +++ /dev/null @@ -1,60 +0,0 @@ -require './spec/simplecov_env' -SimpleCovEnv.start! - -ENV['RAILS_ENV'] = 'test' -require './config/environment' -require 'rspec/expectations' - -if ENV['CI'] - require 'knapsack' - Knapsack::Adapters::SpinachAdapter.bind -end - -WebMock.enable! - -%w(select2_helper test_env repo_helpers wait_for_requests project_forks_helper).each do |f| - require Rails.root.join('spec', 'support', 'helpers', f) -end - -%w(sidekiq webmock).each do |f| - require Rails.root.join('spec', 'support', f) -end - -Dir["#{Rails.root}/features/steps/shared/*.rb"].each { |file| require file } - -Spinach.hooks.before_run do - include RSpec::Mocks::ExampleMethods - include ActiveJob::TestHelper - include FactoryBot::Syntax::Methods - include GitlabRoutingHelper - - RSpec::Mocks.setup - TestEnv.init(mailer: false) - - # skip pre-receive hook check so we can use - # web editor and merge - TestEnv.disable_pre_receive -end - -Spinach.hooks.after_scenario do |scenario_data, step_definitions| - if scenario_data.tags.include?('javascript') - include WaitForRequests - block_and_wait_for_requests_complete - end -end - -module StdoutReporterWithScenarioLocation - # Override the standard reporter to show filename and line number next to each - # scenario for easy, focused re-runs - def before_scenario_run(scenario, step_definitions = nil) - @max_step_name_length = scenario.steps.map(&:name).map(&:length).max if scenario.steps.any? # rubocop:disable Gitlab/ModuleWithInstanceVariables - name = scenario.name - - # This number has no significance, it's just to line things up - max_length = @max_step_name_length + 19 # rubocop:disable Gitlab/ModuleWithInstanceVariables - out.puts "\n #{'Scenario:'.green} #{name.light_green.ljust(max_length)}" \ - " # #{scenario.feature.filename}:#{scenario.line}" - end -end - -Spinach::Reporter::Stdout.prepend(StdoutReporterWithScenarioLocation) diff --git a/features/support/gitaly.rb b/features/support/gitaly.rb deleted file mode 100644 index 3cd5f4ce497..00000000000 --- a/features/support/gitaly.rb +++ /dev/null @@ -1,3 +0,0 @@ -Spinach.hooks.before_scenario do - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true) -end diff --git a/features/support/login_helpers.rb b/features/support/login_helpers.rb deleted file mode 100644 index 540ff25a4f2..00000000000 --- a/features/support/login_helpers.rb +++ /dev/null @@ -1,19 +0,0 @@ -module LoginHelpers - # After inclusion, IntegrationHelpers calls these two methods that aren't - # supported by Spinach, so we perform the end results ourselves - class << self - def setup(*args) - Spinach.hooks.before_scenario do - Warden.test_mode! - end - end - - def teardown(*args) - Spinach.hooks.after_scenario do - Warden.test_reset! - end - end - end - - include Devise::Test::IntegrationHelpers -end diff --git a/features/support/rerun.rb b/features/support/rerun.rb deleted file mode 100644 index 60b78f9d050..00000000000 --- a/features/support/rerun.rb +++ /dev/null @@ -1,16 +0,0 @@ -# The spinach-rerun-reporter doesn't define the on_undefined_step -# See it here: https://github.com/javierav/spinach-rerun-reporter/blob/master/lib/spinach/reporter/rerun.rb -require 'spinach-rerun-reporter' - -module Spinach - class Reporter - class Rerun - def on_undefined_step(step_data, failure, step_definitions = nil) - super step_data, failure, step_definitions - - # save feature file and scenario line - @rerun << "#{current_feature.filename}:#{current_scenario.line}" - end - end - end -end diff --git a/lib/api/api.rb b/lib/api/api.rb index 5139e869c71..de20b2b8e67 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -8,14 +8,15 @@ module API PROJECT_ENDPOINT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze COMMIT_ENDPOINT_REQUIREMENTS = PROJECT_ENDPOINT_REQUIREMENTS.merge(sha: NO_SLASH_URL_PART_REGEX).freeze - use GrapeLogging::Middleware::RequestLogger, - logger: Logger.new(LOG_FILENAME), - formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new, - include: [ - GrapeLogging::Loggers::FilterParameters.new, - GrapeLogging::Loggers::ClientEnv.new, - Gitlab::GrapeLogging::Loggers::UserLogger.new - ] + insert_before Grape::Middleware::Error, + GrapeLogging::Middleware::RequestLogger, + logger: Logger.new(LOG_FILENAME), + formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new, + include: [ + GrapeLogging::Loggers::FilterParameters.new, + GrapeLogging::Loggers::ClientEnv.new, + Gitlab::GrapeLogging::Loggers::UserLogger.new + ] allow_access_with_scope :api prefix :api @@ -139,6 +140,7 @@ module API mount ::API::Keys mount ::API::Labels mount ::API::Lint + mount ::API::Markdown mount ::API::Members mount ::API::MergeRequestDiffs mount ::API::MergeRequests diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 25d78fc761d..174c5af91d5 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -125,7 +125,7 @@ module API # (fixed in https://github.com/rails/rails/pull/25976). project.tags.map(&:name).sort end - expose :ssh_url_to_repo, :http_url_to_repo, :web_url + expose :ssh_url_to_repo, :http_url_to_repo, :web_url, :readme_url expose :avatar_url do |project, options| project.avatar_url(only_path: false) end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 92e3d5cc10a..03b6b30a0d8 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -165,9 +165,12 @@ module API group = find_group!(params[:id]) authorize! :admin_group, group + Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/46285') destroy_conditionally!(group) do |group| - ::Groups::DestroyService.new(group, current_user).execute + ::Groups::DestroyService.new(group, current_user).async_execute end + + accepted! end desc 'Get a list of projects in this group.' do diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb index 09805049169..3308212216e 100644 --- a/lib/api/helpers/pagination.rb +++ b/lib/api/helpers/pagination.rb @@ -2,67 +2,240 @@ module API module Helpers module Pagination def paginate(relation) - relation = add_default_order(relation) + strategy = if params[:pagination] == 'keyset' && Feature.enabled?('api_keyset_pagination') + KeysetPaginationStrategy + else + DefaultPaginationStrategy + end - relation.page(params[:page]).per(params[:per_page]).tap do |data| - add_pagination_headers(data) - end + strategy.new(self).paginate(relation) end - private + class KeysetPaginationInfo + attr_reader :relation, :request_context - def add_pagination_headers(paginated_data) - header 'X-Per-Page', paginated_data.limit_value.to_s - header 'X-Page', paginated_data.current_page.to_s - header 'X-Next-Page', paginated_data.next_page.to_s - header 'X-Prev-Page', paginated_data.prev_page.to_s - header 'Link', pagination_links(paginated_data) + def initialize(relation, request_context) + # This is because it's rather complex to support multiple values with possibly different sort directions + # (and we don't need this in the API) + if relation.order_values.size > 1 + raise "Pagination only supports ordering by a single column." \ + "The following columns were given: #{relation.order_values.map { |v| v.expr.name }}" + end - return if data_without_counts?(paginated_data) + @relation = relation + @request_context = request_context + end - header 'X-Total', paginated_data.total_count.to_s - header 'X-Total-Pages', total_pages(paginated_data).to_s - end + def fields + keys.zip(values).reject { |_, v| v.nil? }.to_h + end - def pagination_links(paginated_data) - request_url = request.url.split('?').first - request_params = params.clone - request_params[:per_page] = paginated_data.limit_value + def column_for_order_by(relation) + relation.order_values.first&.expr&.name + end - links = [] + # Sort direction (`:asc` or `:desc`) + def sort + @sort ||= if order_by_primary_key? + # Default order is by id DESC + :desc + else + # API defaults to DESC order if param `sort` not present + request_context.params[:sort]&.to_sym || :desc + end + end - request_params[:page] = paginated_data.prev_page - links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") if request_params[:page] + # Do we only sort by primary key? + def order_by_primary_key? + keys.size == 1 && keys.first == primary_key + end - request_params[:page] = paginated_data.next_page - links << %(<#{request_url}?#{request_params.to_query}>; rel="next") if request_params[:page] + def primary_key + relation.model.primary_key.to_sym + end - request_params[:page] = 1 - links << %(<#{request_url}?#{request_params.to_query}>; rel="first") + def sort_ascending? + sort == :asc + end - unless data_without_counts?(paginated_data) - request_params[:page] = total_pages(paginated_data) - links << %(<#{request_url}?#{request_params.to_query}>; rel="last") + # Build hash of request parameters for a given record (relevant to pagination) + def params_for(record) + return {} unless record + + keys.each_with_object({}) do |key, h| + h["ks_prev_#{key}".to_sym] = record.attributes[key.to_s] + end end - links.join(', ') - end + private + + # All values present in request parameters that correspond to #keys. + def values + @values ||= keys.map do |key| + request_context.params["ks_prev_#{key}".to_sym] + end + end - def total_pages(paginated_data) - # Ensure there is in total at least 1 page - [paginated_data.total_pages, 1].max + # All keys relevant to pagination. + # This always includes the primary key. Optionally, the `order_by` key is prepended. + def keys + @keys ||= [column_for_order_by(relation), primary_key].compact.uniq + end end - def add_default_order(relation) - if relation.is_a?(ActiveRecord::Relation) && relation.order_values.empty? - relation = relation.order(:id) + class KeysetPaginationStrategy + attr_reader :request_context + delegate :params, :header, :request, to: :request_context + + def initialize(request_context) + @request_context = request_context + end + + def paginate(relation) + pagination = KeysetPaginationInfo.new(relation, request_context) + + paged_relation = relation.limit(per_page) + + if conds = conditions(pagination) + paged_relation = paged_relation.where(*conds) + end + + # In all cases: sort by primary key (possibly in addition to another sort column) + paged_relation = paged_relation.order(pagination.primary_key => pagination.sort) + + add_default_pagination_headers + + if last_record = paged_relation.last + next_page_params = pagination.params_for(last_record) + add_navigation_links(next_page_params) + end + + paged_relation + end + + private + + def conditions(pagination) + fields = pagination.fields + + return nil if fields.empty? + + placeholder = fields.map { '?' } + + comp = if pagination.sort_ascending? + '>' + else + '<' + end + + [ + # Row value comparison: + # (A, B) < (a, b) <=> (A < a) OR (A = a AND B < b) + # <=> A <= a AND ((A < a) OR (A = a AND B < b)) + "(#{fields.keys.join(',')}) #{comp} (#{placeholder.join(',')})", + *fields.values + ] + end + + def per_page + params[:per_page] + end + + def add_default_pagination_headers + header 'X-Per-Page', per_page.to_s + end + + def add_navigation_links(next_page_params) + header 'X-Next-Page', page_href(next_page_params) + header 'Link', link_for('next', next_page_params) end - relation + def page_href(next_page_params) + request_url = request.url.split('?').first + request_params = params.dup + request_params[:per_page] = per_page + + request_params.merge!(next_page_params) if next_page_params + + "#{request_url}?#{request_params.to_query}" + end + + def link_for(rel, next_page_params) + %(<#{page_href(next_page_params)}>; rel="#{rel}") + end end - def data_without_counts?(paginated_data) - paginated_data.is_a?(Kaminari::PaginatableWithoutCount) + class DefaultPaginationStrategy + attr_reader :request_context + delegate :params, :header, :request, to: :request_context + + def initialize(request_context) + @request_context = request_context + end + + def paginate(relation) + relation = add_default_order(relation) + + relation.page(params[:page]).per(params[:per_page]).tap do |data| + add_pagination_headers(data) + end + end + + private + + def add_default_order(relation) + if relation.is_a?(ActiveRecord::Relation) && relation.order_values.empty? + relation = relation.order(:id) + end + + relation + end + + def add_pagination_headers(paginated_data) + header 'X-Per-Page', paginated_data.limit_value.to_s + header 'X-Page', paginated_data.current_page.to_s + header 'X-Next-Page', paginated_data.next_page.to_s + header 'X-Prev-Page', paginated_data.prev_page.to_s + header 'Link', pagination_links(paginated_data) + + return if data_without_counts?(paginated_data) + + header 'X-Total', paginated_data.total_count.to_s + header 'X-Total-Pages', total_pages(paginated_data).to_s + end + + def pagination_links(paginated_data) + request_url = request.url.split('?').first + request_params = params.clone + request_params[:per_page] = paginated_data.limit_value + + links = [] + + request_params[:page] = paginated_data.prev_page + links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") if request_params[:page] + + request_params[:page] = paginated_data.next_page + links << %(<#{request_url}?#{request_params.to_query}>; rel="next") if request_params[:page] + + request_params[:page] = 1 + links << %(<#{request_url}?#{request_params.to_query}>; rel="first") + + unless data_without_counts?(paginated_data) + request_params[:page] = total_pages(paginated_data) + links << %(<#{request_url}?#{request_params.to_query}>; rel="last") + end + + links.join(', ') + end + + def total_pages(paginated_data) + # Ensure there is in total at least 1 page + [paginated_data.total_pages, 1].max + end + + def data_without_counts?(paginated_data) + paginated_data.is_a?(Kaminari::PaginatableWithoutCount) + end end end end diff --git a/lib/api/helpers/related_resources_helpers.rb b/lib/api/helpers/related_resources_helpers.rb index 7f4d6e58b34..a2cbed30229 100644 --- a/lib/api/helpers/related_resources_helpers.rb +++ b/lib/api/helpers/related_resources_helpers.rb @@ -13,9 +13,14 @@ module API def expose_url(path) url_options = Gitlab::Application.routes.default_url_options - protocol, host, port = url_options.slice(:protocol, :host, :port).values + protocol, host, port, script_name = url_options.values_at(:protocol, :host, :port, :script_name) - URI::Generic.build(scheme: protocol, host: host, port: port, path: path).to_s + # Using a blank component at the beginning of the join we ensure + # that the resulted path will start with '/'. If the resulted path + # does not start with '/', URI::Generic#build will fail + path_with_script_name = File.join('', [script_name, path].select(&:present?)) + + URI::Generic.build(scheme: protocol, host: host, port: port, path: path_with_script_name).to_s end private diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 6b72caea8fd..a3dac36b8b6 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -113,7 +113,7 @@ module API { api_version: API.version, gitlab_version: Gitlab::VERSION, - gitlab_rev: Gitlab::REVISION, + gitlab_rev: Gitlab.revision, redis: redis_ping } end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 12ff2a1398b..6d75e8817c4 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -2,7 +2,7 @@ module API class Issues < Grape::API include PaginationParams - before { authenticate! } + before { authenticate_non_get! } helpers ::Gitlab::IssuableMetadata @@ -13,6 +13,7 @@ module API args.delete(:id) args[:milestone_title] = args.delete(:milestone) args[:label_name] = args.delete(:labels) + args[:scope] = args[:scope].underscore if args[:scope] issues = IssuesFinder.new(current_user, args).execute .preload(:assignees, :labels, :notes, :timelogs) @@ -36,8 +37,8 @@ module API optional :updated_before, type: DateTime, desc: 'Return issues updated before the specified time' optional :author_id, type: Integer, desc: 'Return issues which are authored by the user with the given ID' optional :assignee_id, type: Integer, desc: 'Return issues which are assigned to the user with the given ID' - optional :scope, type: String, values: %w[created-by-me assigned-to-me all], - desc: 'Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all`' + optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], + desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`' optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji' use :pagination end @@ -66,10 +67,11 @@ module API optional :state, type: String, values: %w[opened closed all], default: 'all', desc: 'Return opened, closed, or all issues' use :issues_params - optional :scope, type: String, values: %w[created-by-me assigned-to-me all], default: 'created-by-me', - desc: 'Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all`' + optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], default: 'created_by_me', + desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`' end get do + authenticate! unless params[:scope] == 'all' issues = paginate(find_issues) options = { diff --git a/lib/api/markdown.rb b/lib/api/markdown.rb new file mode 100644 index 00000000000..b9ed68aa584 --- /dev/null +++ b/lib/api/markdown.rb @@ -0,0 +1,33 @@ +module API + class Markdown < Grape::API + params do + requires :text, type: String, desc: "The markdown text to render" + optional :gfm, type: Boolean, desc: "Render text using GitLab Flavored Markdown" + optional :project, type: String, desc: "The full path of a project to use as the context when creating references using GitLab Flavored Markdown" + end + resource :markdown do + desc "Render markdown text" do + detail "This feature was introduced in GitLab 11.0." + end + post do + # Explicitly set CommonMark as markdown engine to use. + # Remove this set when https://gitlab.com/gitlab-org/gitlab-ce/issues/43011 is done. + context = { markdown_engine: :common_mark, only_path: false } + + if params[:project] + project = Project.find_by_full_path(params[:project]) + + not_found!("Project") unless can?(current_user, :read_project, project) + + context[:project] = project + else + context[:skip_project_check] = true + end + + context[:pipeline] = params[:gfm] ? :full : :plain_markdown + + { html: Banzai.render(params[:text], context) } + end + end + end +end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index d4cc18f622b..bc4df16e3a8 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -38,6 +38,7 @@ module API args[:milestone_title] = args.delete(:milestone) args[:label_name] = args.delete(:labels) + args[:scope] = args[:scope].underscore if args[:scope] merge_requests = MergeRequestsFinder.new(current_user, args).execute .reorder(args[:order_by] => args[:sort]) @@ -79,8 +80,8 @@ module API optional :view, type: String, values: %w[simple], desc: 'If simple, returns the `iid`, URL, title, description, and basic state of merge request' optional :author_id, type: Integer, desc: 'Return merge requests which are authored by the user with the given ID' optional :assignee_id, type: Integer, desc: 'Return merge requests which are assigned to the user with the given ID' - optional :scope, type: String, values: %w[created-by-me assigned-to-me all], - desc: 'Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`' + optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], + desc: 'Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`' optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji' optional :source_branch, type: String, desc: 'Return merge requests with the given source branch' optional :target_branch, type: String, desc: 'Return merge requests with the given target branch' @@ -95,8 +96,8 @@ module API end params do use :merge_requests_params - optional :scope, type: String, values: %w[created-by-me assigned-to-me all], default: 'created-by-me', - desc: 'Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`' + optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], default: 'created_by_me', + desc: 'Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`' end get do authenticate! unless params[:scope] == 'all' diff --git a/lib/api/milestone_responses.rb b/lib/api/milestone_responses.rb index c570eace862..a8eb137e46a 100644 --- a/lib/api/milestone_responses.rb +++ b/lib/api/milestone_responses.rb @@ -24,7 +24,7 @@ module API optional :state_event, type: String, values: %w[close activate], desc: 'The state event of the milestone ' use :optional_params - at_least_one_of :title, :description, :due_date, :state_event + at_least_one_of :title, :description, :start_date, :due_date, :state_event end def list_milestones_for(parent) diff --git a/lib/api/runner.rb b/lib/api/runner.rb index 649feba1036..a7f1cb1131f 100644 --- a/lib/api/runner.rb +++ b/lib/api/runner.rb @@ -149,6 +149,7 @@ module API end patch '/:id/trace' do job = authenticate_job! + forbidden!('Job is not running') unless job.running? error!('400 Missing header Content-Range', 400) unless request.headers.key?('Content-Range') content_range = request.headers['Content-Range'] diff --git a/lib/api/runners.rb b/lib/api/runners.rb index 5f2a9567605..5cb96d467c0 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -14,7 +14,7 @@ module API use :pagination end get do - runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: %w(specific shared)) + runners = filter_runners(current_user.ci_owned_runners, params[:scope], without: %w(specific shared)) present paginate(runners), with: Entities::Runner end @@ -184,40 +184,35 @@ module API def authenticate_show_runner!(runner) return if runner.is_shared || current_user.admin? - forbidden!("No access granted") unless user_can_access_runner?(runner) + forbidden!("No access granted") unless can?(current_user, :read_runner, runner) end def authenticate_update_runner!(runner) return if current_user.admin? - forbidden!("Runner is shared") if runner.is_shared? - forbidden!("No access granted") unless user_can_access_runner?(runner) + forbidden!("No access granted") unless can?(current_user, :update_runner, runner) end def authenticate_delete_runner!(runner) return if current_user.admin? - forbidden!("Runner is shared") if runner.is_shared? forbidden!("Runner associated with more than one project") if runner.projects.count > 1 - forbidden!("No access granted") unless user_can_access_runner?(runner) + forbidden!("No access granted") unless can?(current_user, :delete_runner, runner) end def authenticate_enable_runner!(runner) - forbidden!("Runner is shared") if runner.is_shared? - forbidden!("Runner is locked") if runner.locked? + forbidden!("Runner is a group runner") if runner.group_type? + return if current_user.admin? - forbidden!("No access granted") unless user_can_access_runner?(runner) + forbidden!("Runner is locked") if runner.locked? + forbidden!("No access granted") unless can?(current_user, :assign_runner, runner) end def authenticate_list_runners_jobs!(runner) return if current_user.admin? - forbidden!("No access granted") unless user_can_access_runner?(runner) - end - - def user_can_access_runner?(runner) - current_user.ci_authorized_runners.exists?(runner.id) + forbidden!("No access granted") unless can?(current_user, :read_runner, runner) end end end diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 152df23a327..e31c332b6e4 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -5,7 +5,7 @@ module API helpers do def current_settings @current_setting ||= - (ApplicationSetting.current || ApplicationSetting.create_from_defaults) + (ApplicationSetting.current_without_cache || ApplicationSetting.create_from_defaults) end end diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb index 2c52d21fa1c..4fa7d196e50 100644 --- a/lib/api/v3/groups.rb +++ b/lib/api/v3/groups.rb @@ -131,7 +131,9 @@ module API delete ":id" do group = find_group!(params[:id]) authorize! :admin_group, group - present ::Groups::DestroyService.new(group, current_user).execute, with: Entities::GroupDetail, current_user: current_user + ::Groups::DestroyService.new(group, current_user).async_execute + + accepted! end desc 'Get a list of projects in this group.' do diff --git a/lib/api/v3/runners.rb b/lib/api/v3/runners.rb index c6d9957d452..8a5c46805bd 100644 --- a/lib/api/v3/runners.rb +++ b/lib/api/v3/runners.rb @@ -58,7 +58,7 @@ module API end def user_can_access_runner?(runner) - current_user.ci_authorized_runners.exists?(runner.id) + current_user.ci_owned_runners.exists?(runner.id) end end end diff --git a/lib/api/v3/settings.rb b/lib/api/v3/settings.rb index 9b4ab7630fb..fc56495c8b1 100644 --- a/lib/api/v3/settings.rb +++ b/lib/api/v3/settings.rb @@ -6,7 +6,7 @@ module API helpers do def current_settings @current_setting ||= - (ApplicationSetting.current || ApplicationSetting.create_from_defaults) + (ApplicationSetting.current_without_cache || ApplicationSetting.create_from_defaults) end end diff --git a/lib/api/version.rb b/lib/api/version.rb index 9ba576bd828..3b10bfa6a7d 100644 --- a/lib/api/version.rb +++ b/lib/api/version.rb @@ -6,7 +6,7 @@ module API detail 'This feature was introduced in GitLab 8.13.' end get '/version' do - { version: Gitlab::VERSION, revision: Gitlab::REVISION } + { version: Gitlab::VERSION, revision: Gitlab.revision } end end end diff --git a/lib/backup/artifacts.rb b/lib/backup/artifacts.rb index 6a5a223a614..45a935ab352 100644 --- a/lib/backup/artifacts.rb +++ b/lib/backup/artifacts.rb @@ -2,7 +2,11 @@ require 'backup/files' module Backup class Artifacts < Files - def initialize + attr_reader :progress + + def initialize(progress) + @progress = progress + super('artifacts', JobArtifactUploader.root) end end diff --git a/lib/backup/builds.rb b/lib/backup/builds.rb index f869916e199..adf85ca4719 100644 --- a/lib/backup/builds.rb +++ b/lib/backup/builds.rb @@ -2,7 +2,11 @@ require 'backup/files' module Backup class Builds < Files - def initialize + attr_reader :progress + + def initialize(progress) + @progress = progress + super('builds', Settings.gitlab_ci.builds_path) end end diff --git a/lib/backup/database.rb b/lib/backup/database.rb index 5e6828de597..1608f7ad02d 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -2,9 +2,11 @@ require 'yaml' module Backup class Database + attr_reader :progress attr_reader :config, :db_file_name - def initialize + def initialize(progress) + @progress = progress @config = YAML.load_file(File.join(Rails.root, 'config', 'database.yml'))[Rails.env] @db_file_name = File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz') end @@ -19,12 +21,12 @@ module Backup dump_pid = case config["adapter"] when /^mysql/ then - $progress.print "Dumping MySQL database #{config['database']} ... " + progress.print "Dumping MySQL database #{config['database']} ... " # Workaround warnings from MySQL 5.6 about passwords on cmd line ENV['MYSQL_PWD'] = config["password"].to_s if config["password"] spawn('mysqldump', *mysql_args, config['database'], out: compress_wr) when "postgresql" then - $progress.print "Dumping PostgreSQL database #{config['database']} ... " + progress.print "Dumping PostgreSQL database #{config['database']} ... " pg_env pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump. if Gitlab.config.backup.pg_schema @@ -53,12 +55,12 @@ module Backup restore_pid = case config["adapter"] when /^mysql/ then - $progress.print "Restoring MySQL database #{config['database']} ... " + progress.print "Restoring MySQL database #{config['database']} ... " # Workaround warnings from MySQL 5.6 about passwords on cmd line ENV['MYSQL_PWD'] = config["password"].to_s if config["password"] spawn('mysql', *mysql_args, config['database'], in: decompress_rd) when "postgresql" then - $progress.print "Restoring PostgreSQL database #{config['database']} ... " + progress.print "Restoring PostgreSQL database #{config['database']} ... " pg_env spawn('psql', config['database'], in: decompress_rd) end @@ -111,9 +113,9 @@ module Backup def report_success(success) if success - $progress.puts '[DONE]'.color(:green) + progress.puts '[DONE]'.color(:green) else - $progress.puts '[FAILED]'.color(:red) + progress.puts '[FAILED]'.color(:red) end end end diff --git a/lib/backup/lfs.rb b/lib/backup/lfs.rb index 4e234e50a7a..185ff8ae6bd 100644 --- a/lib/backup/lfs.rb +++ b/lib/backup/lfs.rb @@ -2,7 +2,11 @@ require 'backup/files' module Backup class Lfs < Files - def initialize + attr_reader :progress + + def initialize(progress) + @progress = progress + super('lfs', Settings.lfs.storage_path) end end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index f27ce4d2b2b..a8da0c7edef 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -4,6 +4,12 @@ module Backup FOLDERS_TO_BACKUP = %w[repositories db].freeze FILE_NAME_SUFFIX = '_gitlab_backup.tar'.freeze + attr_reader :progress + + def initialize(progress) + @progress = progress + end + def pack # Make sure there is a connection ActiveRecord::Base.connection.reconnect! @@ -14,11 +20,11 @@ module Backup end # create archive - $progress.print "Creating backup archive: #{tar_file} ... " + progress.print "Creating backup archive: #{tar_file} ... " # Set file permissions on open to prevent chmod races. tar_system_options = { out: [tar_file, 'w', Gitlab.config.backup.archive_permissions] } if Kernel.system('tar', '-cf', '-', *backup_contents, tar_system_options) - $progress.puts "done".color(:green) + progress.puts "done".color(:green) else puts "creating archive #{tar_file} failed".color(:red) abort 'Backup failed' @@ -29,11 +35,11 @@ module Backup end def upload - $progress.print "Uploading backup archive to remote storage #{remote_directory} ... " + progress.print "Uploading backup archive to remote storage #{remote_directory} ... " connection_settings = Gitlab.config.backup.upload.connection if connection_settings.blank? - $progress.puts "skipped".color(:yellow) + progress.puts "skipped".color(:yellow) return end @@ -43,7 +49,7 @@ module Backup multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size, encryption: Gitlab.config.backup.upload.encryption, storage_class: Gitlab.config.backup.upload.storage_class) - $progress.puts "done".color(:green) + progress.puts "done".color(:green) else puts "uploading backup to #{remote_directory} failed".color(:red) abort 'Backup failed' @@ -51,13 +57,13 @@ module Backup end def cleanup - $progress.print "Deleting tmp directories ... " + progress.print "Deleting tmp directories ... " backup_contents.each do |dir| next unless File.exist?(File.join(backup_path, dir)) if FileUtils.rm_rf(File.join(backup_path, dir)) - $progress.puts "done".color(:green) + progress.puts "done".color(:green) else puts "deleting tmp directory '#{dir}' failed".color(:red) abort 'Backup failed' @@ -67,7 +73,7 @@ module Backup def remove_old # delete backups - $progress.print "Deleting old backups ... " + progress.print "Deleting old backups ... " keep_time = Gitlab.config.backup.keep_time.to_i if keep_time > 0 @@ -88,31 +94,32 @@ module Backup FileUtils.rm(file) removed += 1 rescue => e - $progress.puts "Deleting #{file} failed: #{e.message}".color(:red) + progress.puts "Deleting #{file} failed: #{e.message}".color(:red) end end end end - $progress.puts "done. (#{removed} removed)".color(:green) + progress.puts "done. (#{removed} removed)".color(:green) else - $progress.puts "skipping".color(:yellow) + progress.puts "skipping".color(:yellow) end end + # rubocop: disable Metrics/AbcSize def unpack Dir.chdir(backup_path) do # check for existing backups in the backup dir if backup_file_list.empty? - $progress.puts "No backups found in #{backup_path}" - $progress.puts "Please make sure that file name ends with #{FILE_NAME_SUFFIX}" + progress.puts "No backups found in #{backup_path}" + progress.puts "Please make sure that file name ends with #{FILE_NAME_SUFFIX}" exit 1 elsif backup_file_list.many? && ENV["BACKUP"].nil? - $progress.puts 'Found more than one backup:' + progress.puts 'Found more than one backup:' # print list of available backups - $progress.puts " " + available_timestamps.join("\n ") - $progress.puts 'Please specify which one you want to restore:' - $progress.puts 'rake gitlab:backup:restore BACKUP=timestamp_of_backup' + progress.puts " " + available_timestamps.join("\n ") + progress.puts 'Please specify which one you want to restore:' + progress.puts 'rake gitlab:backup:restore BACKUP=timestamp_of_backup' exit 1 end @@ -123,31 +130,31 @@ module Backup end unless File.exist?(tar_file) - $progress.puts "The backup file #{tar_file} does not exist!" + progress.puts "The backup file #{tar_file} does not exist!" exit 1 end - $progress.print 'Unpacking backup ... ' + progress.print 'Unpacking backup ... ' unless Kernel.system(*%W(tar -xf #{tar_file})) - $progress.puts 'unpacking backup failed'.color(:red) + progress.puts 'unpacking backup failed'.color(:red) exit 1 else - $progress.puts 'done'.color(:green) + progress.puts 'done'.color(:green) end ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0 # restoring mismatching backups can lead to unexpected problems if settings[:gitlab_version] != Gitlab::VERSION - $progress.puts(<<~HEREDOC.color(:red)) + progress.puts(<<~HEREDOC.color(:red)) GitLab version mismatch: Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup! Please switch to the following version and try again: version: #{settings[:gitlab_version]} HEREDOC - $progress.puts - $progress.puts "Hint: git checkout v#{settings[:gitlab_version]}" + progress.puts + progress.puts "Hint: git checkout v#{settings[:gitlab_version]}" exit 1 end end diff --git a/lib/backup/pages.rb b/lib/backup/pages.rb index 5830b209d6e..542e35a7c7c 100644 --- a/lib/backup/pages.rb +++ b/lib/backup/pages.rb @@ -2,7 +2,11 @@ require 'backup/files' module Backup class Pages < Files - def initialize + attr_reader :progress + + def initialize(progress) + @progress = progress + super('pages', Gitlab.config.pages.path) end end diff --git a/lib/backup/registry.rb b/lib/backup/registry.rb index 91698669402..35821805797 100644 --- a/lib/backup/registry.rb +++ b/lib/backup/registry.rb @@ -2,7 +2,11 @@ require 'backup/files' module Backup class Registry < Files - def initialize + attr_reader :progress + + def initialize(progress) + @progress = progress + super('registry', Settings.registry.path) end end diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 65e06fd78c0..c3360c391af 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -6,6 +6,12 @@ module Backup include Backup::Helper # rubocop:disable Metrics/AbcSize + attr_reader :progress + + def initialize(progress) + @progress = progress + end + def dump prepare @@ -67,6 +73,9 @@ module Backup end def prepare_directories + # TODO: Need to find a way to do this for gitaly + # Gitaly discussion issue: https://gitlab.com/gitlab-org/gitaly/issues/1194 + Gitlab.config.repositories.storages.each do |name, repository_storage| path = repository_storage.legacy_disk_path next unless File.exist?(path) @@ -87,70 +96,65 @@ module Backup end end + def restore_custom_hooks(project) + # TODO: Need to find a way to do this for gitaly + # Gitaly migration issue: https://gitlab.com/gitlab-org/gitaly/issues/1195 + in_path(path_to_tars(project)) do |dir| + path_to_project_repo = path_to_repo(project) + cmd = %W(tar -xf #{path_to_tars(project, dir)} -C #{path_to_project_repo} #{dir}) + + output, status = Gitlab::Popen.popen(cmd) + unless status.zero? + progress_warn(project, cmd.join(' '), output) + end + end + end + def restore prepare_directories + gitlab_shell = Gitlab::Shell.new Project.find_each(batch_size: 1000) do |project| - progress.print " * #{display_repo_path(project)} ... " - path_to_project_repo = path_to_repo(project) + progress.print " * #{project.full_path} ... " path_to_project_bundle = path_to_bundle(project) - project.ensure_storage_path_exists - cmd = if File.exist?(path_to_project_bundle) - %W(#{Gitlab.config.git.bin_path} clone --bare --mirror #{path_to_project_bundle} #{path_to_project_repo}) - else - %W(#{Gitlab.config.git.bin_path} init --bare #{path_to_project_repo}) - end + restore_repo_success = nil + if File.exist?(path_to_project_bundle) + begin + gitlab_shell.remove_repository(project.repository_storage, project.disk_path) if project.repository_exists? + project.repository.create_from_bundle path_to_project_bundle + restore_repo_success = true + rescue => e + restore_repo_success = false + progress.puts "Error: #{e}".color(:red) + end + else + restore_repo_success = gitlab_shell.create_repository(project.repository_storage, project.disk_path) + end - output, status = Gitlab::Popen.popen(cmd) - if status.zero? + if restore_repo_success progress.puts "[DONE]".color(:green) else - progress_warn(project, cmd.join(' '), output) + progress.puts "[Failed] restoring #{project.full_path} repository".color(:red) end - in_path(path_to_tars(project)) do |dir| - cmd = %W(tar -xf #{path_to_tars(project, dir)} -C #{path_to_project_repo} #{dir}) - - output, status = Gitlab::Popen.popen(cmd) - unless status.zero? - progress_warn(project, cmd.join(' '), output) - end - end + restore_custom_hooks(project) wiki = ProjectWiki.new(project) - path_to_wiki_repo = path_to_repo(wiki) path_to_wiki_bundle = path_to_bundle(wiki) if File.exist?(path_to_wiki_bundle) - progress.print " * #{display_repo_path(wiki)} ... " - - # If a wiki bundle exists, first remove the empty repo - # that was initialized with ProjectWiki.new() and then - # try to restore with 'git clone --bare'. - FileUtils.rm_rf(path_to_wiki_repo) - cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_wiki_bundle} #{path_to_wiki_repo}) - - output, status = Gitlab::Popen.popen(cmd) - if status.zero? - progress.puts " [DONE]".color(:green) - else - progress_warn(project, cmd.join(' '), output) + progress.print " * #{wiki.full_path} ... " + begin + gitlab_shell.remove_repository(wiki.repository_storage, wiki.disk_path) if wiki.repository_exists? + wiki.repository.create_from_bundle(path_to_wiki_bundle) + progress.puts "[DONE]".color(:green) + rescue => e + progress.puts "[Failed] restoring #{wiki.full_path} wiki".color(:red) + progress.puts "Error #{e}".color(:red) end end end - - progress.print 'Put GitLab hooks in repositories dirs'.color(:yellow) - cmd = %W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args - - output, status = Gitlab::Popen.popen(cmd) - if status.zero? - progress.puts " [DONE]".color(:green) - else - puts " [FAILED]".color(:red) - puts "failed: #{cmd}" - puts output - end end # rubocop:enable Metrics/AbcSize @@ -217,10 +221,6 @@ module Backup Gitlab.config.repositories.storages.values.map { |rs| rs.legacy_disk_path } end - def progress - $progress - end - def display_repo_path(project) project.hashed_storage?(:repository) ? "#{project.full_path} (#{project.disk_path})" : project.full_path end diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb index d46e2cd869d..49b117a7ee3 100644 --- a/lib/backup/uploads.rb +++ b/lib/backup/uploads.rb @@ -2,7 +2,11 @@ require 'backup/files' module Backup class Uploads < Files - def initialize + attr_reader :progress + + def initialize(progress) + @progress = progress + super('uploads', Rails.root.join('public/uploads')) end end diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb index f2e9a5a1116..4bc82ecb4d6 100644 --- a/lib/banzai/filter/gollum_tags_filter.rb +++ b/lib/banzai/filter/gollum_tags_filter.rb @@ -58,6 +58,9 @@ module Banzai def call doc.search(".//text()").each do |node| + # Do not perform linking inside <code> blocks + next unless node.ancestors('code').empty? + # A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running # before this one, it will be converted into `[[<em>TOC</em>]]`, so it # needs special-case handling diff --git a/lib/banzai/filter/plantuml_filter.rb b/lib/banzai/filter/plantuml_filter.rb index 5325819d828..28933c78966 100644 --- a/lib/banzai/filter/plantuml_filter.rb +++ b/lib/banzai/filter/plantuml_filter.rb @@ -23,7 +23,7 @@ module Banzai private def settings - ApplicationSetting.current || ApplicationSetting.create_from_defaults + Gitlab::CurrentSettings.current_application_settings end def plantuml_setup diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index b9d5ecf70ec..2f023f4f242 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -73,7 +73,7 @@ module Banzai # # Note that while the key might exist, its value could be nil! def validate - needs :project + needs :project unless skip_project_check? end # Iterates over all <a> and text() nodes in a document. diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 8b2f05fffec..a1f24e8b093 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -42,9 +42,9 @@ module Banzai end def self.transform_context(context) - context.merge( - only_path: true, + context[:only_path] = true unless context.key?(:only_path) + context.merge( # EmojiFilter asset_host: Gitlab::Application.config.asset_host, asset_root: Gitlab.config.gitlab.base_url diff --git a/lib/feature.rb b/lib/feature.rb index 8e9ba5c530a..6474de6e56d 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -1,3 +1,6 @@ +require 'flipper/adapters/active_record' +require 'flipper/adapters/active_support_cache_store' + class Feature # Classes to override flipper table names class FlipperFeature < Flipper::Adapters::ActiveRecord::Feature @@ -60,7 +63,8 @@ class Feature end def flipper - @flipper ||= Flipper.instance + Thread.current[:flipper] ||= + Flipper.new(flipper_adapter).tap { |flip| flip.memoize = true } end # This method is called from config/initializers/flipper.rb and can be used @@ -68,5 +72,16 @@ class Feature # See https://docs.gitlab.com/ee/development/feature_flags.html#feature-groups def register_feature_groups end + + def flipper_adapter + active_record_adapter = Flipper::Adapters::ActiveRecord.new( + feature_class: FlipperFeature, + gate_class: FlipperGate) + + Flipper::Adapters::ActiveSupportCacheStore.new( + active_record_adapter, + Rails.cache, + expires_in: 1.hour) + end end end diff --git a/lib/gitlab.rb b/lib/gitlab.rb index c5498d0da1a..a129746e2c6 100644 --- a/lib/gitlab.rb +++ b/lib/gitlab.rb @@ -9,11 +9,30 @@ module Gitlab Settings end + def self.migrations_hash + @_migrations_hash ||= Digest::MD5.hexdigest(ActiveRecord::Migrator.get_all_versions.to_s) + end + + def self.revision + @_revision ||= begin + if File.exist?(root.join("REVISION")) + File.read(root.join("REVISION")).strip.freeze + else + result = Gitlab::Popen.popen_with_detail(%W[#{config.git.bin_path} log --pretty=format:%h -n 1]) + + if result.status.success? + result.stdout.chomp.freeze + else + "Unknown".freeze + end + end + end + end + COM_URL = 'https://gitlab.com'.freeze APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))} SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z} VERSION = File.read(root.join("VERSION")).strip.freeze - REVISION = Gitlab::Popen.popen(%W(#{config.git.bin_path} log --pretty=format:%h -n 1)).first.chomp.freeze def self.com? # Check `gl_subdomain?` as well to keep parity with gitlab.com diff --git a/lib/gitlab/auth/blocked_user_tracker.rb b/lib/gitlab/auth/blocked_user_tracker.rb index dae03a179e4..7609a7b04f6 100644 --- a/lib/gitlab/auth/blocked_user_tracker.rb +++ b/lib/gitlab/auth/blocked_user_tracker.rb @@ -17,7 +17,9 @@ module Gitlab # message passed along by Warden. return unless message == User::BLOCKED_MESSAGE - login = env.dig(ACTIVE_RECORD_REQUEST_PARAMS, 'user', 'login') + # Check for either LDAP or regular GitLab account logins + login = env.dig(ACTIVE_RECORD_REQUEST_PARAMS, 'username') || + env.dig(ACTIVE_RECORD_REQUEST_PARAMS, 'user', 'login') return unless login.present? diff --git a/lib/gitlab/auth/ldap/access.rb b/lib/gitlab/auth/ldap/access.rb index 34286900e72..865185eb5db 100644 --- a/lib/gitlab/auth/ldap/access.rb +++ b/lib/gitlab/auth/ldap/access.rb @@ -6,7 +6,7 @@ module Gitlab module Auth module LDAP class Access - attr_reader :provider, :user + attr_reader :provider, :user, :ldap_identity def self.open(user, &block) Gitlab::Auth::LDAP::Adapter.open(user.ldap_identity.provider) do |adapter| @@ -14,9 +14,12 @@ module Gitlab end end - def self.allowed?(user) + def self.allowed?(user, options = {}) self.open(user) do |access| + # Whether user is allowed, or not, we should update + # permissions to keep things clean if access.allowed? + access.update_user Users::UpdateService.new(user, user: user, last_credential_check_at: Time.now).execute true @@ -29,7 +32,8 @@ module Gitlab def initialize(user, adapter = nil) @adapter = adapter @user = user - @provider = user.ldap_identity.provider + @ldap_identity = user.ldap_identity + @provider = adapter&.provider || ldap_identity&.provider end def allowed? @@ -40,7 +44,7 @@ module Gitlab end # Block user in GitLab if he/she was blocked in AD - if Gitlab::Auth::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter) + if Gitlab::Auth::LDAP::Person.disabled_via_active_directory?(ldap_identity.extern_uid, adapter) block_user(user, 'is disabled in Active Directory') false else @@ -64,27 +68,44 @@ module Gitlab Gitlab::Auth::LDAP::Config.new(provider) end + def find_ldap_user + Gitlab::Auth::LDAP::Person.find_by_dn(ldap_identity.extern_uid, adapter) + end + def ldap_user - @ldap_user ||= Gitlab::Auth::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter) + return unless provider + + @ldap_user ||= find_ldap_user end def block_user(user, reason) user.ldap_block - Gitlab::AppLogger.info( - "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \ - "blocking Gitlab user \"#{user.name}\" (#{user.email})" - ) + if provider + Gitlab::AppLogger.info( + "LDAP account \"#{ldap_identity.extern_uid}\" #{reason}, " \ + "blocking Gitlab user \"#{user.name}\" (#{user.email})" + ) + else + Gitlab::AppLogger.info( + "Account is not provided by LDAP, " \ + "blocking Gitlab user \"#{user.name}\" (#{user.email})" + ) + end end def unblock_user(user, reason) user.activate Gitlab::AppLogger.info( - "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \ + "LDAP account \"#{ldap_identity.extern_uid}\" #{reason}, " \ "unblocking Gitlab user \"#{user.name}\" (#{user.email})" ) end + + def update_user + # no-op in CE + end end end end diff --git a/lib/gitlab/auth/ldap/config.rb b/lib/gitlab/auth/ldap/config.rb index 77185f52ced..d4415eaa6dc 100644 --- a/lib/gitlab/auth/ldap/config.rb +++ b/lib/gitlab/auth/ldap/config.rb @@ -11,6 +11,8 @@ module Gitlab attr_accessor :provider, :options + InvalidProvider = Class.new(StandardError) + def self.enabled? Gitlab.config.ldap.enabled end @@ -22,6 +24,10 @@ module Gitlab def self.available_servers return [] unless enabled? + _available_servers + end + + def self._available_servers Array.wrap(servers.first) end @@ -34,7 +40,7 @@ module Gitlab end def self.invalid_provider(provider) - raise "Unknown provider (#{provider}). Available providers: #{providers}" + raise InvalidProvider.new("Unknown provider (#{provider}). Available providers: #{providers}") end def initialize(provider) @@ -84,13 +90,17 @@ module Gitlab end def base - options['base'] + @base ||= Person.normalize_dn(options['base']) end def uid options['uid'] end + def label + options['label'] + end + def sync_ssh_keys? sync_ssh_keys.present? end @@ -132,6 +142,10 @@ module Gitlab options['timeout'].to_i end + def external_groups + options['external_groups'] || [] + end + def has_auth? options['password'] || options['bind_dn'] end diff --git a/lib/gitlab/auth/saml/config.rb b/lib/gitlab/auth/saml/config.rb index 2760b1a3247..5fa9581f837 100644 --- a/lib/gitlab/auth/saml/config.rb +++ b/lib/gitlab/auth/saml/config.rb @@ -14,6 +14,10 @@ module Gitlab def external_groups options[:external_groups] end + + def admin_groups + options[:admin_groups] + end end end end diff --git a/lib/gitlab/auth/saml/user.rb b/lib/gitlab/auth/saml/user.rb index cb01cd8004c..b8c84c37cd5 100644 --- a/lib/gitlab/auth/saml/user.rb +++ b/lib/gitlab/auth/saml/user.rb @@ -20,10 +20,8 @@ module Gitlab user ||= find_or_build_ldap_user if auto_link_ldap_user? user ||= build_new_user if signup_enabled? - if external_users_enabled? && user - # Check if there is overlap between the user's groups and the external groups - # setting then set user as external or internal. - user.external = !(auth_hash.groups & saml_config.external_groups).empty? + if user + user.external = !(auth_hash.groups & saml_config.external_groups).empty? if external_users_enabled? end user diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb index cf02030c577..4dc23f977da 100644 --- a/lib/gitlab/auth/user_auth_finders.rb +++ b/lib/gitlab/auth/user_auth_finders.rb @@ -1,9 +1,5 @@ module Gitlab module Auth - # - # Exceptions - # - AuthenticationError = Class.new(StandardError) MissingTokenError = Class.new(AuthenticationError) TokenNotFoundError = Class.new(AuthenticationError) @@ -61,6 +57,12 @@ module Gitlab private + def route_authentication_setting + return {} unless respond_to?(:route_setting) + + route_setting(:authentication) || {} + end + def access_token strong_memoize(:access_token) do find_oauth_access_token || find_personal_access_token diff --git a/lib/gitlab/ci/pipeline/expression.rb b/lib/gitlab/ci/pipeline/expression.rb new file mode 100644 index 00000000000..f57df7c5637 --- /dev/null +++ b/lib/gitlab/ci/pipeline/expression.rb @@ -0,0 +1,10 @@ +module Gitlab + module Ci + module Pipeline + module Expression + ExpressionError = Class.new(StandardError) + RuntimeError = Class.new(ExpressionError) + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb new file mode 100644 index 00000000000..10957598f76 --- /dev/null +++ b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb @@ -0,0 +1,29 @@ +module Gitlab + module Ci + module Pipeline + module Expression + module Lexeme + class Matches < Lexeme::Operator + PATTERN = /=~/.freeze + + def initialize(left, right) + @left = left + @right = right + end + + def evaluate(variables = {}) + text = @left.evaluate(variables) + regexp = @right.evaluate(variables) + + regexp.scan(text.to_s).any? + end + + def self.build(_value, behind, ahead) + new(behind, ahead) + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb new file mode 100644 index 00000000000..9b239c29ea4 --- /dev/null +++ b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb @@ -0,0 +1,33 @@ +module Gitlab + module Ci + module Pipeline + module Expression + module Lexeme + require_dependency 're2' + + class Pattern < Lexeme::Value + PATTERN = %r{^/.+/[ismU]*$}.freeze + + def initialize(regexp) + @value = regexp + + unless Gitlab::UntrustedRegexp.valid?(@value) + raise Lexer::SyntaxError, 'Invalid regular expression!' + end + end + + def evaluate(variables = {}) + Gitlab::UntrustedRegexp.fabricate(@value) + rescue RegexpError + raise Expression::RuntimeError, 'Invalid regular expression!' + end + + def self.build(string) + new(string) + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/expression/lexer.rb b/lib/gitlab/ci/pipeline/expression/lexer.rb index e1c68b7c3c2..4cacb1e62c9 100644 --- a/lib/gitlab/ci/pipeline/expression/lexer.rb +++ b/lib/gitlab/ci/pipeline/expression/lexer.rb @@ -5,15 +5,17 @@ module Gitlab class Lexer include ::Gitlab::Utils::StrongMemoize + SyntaxError = Class.new(Expression::ExpressionError) + LEXEMES = [ Expression::Lexeme::Variable, Expression::Lexeme::String, + Expression::Lexeme::Pattern, Expression::Lexeme::Null, - Expression::Lexeme::Equals + Expression::Lexeme::Equals, + Expression::Lexeme::Matches ].freeze - SyntaxError = Class.new(Statement::StatementError) - MAX_TOKENS = 100 def initialize(statement, max_tokens: MAX_TOKENS) diff --git a/lib/gitlab/ci/pipeline/expression/statement.rb b/lib/gitlab/ci/pipeline/expression/statement.rb index 09a7c98464b..b36f1e0f865 100644 --- a/lib/gitlab/ci/pipeline/expression/statement.rb +++ b/lib/gitlab/ci/pipeline/expression/statement.rb @@ -3,15 +3,16 @@ module Gitlab module Pipeline module Expression class Statement - StatementError = Class.new(StandardError) + StatementError = Class.new(Expression::ExpressionError) GRAMMAR = [ + %w[variable], %w[variable equals string], %w[variable equals variable], %w[variable equals null], %w[string equals variable], %w[null equals variable], - %w[variable] + %w[variable matches pattern] ].freeze def initialize(statement, variables = {}) @@ -35,11 +36,13 @@ module Gitlab def truthful? evaluate.present? + rescue Expression::ExpressionError + false end def valid? parse_tree.is_a?(Lexeme::Base) - rescue StatementError + rescue Expression::ExpressionError false end end diff --git a/lib/gitlab/ci/pipeline/preloader.rb b/lib/gitlab/ci/pipeline/preloader.rb new file mode 100644 index 00000000000..e7a2e5511cf --- /dev/null +++ b/lib/gitlab/ci/pipeline/preloader.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + # Class for preloading data associated with pipelines such as commit + # authors. + module Preloader + def self.preload(pipelines) + # This ensures that all the pipeline commits are eager loaded before we + # start using them. + pipelines.each(&:commit) + + pipelines.each do |pipeline| + # This preloads the author of every commit. We're using "lazy_author" + # here since "author" immediately loads the data on the first call. + pipeline.commit.try(:lazy_author) + + # This preloads the number of warnings for every pipeline, ensuring + # that Ci::Pipeline#has_warnings? doesn't execute any additional + # queries. + pipeline.number_of_warnings + end + end + end + end + end +end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index e392a015b91..6cf7aa1bf0d 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -9,8 +9,8 @@ module Gitlab end end - def fake_application_settings(defaults = ::ApplicationSetting.defaults) - Gitlab::FakeApplicationSettings.new(defaults) + def fake_application_settings(attributes = {}) + Gitlab::FakeApplicationSettings.new(::ApplicationSetting.defaults.merge(attributes || {})) end def method_missing(name, *args, &block) @@ -25,43 +25,35 @@ module Gitlab def ensure_application_settings! return in_memory_application_settings if ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true' - - cached_application_settings || uncached_application_settings - end - - def cached_application_settings - begin - ::ApplicationSetting.cached - rescue ::Redis::BaseError, ::Errno::ENOENT, ::Errno::EADDRNOTAVAIL - # In case Redis isn't running or the Redis UNIX socket file is not available - end - end - - def uncached_application_settings return fake_application_settings unless connect_to_db? - db_settings = ::ApplicationSetting.current - + current_settings = ::ApplicationSetting.current # If there are pending migrations, it's possible there are columns that # need to be added to the application settings. To prevent Rake tasks # and other callers from failing, use any loaded settings and return # defaults for missing columns. if ActiveRecord::Migrator.needs_migration? - defaults = ::ApplicationSetting.defaults - defaults.merge!(db_settings.attributes.symbolize_keys) if db_settings.present? - return fake_application_settings(defaults) + return fake_application_settings(current_settings&.attributes) end - return db_settings if db_settings.present? + return current_settings if current_settings.present? - ::ApplicationSetting.create_from_defaults || in_memory_application_settings + with_fallback_to_fake_application_settings do + ::ApplicationSetting.create_from_defaults || in_memory_application_settings + end end def in_memory_application_settings - @in_memory_application_settings ||= ::ApplicationSetting.new(::ApplicationSetting.defaults) # rubocop:disable Gitlab/ModuleWithInstanceVariables - rescue ActiveRecord::StatementInvalid, ActiveRecord::UnknownAttributeError - # In case migrations the application_settings table is not created yet, - # we fallback to a simple OpenStruct + with_fallback_to_fake_application_settings do + @in_memory_application_settings ||= ::ApplicationSetting.build_from_defaults # rubocop:disable Gitlab/ModuleWithInstanceVariables + end + end + + def with_fallback_to_fake_application_settings(&block) + yield + rescue + # In case the application_settings table is not created yet, or if a new + # ApplicationSetting column is not yet migrated we fallback to a simple OpenStruct fake_application_settings end diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index 76501dd50e8..d49d055c3f2 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -43,7 +43,7 @@ module Gitlab end def self.version - database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1] + @version ||= database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1] end def self.join_lateral_supported? diff --git a/lib/gitlab/database/count.rb b/lib/gitlab/database/count.rb new file mode 100644 index 00000000000..3374203960e --- /dev/null +++ b/lib/gitlab/database/count.rb @@ -0,0 +1,48 @@ +# For large tables, PostgreSQL can take a long time to count rows due to MVCC. +# We can optimize this by using the reltuples count as described in https://wiki.postgresql.org/wiki/Slow_Counting. +module Gitlab + module Database + module Count + CONNECTION_ERRORS = + if defined?(PG) + [ + ActionView::Template::Error, + ActiveRecord::StatementInvalid, + PG::Error + ].freeze + else + [ + ActionView::Template::Error, + ActiveRecord::StatementInvalid + ].freeze + end + + def self.approximate_count(model) + return model.count unless Gitlab::Database.postgresql? + + execute_estimate_if_updated_recently(model) || model.count + end + + def self.execute_estimate_if_updated_recently(model) + ActiveRecord::Base.connection.select_value(postgresql_estimate_query(model)).to_i if reltuples_updated_recently?(model) + rescue *CONNECTION_ERRORS + end + + def self.reltuples_updated_recently?(model) + time = "to_timestamp(#{1.hour.ago.to_i})" + query = <<~SQL + SELECT 1 FROM pg_stat_user_tables WHERE relname = '#{model.table_name}' AND + (last_vacuum > #{time} OR last_autovacuum > #{time} OR last_analyze > #{time} OR last_autoanalyze > #{time}) + SQL + + ActiveRecord::Base.connection.select_all(query).count > 0 + rescue *CONNECTION_ERRORS + false + end + + def self.postgresql_estimate_query(model) + "SELECT reltuples::bigint AS estimate FROM pg_class where relname = '#{model.table_name}'" + end + end + end +end diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb index 05a60deb7d3..764f93f6d3d 100644 --- a/lib/gitlab/email/handler/create_issue_handler.rb +++ b/lib/gitlab/email/handler/create_issue_handler.rb @@ -47,7 +47,7 @@ module Gitlab project, author, title: mail.subject, - description: message + description: message_including_reply ).execute end end diff --git a/lib/gitlab/email/handler/reply_processing.rb b/lib/gitlab/email/handler/reply_processing.rb index da5ff350549..38b1425364f 100644 --- a/lib/gitlab/email/handler/reply_processing.rb +++ b/lib/gitlab/email/handler/reply_processing.rb @@ -16,8 +16,12 @@ module Gitlab @message ||= process_message end - def process_message - message = ReplyParser.new(mail).execute.strip + def message_including_reply + @message_with_reply ||= process_message(trim_reply: false) + end + + def process_message(**kwargs) + message = ReplyParser.new(mail, **kwargs).execute.strip add_attachments(message) end diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb index 01c28d051ee..ae6b84607d6 100644 --- a/lib/gitlab/email/reply_parser.rb +++ b/lib/gitlab/email/reply_parser.rb @@ -4,8 +4,9 @@ module Gitlab class ReplyParser attr_accessor :message - def initialize(message) + def initialize(message, trim_reply: true) @message = message + @trim_reply = trim_reply end def execute @@ -13,7 +14,9 @@ module Gitlab encoding = body.encoding - body = EmailReplyTrimmer.trim(body) + if @trim_reply + body = EmailReplyTrimmer.trim(body) + end return '' unless body diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb index 77af7a868d0..49bc9c0b671 100644 --- a/lib/gitlab/file_detector.rb +++ b/lib/gitlab/file_detector.rb @@ -14,7 +14,7 @@ module Gitlab avatar: /\Alogo\.(png|jpg|gif)\z/, issue_template: %r{\A\.gitlab/issue_templates/[^/]+\.md\z}, merge_request_template: %r{\A\.gitlab/merge_request_templates/[^/]+\.md\z}, - xcode_config: %r{\A[^/]*\.(xcodeproj|xcworkspace)\z}, + xcode_config: %r{\A[^/]*\.(xcodeproj|xcworkspace)(/.+)?\z}, # Configuration files gitignore: '.gitignore', diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index d78d29b7ac6..156d077a69c 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -104,25 +104,22 @@ module Gitlab # file.rb # oid: 4a # # - # Blob.find_entry_by_path(repo, '1a', 'app/file.rb') # => '4a' + # Blob.find_entry_by_path(repo, '1a', 'blog', 'app', 'file.rb') # => '4a' # - def find_entry_by_path(repository, root_id, path) + def find_entry_by_path(repository, root_id, *path_parts) root_tree = repository.lookup(root_id) - # Strip leading slashes - path[%r{^/*}] = '' - path_arr = path.split('/') entry = root_tree.find do |entry| - entry[:name] == path_arr[0] + entry[:name] == path_parts[0] end return nil unless entry - if path_arr.size > 1 + if path_parts.size > 1 return nil unless entry[:type] == :tree - path_arr.shift - find_entry_by_path(repository, entry[:oid], path_arr.join('/')) + path_parts.shift + find_entry_by_path(repository, entry[:oid], *path_parts) else [:blob, :commit].include?(entry[:type]) ? entry : nil end @@ -185,10 +182,13 @@ module Gitlab def find_by_rugged(repository, sha, path, limit:) return unless path + # Strip any leading / characters from the path + path = path.sub(%r{\A/*}, '') + rugged_commit = repository.lookup(sha) root_tree = rugged_commit.tree - blob_entry = find_entry_by_path(repository, root_tree.oid, path) + blob_entry = find_entry_by_path(repository, root_tree.oid, *path.split('/')) return nil unless blob_entry diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index fabcd46c8e9..89f761dd515 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -6,6 +6,7 @@ module Gitlab attr_accessor :raw_commit, :head + MAX_COMMIT_MESSAGE_DISPLAY_SIZE = 10.megabytes MIN_SHA_LENGTH = 7 SERIALIZE_KEYS = [ :id, :message, :parent_ids, @@ -63,9 +64,7 @@ module Gitlab if is_enabled repo.gitaly_commit_client.find_commit(commit_id) else - obj = repo.rev_parse_target(commit_id) - - obj.is_a?(Rugged::Commit) ? obj : nil + rugged_find(repo, commit_id) end end @@ -76,6 +75,12 @@ module Gitlab nil end + def rugged_find(repo, commit_id) + obj = repo.rev_parse_target(commit_id) + + obj.is_a?(Rugged::Commit) ? obj : nil + end + # Get last commit for HEAD # # Ex. @@ -297,11 +302,40 @@ module Gitlab nil end end + + def get_message(repository, commit_id) + BatchLoader.for({ repository: repository, commit_id: commit_id }).batch do |items, loader| + items_by_repo = items.group_by { |i| i[:repository] } + + items_by_repo.each do |repo, items| + commit_ids = items.map { |i| i[:commit_id] } + + messages = get_messages(repository, commit_ids) + + messages.each do |commit_sha, message| + loader.call({ repository: repository, commit_id: commit_sha }, message) + end + end + end + end + + def get_messages(repository, commit_ids) + repository.gitaly_migrate(:commit_messages) do |is_enabled| + if is_enabled + repository.gitaly_commit_client.get_commit_messages(commit_ids) + else + commit_ids.map { |id| [id, rugged_find(repository, id).message] }.to_h + end + end + end end def initialize(repository, raw_commit, head = nil) raise "Nil as raw commit passed" unless raw_commit + @repository = repository + @head = head + case raw_commit when Hash init_from_hash(raw_commit) @@ -312,9 +346,6 @@ module Gitlab else raise "Invalid raw commit type: #{raw_commit.class}" end - - @repository = repository - @head = head end def sha @@ -342,21 +373,6 @@ module Gitlab parent_ids.first end - # Shows the diff between the commit's parent and the commit. - # - # Cuts out the header and stats from #to_patch and returns only the diff. - # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/324 - def to_diff - Gitlab::GitalyClient.migrate(:commit_patch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| - if is_enabled - @repository.gitaly_commit_client.patch(id) - else - rugged_diff_from_parent.patch - end - end - end - # Returns a diff object for the changes from this commit's first parent. # If there is no parent, then the diff is between this commit and an # empty repo. See Repository#diff for keys allowed in the +options+ @@ -432,16 +448,6 @@ module Gitlab Gitlab::Git::CommitStats.new(@repository, self) end - def to_patch(options = {}) - begin - rugged_commit.to_mbox(options) - rescue Rugged::InvalidError => ex - if ex.message =~ /commit \w+ is a merge commit/i - 'Patch format is not currently supported for merge commits.' - end - end - end - # Get ref names collection # # Ex. @@ -543,7 +549,7 @@ module Gitlab # TODO: Once gitaly "takes over" Rugged consider separating the # subject from the message to make it clearer when there's one # available but not the other. - @message = (commit.body.presence || commit.subject).dup + @message = message_from_gitaly_body @authored_date = Time.at(commit.author.date.seconds).utc @author_name = commit.author.name.dup @author_email = commit.author.email.dup @@ -595,6 +601,25 @@ module Gitlab def refs(repo) repo.refs_hash[id] end + + def message_from_gitaly_body + return @raw_commit.subject.dup if @raw_commit.body_size.zero? + return @raw_commit.body.dup if full_body_fetched_from_gitaly? + + if @raw_commit.body_size > MAX_COMMIT_MESSAGE_DISPLAY_SIZE + "#{@raw_commit.subject}\n\n--commit message is too big".strip + else + fetch_body_from_gitaly + end + end + + def full_body_fetched_from_gitaly? + @raw_commit.body.bytesize == @raw_commit.body_size + end + + def fetch_body_from_gitaly + self.class.get_message(@repository, id) + end end end end diff --git a/lib/gitlab/git/gitlab_projects.rb b/lib/gitlab/git/gitlab_projects.rb index 68373460d23..00c943fdb25 100644 --- a/lib/gitlab/git/gitlab_projects.rb +++ b/lib/gitlab/git/gitlab_projects.rb @@ -53,7 +53,7 @@ module Gitlab # Import project via git clone --bare # URL must be publicly cloneable def import_project(source, timeout) - Gitlab::GitalyClient.migrate(:import_repository) do |is_enabled| + Gitlab::GitalyClient.migrate(:import_repository, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| if is_enabled gitaly_import_repository(source) else diff --git a/lib/gitlab/git/path_helper.rb b/lib/gitlab/git/path_helper.rb index 155cf52f050..57b82a37d6c 100644 --- a/lib/gitlab/git/path_helper.rb +++ b/lib/gitlab/git/path_helper.rb @@ -6,7 +6,7 @@ module Gitlab class << self def normalize_path(filename) # Strip all leading slashes so that //foo -> foo - filename[%r{^/*}] = '' + filename = filename.sub(%r{\A/*}, '') # Expand relative paths (e.g. foo/../bar) filename = Pathname.new(filename) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 5d47f8b2075..1a21625a322 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -403,10 +403,10 @@ module Gitlab prefix = archive_prefix(ref, commit.id, append_sha: append_sha) { - 'RepoPath' => path, 'ArchivePrefix' => prefix, 'ArchivePath' => archive_file_path(storage_path, commit.id, prefix, format), - 'CommitId' => commit.id + 'CommitId' => commit.id, + 'GitalyRepository' => gitaly_repository.to_h } end @@ -579,11 +579,6 @@ module Gitlab count_commits(from: from, to: to, **options) end - # Counts the amount of commits between `from` and `to`. - def count_commits_between(from, to, options = {}) - count_commits(from: from, to: to, **options) - end - # old_rev and new_rev are commit ID's # the result of this method is an array of Gitlab::Git::RawDiffChange def raw_changes_between(old_rev, new_rev) @@ -781,13 +776,9 @@ module Gitlab end def add_branch(branch_name, user:, target:) - gitaly_migrate(:operation_user_create_branch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| - if is_enabled - gitaly_add_branch(branch_name, user, target) - else - rugged_add_branch(branch_name, user, target) - end - end + gitaly_operation_client.user_create_branch(branch_name, user, target) + rescue GRPC::FailedPrecondition => ex + raise InvalidRef, ex end def add_tag(tag_name, user:, target:, message: nil) @@ -1057,7 +1048,7 @@ module Gitlab return @info_attributes if @info_attributes content = - gitaly_migrate(:get_info_attributes) do |is_enabled| + gitaly_migrate(:get_info_attributes, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| if is_enabled gitaly_repository_client.info_attributes else @@ -1343,7 +1334,7 @@ module Gitlab end def squash_in_progress?(squash_id) - gitaly_migrate(:squash_in_progress) do |is_enabled| + gitaly_migrate(:squash_in_progress, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| if is_enabled gitaly_repository_client.squash_in_progress?(squash_id) else @@ -1472,34 +1463,29 @@ module Gitlab end def branch_names_contains_sha(sha) - gitaly_migrate(:branch_names_contains_sha, - status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| - if is_enabled - gitaly_ref_client.branch_names_contains_sha(sha) - else - refs_contains_sha('refs/heads/', sha) - end - end + gitaly_ref_client.branch_names_contains_sha(sha) end def tag_names_contains_sha(sha) - gitaly_migrate(:tag_names_contains_sha, - status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| - if is_enabled - gitaly_ref_client.tag_names_contains_sha(sha) - else - refs_contains_sha('refs/tags/', sha) - end - end + gitaly_ref_client.tag_names_contains_sha(sha) end def search_files_by_content(query, ref) return [] if empty? || query.blank? - offset = 2 - args = %W(grep -i -I -n -z --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref}) + safe_query = Regexp.escape(query) + ref ||= root_ref - run_git(args).first.scrub.split(/^--\n/) + gitaly_migrate(:search_files_by_content) do |is_enabled| + if is_enabled + gitaly_repository_client.search_files_by_content(ref, safe_query) + else + offset = 2 + args = %W(grep -i -I -n -z --before-context #{offset} --after-context #{offset} -E -e #{safe_query} #{ref}) + + run_git(args).first.scrub.split(/^--\n/) + end + end end def can_be_merged?(source_sha, target_branch) @@ -1514,12 +1500,19 @@ module Gitlab def search_files_by_name(query, ref) safe_query = Regexp.escape(query.sub(%r{^/*}, "")) + ref ||= root_ref return [] if empty? || safe_query.blank? - args = %W(ls-tree -r --name-status --full-tree #{ref || root_ref} -- #{safe_query}) + gitaly_migrate(:search_files_by_name) do |is_enabled| + if is_enabled + gitaly_repository_client.search_files_by_name(ref, safe_query) + else + args = %W(ls-tree -r --name-status --full-tree #{ref} -- #{safe_query}) - run_git(args).first.lines.map(&:strip) + run_git(args).first.lines.map(&:strip) + end + end end def find_commits_by_message(query, ref, path, limit, offset) @@ -1595,14 +1588,12 @@ module Gitlab end def checksum - gitaly_migrate(:calculate_checksum, - status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| - if is_enabled - gitaly_repository_client.calculate_checksum - else - calculate_checksum_by_shelling_out - end - end + # The exists? RPC is much cheaper, so we perform this request first + raise NoRepository, "Repository does not exists" unless exists? + + gitaly_repository_client.calculate_checksum + rescue GRPC::NotFound + raise NoRepository # Guard against data races. end private @@ -1625,27 +1616,6 @@ module Gitlab end end - def refs_contains_sha(refs_prefix, sha) - refs_prefix << "/" unless refs_prefix.ends_with?('/') - - # By forcing the output to %(refname) each line wiht a ref will start with - # the ref prefix. All other lines can be discarded. - args = %W(for-each-ref --contains=#{sha} --format=%(refname) #{refs_prefix}) - names, code = run_git(args) - - return [] unless code.zero? - - refs = [] - left_slice_count = refs_prefix.length - names.lines.each do |line| - next unless line.start_with?(refs_prefix) - - refs << encode_utf8(line.rstrip[left_slice_count..-1]) - end - - refs - end - def rugged_write_config(full_path:) rugged.config['gitlab.fullpath'] = full_path end @@ -1992,7 +1962,12 @@ module Gitlab end target_commit = Gitlab::Git::Commit.find(self, ref.target) - Gitlab::Git::Tag.new(self, ref.name, ref.target, target_commit, message) + Gitlab::Git::Tag.new(self, { + name: ref.name, + target: ref.target, + target_commit: target_commit, + message: message + }) end.sort_by(&:name) end @@ -2237,22 +2212,6 @@ module Gitlab end end - def gitaly_add_branch(branch_name, user, target) - gitaly_operation_client.user_create_branch(branch_name, user, target) - rescue GRPC::FailedPrecondition => ex - raise InvalidRef, ex - end - - def rugged_add_branch(branch_name, user, target) - target_object = Ref.dereference_object(lookup(target)) - raise InvalidRef.new("target not found: #{target}") unless target_object - - OperationService.new(user, self).add_branch(branch_name, target_object.oid) - find_branch(branch_name) - rescue Rugged::ReferenceError => ex - raise InvalidRef, ex - end - def rugged_cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:) OperationService.new(user, self).with_branch( branch_name, @@ -2408,7 +2367,7 @@ module Gitlab end def gitaly_delete_refs(*ref_names) - gitaly_ref_client.delete_refs(refs: ref_names) + gitaly_ref_client.delete_refs(refs: ref_names) if ref_names.any? end def rugged_remove_remote(remote_name) @@ -2538,36 +2497,6 @@ module Gitlab rev_parse_target(ref).oid end - def calculate_checksum_by_shelling_out - raise NoRepository unless exists? - - args = %W(--git-dir=#{path} show-ref --heads --tags) - output, status = run_git(args) - - if status.nil? || !status.zero? - # Non-valid git repositories return 128 as the status code and an error output - raise InvalidRepository if status == 128 && output.to_s.downcase =~ /not a git repository/ - # Empty repositories returns with a non-zero status and an empty output. - raise ChecksumError, output unless output.blank? - - return EMPTY_REPOSITORY_CHECKSUM - end - - refs = output.split("\n") - - result = refs.inject(nil) do |checksum, ref| - value = Digest::SHA1.hexdigest(ref).hex - - if checksum.nil? - value - else - checksum ^ value - end - end - - result.to_s(16) - end - def build_git_cmd(*args) object_directories = alternate_object_directories.join(File::PATH_SEPARATOR) diff --git a/lib/gitlab/git/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb index 8a01f92e2af..e35ea5762eb 100644 --- a/lib/gitlab/git/repository_mirroring.rb +++ b/lib/gitlab/git/repository_mirroring.rb @@ -35,7 +35,11 @@ module Gitlab next if name =~ /\^\{\}\Z/ target_commit = Gitlab::Git::Commit.find(self, target) - Gitlab::Git::Tag.new(self, name, target, target_commit) + Gitlab::Git::Tag.new(self, { + name: name, + target: target, + target_commit: target_commit + }) end.compact end diff --git a/lib/gitlab/git/tag.rb b/lib/gitlab/git/tag.rb index 8a8f7b051ed..e44284572fd 100644 --- a/lib/gitlab/git/tag.rb +++ b/lib/gitlab/git/tag.rb @@ -1,17 +1,99 @@ module Gitlab module Git class Tag < Ref - attr_reader :object_sha + extend Gitlab::EncodingHelper + + attr_reader :object_sha, :repository + + MAX_TAG_MESSAGE_DISPLAY_SIZE = 10.megabytes + SERIALIZE_KEYS = %i[name target target_commit message].freeze + + attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator + + class << self + def get_message(repository, tag_id) + BatchLoader.for({ repository: repository, tag_id: tag_id }).batch do |items, loader| + items_by_repo = items.group_by { |i| i[:repository] } + + items_by_repo.each do |repo, items| + tag_ids = items.map { |i| i[:tag_id] } + + messages = get_messages(repository, tag_ids) + + messages.each do |id, message| + loader.call({ repository: repository, tag_id: id }, message) + end + end + end + end + + def get_messages(repository, tag_ids) + repository.gitaly_migrate(:tag_messages) do |is_enabled| + if is_enabled + repository.gitaly_ref_client.get_tag_messages(tag_ids) + else + tag_ids.map do |id| + tag = repository.rugged.lookup(id) + message = tag.is_a?(Rugged::Commit) ? "" : tag.message + + [id, message] + end.to_h + end + end + end + end + + def initialize(repository, raw_tag) + @repository = repository + @raw_tag = raw_tag + + case raw_tag + when Hash + init_from_hash + when Gitaly::Tag + init_from_gitaly + end - def initialize(repository, name, target, target_commit, message = nil) super(repository, name, target, target_commit) + end + + def init_from_hash + raw_tag = @raw_tag.symbolize_keys + + SERIALIZE_KEYS.each do |key| + send("#{key}=", raw_tag[key]) # rubocop:disable GitlabSecurity/PublicSend + end + end + + def init_from_gitaly + @name = encode!(@raw_tag.name.dup) + @target = @raw_tag.id + @message = message_from_gitaly_tag - @message = message + if @raw_tag.target_commit.present? + @target_commit = Gitlab::Git::Commit.decorate(repository, @raw_tag.target_commit) + end end def message encode! @message end + + private + + def message_from_gitaly_tag + return @raw_tag.message.dup if full_message_fetched_from_gitaly? + + if @raw_tag.message_size > MAX_TAG_MESSAGE_DISPLAY_SIZE + '--tag message is too big' + else + self.class.get_message(@repository, target) + end + end + + def full_message_fetched_from_gitaly? + @raw_tag.message.bytesize == @raw_tag.message_size + end end end end diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb index d75a5f15c29..1ab8c4e0229 100644 --- a/lib/gitlab/git/wiki.rb +++ b/lib/gitlab/git/wiki.rb @@ -131,7 +131,7 @@ module Gitlab def page_formatted_data(title:, dir: nil, version: nil) version = version&.id - @repository.gitaly_migrate(:wiki_page_formatted_data) do |is_enabled| + @repository.gitaly_migrate(:wiki_page_formatted_data, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| if is_enabled gitaly_wiki_client.get_formatted_data(title: title, dir: dir, version: version) else diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index a36e6c822f9..1f5f88bf792 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -334,6 +334,22 @@ module Gitlab signatures end + def get_commit_messages(commit_ids) + request = Gitaly::GetCommitMessagesRequest.new(repository: @gitaly_repo, commit_ids: commit_ids) + response = GitalyClient.call(@repository.storage, :commit_service, :get_commit_messages, request) + + messages = Hash.new { |h, k| h[k] = ''.b } + current_commit_id = nil + + response.each do |rpc_message| + current_commit_id = rpc_message.commit_id if rpc_message.commit_id.present? + + messages[current_commit_id] << rpc_message.message + end + + messages + end + private def call_commit_diff(request_params, options = {}) diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index 831cfd1e014..44b0e517bf0 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -40,7 +40,7 @@ module Gitlab raise Gitlab::Git::Repository::TagExistsError end - Util.gitlab_tag_from_gitaly_tag(@repository, response.tag) + Gitlab::Git::Tag.new(@repository, response.tag) rescue GRPC::FailedPrecondition => e raise Gitlab::Git::Repository::InvalidRef, e end diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb index ba6b577fd17..3ac46be6208 100644 --- a/lib/gitlab/gitaly_client/ref_service.rb +++ b/lib/gitlab/gitaly_client/ref_service.rb @@ -171,6 +171,22 @@ module Gitlab consume_ref_contains_sha_response(stream, :branch_names) end + def get_tag_messages(tag_ids) + request = Gitaly::GetTagMessagesRequest.new(repository: @gitaly_repo, tag_ids: tag_ids) + response = GitalyClient.call(@repository.storage, :ref_service, :get_tag_messages, request) + + messages = Hash.new { |h, k| h[k] = ''.b } + current_tag_id = nil + + response.each do |rpc_message| + current_tag_id = rpc_message.tag_id if rpc_message.tag_id.present? + + messages[current_tag_id] << rpc_message.message + end + + messages + end + private def consume_refs_response(response) @@ -210,7 +226,7 @@ module Gitlab def consume_tags_response(response) response.flat_map do |message| - message.tags.map { |gitaly_tag| Util.gitlab_tag_from_gitaly_tag(@repository, gitaly_tag) } + message.tags.map { |gitaly_tag| Gitlab::Git::Tag.new(@repository, gitaly_tag) } end end diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index 132a5947f17..ee01f5a5bd9 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -301,6 +301,16 @@ module Gitlab GitalyClient.call(@storage, :repository_service, :get_raw_changes, request) end + + def search_files_by_name(ref, query) + request = Gitaly::SearchFilesByNameRequest.new(repository: @gitaly_repo, ref: ref, query: query) + GitalyClient.call(@storage, :repository_service, :search_files_by_name, request).flat_map(&:files) + end + + def search_files_by_content(ref, query) + request = Gitaly::SearchFilesByContentRequest.new(repository: @gitaly_repo, ref: ref, query: query) + GitalyClient.call(@storage, :repository_service, :search_files_by_content, request).flat_map(&:matches) + end end end end diff --git a/lib/gitlab/gitaly_client/util.rb b/lib/gitlab/gitaly_client/util.rb index 405567db94a..9c19c51d412 100644 --- a/lib/gitlab/gitaly_client/util.rb +++ b/lib/gitlab/gitaly_client/util.rb @@ -21,20 +21,6 @@ module Gitlab gitaly_repository.relative_path, gitaly_repository.gl_repository) end - - def gitlab_tag_from_gitaly_tag(repository, gitaly_tag) - if gitaly_tag.target_commit.present? - commit = Gitlab::Git::Commit.decorate(repository, gitaly_tag.target_commit) - end - - Gitlab::Git::Tag.new( - repository, - Gitlab::EncodingHelper.encode!(gitaly_tag.name.dup), - gitaly_tag.id, - commit, - Gitlab::EncodingHelper.encode!(gitaly_tag.message.chomp) - ) - end end end end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index c741dabe168..0d31934347f 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -15,7 +15,7 @@ module Gitlab gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn if Gitlab::CurrentSettings.clientside_sentry_enabled gon.gitlab_url = Gitlab.config.gitlab.url - gon.revision = Gitlab::REVISION + gon.revision = Gitlab.revision gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png') gon.sprite_icons = IconsHelper.sprite_icon_path gon.sprite_file_icons = IconsHelper.sprite_file_icons_path diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index e3e9f156fb4..4a41a69840b 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -28,7 +28,7 @@ module Gitlab IMPORTED_OBJECT_MAX_RETRIES = 5.freeze - EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels].freeze + EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels project_feature].freeze TOKEN_RESET_MODELS = %w[Ci::Trigger Ci::Build ProjectHook].freeze diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb index c9122a23568..d323cb9dadf 100644 --- a/lib/gitlab/incoming_email.rb +++ b/lib/gitlab/incoming_email.rb @@ -57,7 +57,7 @@ module Gitlab regex = Regexp.escape(wildcard_address) regex = regex.sub(Regexp.escape(WILDCARD_PLACEHOLDER), '(.+)') - Regexp.new(regex).freeze + Regexp.new(/\A#{regex}\z/).freeze end end end diff --git a/lib/gitlab/metrics/web_transaction.rb b/lib/gitlab/metrics/web_transaction.rb index 89ff02a96d6..3799aaebf1c 100644 --- a/lib/gitlab/metrics/web_transaction.rb +++ b/lib/gitlab/metrics/web_transaction.rb @@ -4,18 +4,6 @@ module Gitlab CONTROLLER_KEY = 'action_controller.instance'.freeze ENDPOINT_KEY = 'api.endpoint'.freeze - CONTENT_TYPES = { - 'text/html' => :html, - 'text/plain' => :txt, - 'application/json' => :json, - 'text/js' => :js, - 'application/atom+xml' => :atom, - 'image/png' => :png, - 'image/jpeg' => :jpeg, - 'image/gif' => :gif, - 'image/svg+xml' => :svg - }.freeze - def initialize(env) super() @env = env @@ -40,7 +28,11 @@ module Gitlab controller = @env[CONTROLLER_KEY] action = "#{controller.action_name}" - suffix = CONTENT_TYPES[controller.content_type] + + # Devise exposes a method called "request_format" that does the below. + # However, this method is not available to all controllers (e.g. certain + # Doorkeeper controllers). As such we use the underlying code directly. + suffix = controller.request.format.try(:ref) if suffix && suffix != :html action += ".#{suffix}" diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 390efda326a..2e9b6e302f5 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -59,7 +59,7 @@ module Gitlab startline = 0 result.each_line.each_with_index do |line, index| - prefix ||= line.match(/^(?<ref>[^:]*):(?<filename>.*)\x00(?<startline>\d+)\x00/)&.tap do |matches| + prefix ||= line.match(/^(?<ref>[^:]*):(?<filename>[^\x00]*)\x00(?<startline>\d+)\x00/)&.tap do |matches| ref = matches[:ref] filename = matches[:filename] startline = matches[:startline] diff --git a/lib/gitlab/serializer/pagination.rb b/lib/gitlab/serializer/pagination.rb index 9c92b83dddc..6bb00d8ae21 100644 --- a/lib/gitlab/serializer/pagination.rb +++ b/lib/gitlab/serializer/pagination.rb @@ -17,8 +17,6 @@ module Gitlab end end - private - # Methods needed by `API::Helpers::Pagination` # diff --git a/lib/gitlab/untrusted_regexp.rb b/lib/gitlab/untrusted_regexp.rb index 7ce2e9d636e..dc2d91dfa23 100644 --- a/lib/gitlab/untrusted_regexp.rb +++ b/lib/gitlab/untrusted_regexp.rb @@ -9,9 +9,15 @@ module Gitlab # there is a strict limit on total execution time. See the RE2 documentation # at https://github.com/google/re2/wiki/Syntax for more details. class UntrustedRegexp - delegate :===, to: :regexp + require_dependency 're2' + + delegate :===, :source, to: :regexp + + def initialize(pattern, multiline: false) + if multiline + pattern = "(?m)#{pattern}" + end - def initialize(pattern) @regexp = RE2::Regexp.new(pattern, log_errors: false) raise RegexpError.new(regexp.error) unless regexp.ok? @@ -31,6 +37,41 @@ module Gitlab RE2.Replace(text, regexp, rewrite) end + def ==(other) + self.source == other.source + end + + # Handles regular expressions with the preferred RE2 library where possible + # via UntustedRegex. Falls back to Ruby's built-in regular expression library + # when the syntax would be invalid in RE2. + # + # One difference between these is `(?m)` multi-line mode. Ruby regex enables + # this by default, but also handles `^` and `$` differently. + # See: https://www.regular-expressions.info/modifiers.html + def self.with_fallback(pattern, multiline: false) + UntrustedRegexp.new(pattern, multiline: multiline) + rescue RegexpError + Regexp.new(pattern) + end + + def self.valid?(pattern) + !!self.fabricate(pattern) + rescue RegexpError + false + end + + def self.fabricate(pattern) + matches = pattern.match(%r{^/(?<regexp>.+)/(?<flags>[ismU]*)$}) + + raise RegexpError, 'Invalid regular expression!' if matches.nil? + + expression = matches[:regexp] + flags = matches[:flags] + expression.prepend("(?#{flags})") if flags.present? + + self.new(expression, multiline: false) + end + private attr_reader :regexp diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 1f060de657d..e893e46ee86 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -65,12 +65,7 @@ module Gitlab params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format, append_sha: append_sha) raise "Repository or ref not found" if params.empty? - if Gitlab::GitalyClient.feature_enabled?(:workhorse_archive, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) - params.merge!( - 'GitalyServer' => gitaly_server_hash(repository), - 'GitalyRepository' => repository.gitaly_repository.to_h - ) - end + params['GitalyServer'] = gitaly_server_hash(repository) # If present DisableCache must be a Boolean. Otherwise workhorse ignores it. params['DisableCache'] = true if git_archive_cache_disabled? diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 24e37f6c6cc..e96fbb64372 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -6,7 +6,6 @@ namespace :gitlab do desc "GitLab | Create a backup of the GitLab system" task create: :gitlab_environment do warn_user_is_not_gitlab - configure_cron_mode Rake::Task["gitlab:backup:db:create"].invoke Rake::Task["gitlab:backup:repo:create"].invoke @@ -17,7 +16,7 @@ namespace :gitlab do Rake::Task["gitlab:backup:lfs:create"].invoke Rake::Task["gitlab:backup:registry:create"].invoke - backup = Backup::Manager.new + backup = Backup::Manager.new(progress) backup.pack backup.cleanup backup.remove_old @@ -27,9 +26,8 @@ namespace :gitlab do desc 'GitLab | Restore a previously created backup' task restore: :gitlab_environment do warn_user_is_not_gitlab - configure_cron_mode - backup = Backup::Manager.new + backup = Backup::Manager.new(progress) backup.unpack unless backup.skipped?('db') @@ -49,9 +47,9 @@ namespace :gitlab do # Drop all tables Load the schema to ensure we don't have any newer tables # hanging out from a failed upgrade - $progress.puts 'Cleaning the database ... '.color(:blue) + progress.puts 'Cleaning the database ... '.color(:blue) Rake::Task['gitlab:db:drop_tables'].invoke - $progress.puts 'done'.color(:green) + progress.puts 'done'.color(:green) Rake::Task['gitlab:backup:db:restore'].invoke rescue Gitlab::TaskAbortedByUserError puts "Quitting...".color(:red) @@ -74,173 +72,173 @@ namespace :gitlab do namespace :repo do task create: :gitlab_environment do - $progress.puts "Dumping repositories ...".color(:blue) + progress.puts "Dumping repositories ...".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("repositories") - $progress.puts "[SKIPPED]".color(:cyan) + progress.puts "[SKIPPED]".color(:cyan) else - Backup::Repository.new.dump - $progress.puts "done".color(:green) + Backup::Repository.new(progress).dump + progress.puts "done".color(:green) end end task restore: :gitlab_environment do - $progress.puts "Restoring repositories ...".color(:blue) - Backup::Repository.new.restore - $progress.puts "done".color(:green) + progress.puts "Restoring repositories ...".color(:blue) + Backup::Repository.new(progress).restore + progress.puts "done".color(:green) end end namespace :db do task create: :gitlab_environment do - $progress.puts "Dumping database ... ".color(:blue) + progress.puts "Dumping database ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("db") - $progress.puts "[SKIPPED]".color(:cyan) + progress.puts "[SKIPPED]".color(:cyan) else - Backup::Database.new.dump - $progress.puts "done".color(:green) + Backup::Database.new(progress).dump + progress.puts "done".color(:green) end end task restore: :gitlab_environment do - $progress.puts "Restoring database ... ".color(:blue) - Backup::Database.new.restore - $progress.puts "done".color(:green) + progress.puts "Restoring database ... ".color(:blue) + Backup::Database.new(progress).restore + progress.puts "done".color(:green) end end namespace :builds do task create: :gitlab_environment do - $progress.puts "Dumping builds ... ".color(:blue) + progress.puts "Dumping builds ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("builds") - $progress.puts "[SKIPPED]".color(:cyan) + progress.puts "[SKIPPED]".color(:cyan) else - Backup::Builds.new.dump - $progress.puts "done".color(:green) + Backup::Builds.new(progress).dump + progress.puts "done".color(:green) end end task restore: :gitlab_environment do - $progress.puts "Restoring builds ... ".color(:blue) - Backup::Builds.new.restore - $progress.puts "done".color(:green) + progress.puts "Restoring builds ... ".color(:blue) + Backup::Builds.new(progress).restore + progress.puts "done".color(:green) end end namespace :uploads do task create: :gitlab_environment do - $progress.puts "Dumping uploads ... ".color(:blue) + progress.puts "Dumping uploads ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("uploads") - $progress.puts "[SKIPPED]".color(:cyan) + progress.puts "[SKIPPED]".color(:cyan) else - Backup::Uploads.new.dump - $progress.puts "done".color(:green) + Backup::Uploads.new(progress).dump + progress.puts "done".color(:green) end end task restore: :gitlab_environment do - $progress.puts "Restoring uploads ... ".color(:blue) - Backup::Uploads.new.restore - $progress.puts "done".color(:green) + progress.puts "Restoring uploads ... ".color(:blue) + Backup::Uploads.new(progress).restore + progress.puts "done".color(:green) end end namespace :artifacts do task create: :gitlab_environment do - $progress.puts "Dumping artifacts ... ".color(:blue) + progress.puts "Dumping artifacts ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("artifacts") - $progress.puts "[SKIPPED]".color(:cyan) + progress.puts "[SKIPPED]".color(:cyan) else - Backup::Artifacts.new.dump - $progress.puts "done".color(:green) + Backup::Artifacts.new(progress).dump + progress.puts "done".color(:green) end end task restore: :gitlab_environment do - $progress.puts "Restoring artifacts ... ".color(:blue) - Backup::Artifacts.new.restore - $progress.puts "done".color(:green) + progress.puts "Restoring artifacts ... ".color(:blue) + Backup::Artifacts.new(progress).restore + progress.puts "done".color(:green) end end namespace :pages do task create: :gitlab_environment do - $progress.puts "Dumping pages ... ".color(:blue) + progress.puts "Dumping pages ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("pages") - $progress.puts "[SKIPPED]".color(:cyan) + progress.puts "[SKIPPED]".color(:cyan) else - Backup::Pages.new.dump - $progress.puts "done".color(:green) + Backup::Pages.new(progress).dump + progress.puts "done".color(:green) end end task restore: :gitlab_environment do - $progress.puts "Restoring pages ... ".color(:blue) - Backup::Pages.new.restore - $progress.puts "done".color(:green) + progress.puts "Restoring pages ... ".color(:blue) + Backup::Pages.new(progress).restore + progress.puts "done".color(:green) end end namespace :lfs do task create: :gitlab_environment do - $progress.puts "Dumping lfs objects ... ".color(:blue) + progress.puts "Dumping lfs objects ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("lfs") - $progress.puts "[SKIPPED]".color(:cyan) + progress.puts "[SKIPPED]".color(:cyan) else - Backup::Lfs.new.dump - $progress.puts "done".color(:green) + Backup::Lfs.new(progress).dump + progress.puts "done".color(:green) end end task restore: :gitlab_environment do - $progress.puts "Restoring lfs objects ... ".color(:blue) - Backup::Lfs.new.restore - $progress.puts "done".color(:green) + progress.puts "Restoring lfs objects ... ".color(:blue) + Backup::Lfs.new(progress).restore + progress.puts "done".color(:green) end end namespace :registry do task create: :gitlab_environment do - $progress.puts "Dumping container registry images ... ".color(:blue) + progress.puts "Dumping container registry images ... ".color(:blue) if Gitlab.config.registry.enabled if ENV["SKIP"] && ENV["SKIP"].include?("registry") - $progress.puts "[SKIPPED]".color(:cyan) + progress.puts "[SKIPPED]".color(:cyan) else - Backup::Registry.new.dump - $progress.puts "done".color(:green) + Backup::Registry.new(progress).dump + progress.puts "done".color(:green) end else - $progress.puts "[DISABLED]".color(:cyan) + progress.puts "[DISABLED]".color(:cyan) end end task restore: :gitlab_environment do - $progress.puts "Restoring container registry images ... ".color(:blue) + progress.puts "Restoring container registry images ... ".color(:blue) if Gitlab.config.registry.enabled - Backup::Registry.new.restore - $progress.puts "done".color(:green) + Backup::Registry.new(progress).restore + progress.puts "done".color(:green) else - $progress.puts "[DISABLED]".color(:cyan) + progress.puts "[DISABLED]".color(:cyan) end end end - def configure_cron_mode + def progress if ENV['CRON'] # We need an object we can say 'puts' and 'print' to; let's use a # StringIO. require 'stringio' - $progress = StringIO.new + StringIO.new else - $progress = $stdout + $stdout end end end # namespace end: backup diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake index 47ed522aec3..289aa5d9060 100644 --- a/lib/tasks/gitlab/info.rake +++ b/lib/tasks/gitlab/info.rake @@ -47,7 +47,7 @@ namespace :gitlab do puts "" puts "GitLab information".color(:yellow) puts "Version:\t#{Gitlab::VERSION}" - puts "Revision:\t#{Gitlab::REVISION}" + puts "Revision:\t#{Gitlab.revision}" puts "Directory:\t#{Rails.root}" puts "DB Adapter:\t#{database_adapter}" puts "URL:\t\t#{Gitlab.config.gitlab.url}" diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake index 523b0fa055b..2222807fe13 100644 --- a/lib/tasks/gitlab/test.rake +++ b/lib/tasks/gitlab/test.rake @@ -4,7 +4,6 @@ namespace :gitlab do cmds = [ %w(rake brakeman), %w(rake rubocop), - %w(rake spinach), %w(rake spec), %w(rake karma) ] diff --git a/lib/tasks/migrate/add_limits_mysql.rake b/lib/tasks/migrate/add_limits_mysql.rake index c6204f89de4..9b05876034c 100644 --- a/lib/tasks/migrate/add_limits_mysql.rake +++ b/lib/tasks/migrate/add_limits_mysql.rake @@ -2,6 +2,7 @@ require Rails.root.join('db/migrate/limits_to_mysql') require Rails.root.join('db/migrate/markdown_cache_limits_to_mysql') require Rails.root.join('db/migrate/merge_request_diff_file_limits_to_mysql') require Rails.root.join('db/migrate/limits_ci_build_trace_chunks_raw_data_for_mysql') +require Rails.root.join('db/migrate/gpg_keys_limits_to_mysql') desc "GitLab | Add limits to strings in mysql database" task add_limits_mysql: :environment do @@ -10,4 +11,5 @@ task add_limits_mysql: :environment do MarkdownCacheLimitsToMysql.new.up MergeRequestDiffFileLimitsToMysql.new.up LimitsCiBuildTraceChunksRawDataForMysql.new.up + IncreaseMysqlTextLimitForGpgKeys.new.up end diff --git a/lib/tasks/migrate/composite_primary_keys.rake b/lib/tasks/migrate/composite_primary_keys.rake new file mode 100644 index 00000000000..eb112434dd9 --- /dev/null +++ b/lib/tasks/migrate/composite_primary_keys.rake @@ -0,0 +1,15 @@ +namespace :gitlab do + namespace :db do + desc 'GitLab | Adds primary keys to tables that only have composite unique keys' + task composite_primary_keys_add: :environment do + require Rails.root.join('db/optional_migrations/composite_primary_keys') + CompositePrimaryKeysMigration.new.up + end + + desc 'GitLab | Removes previously added composite primary keys' + task composite_primary_keys_drop: :environment do + require Rails.root.join('db/optional_migrations/composite_primary_keys') + CompositePrimaryKeysMigration.new.down + end + end +end diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake index af30ecb0e9b..e7aab50e42a 100644 --- a/lib/tasks/migrate/setup_postgresql.rake +++ b/lib/tasks/migrate/setup_postgresql.rake @@ -8,6 +8,7 @@ task setup_postgresql: :environment do require Rails.root.join('db/migrate/20170503185032_index_redirect_routes_path_for_like') require Rails.root.join('db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb') require Rails.root.join('db/migrate/20180215181245_users_name_lower_index.rb') + require Rails.root.join('db/migrate/20180504195842_project_name_lower_index.rb') require Rails.root.join('db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb') NamespacesProjectsPathLowerIndexes.new.up @@ -18,5 +19,6 @@ task setup_postgresql: :environment do IndexRedirectRoutesPathForLike.new.up AddIndexOnNamespacesLowerName.new.up UsersNameLowerIndex.new.up + ProjectNameLowerIndex.new.up AddPathIndexToRedirectRoutes.new.up end diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake deleted file mode 100644 index 19ff13f06c0..00000000000 --- a/lib/tasks/spinach.rake +++ /dev/null @@ -1,60 +0,0 @@ -Rake::Task["spinach"].clear if Rake::Task.task_defined?('spinach') - -namespace :spinach do - namespace :project do - desc "GitLab | Spinach | Run project commits, issues and merge requests spinach features" - task :half do - run_spinach_tests('@project_commits,@project_issues,@project_merge_requests') - end - - desc "GitLab | Spinach | Run remaining project spinach features" - task :rest do - run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets,~@project_commits,~@project_issues,~@project_merge_requests') - end - end - - desc "GitLab | Spinach | Run project spinach features" - task :project do - run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets') - end - - desc "GitLab | Spinach | Run other spinach features" - task :other do - run_spinach_tests('@admin,@dashboard,@profile,@public,@snippets') - end - - desc "GitLab | Spinach | Run other spinach features" - task :builds do - run_spinach_tests('@builds') - end -end - -desc "GitLab | Run spinach" -task :spinach do - run_spinach_tests(nil) -end - -def run_system_command(cmd) - system({ 'RAILS_ENV' => 'test', 'force' => 'yes' }, *cmd) -end - -def run_spinach_command(args) - run_system_command(%w(spinach -r rerun) + args) -end - -def run_spinach_tests(tags) - success = run_spinach_command(%W(--tags #{tags})) - 3.times do |_| - break if success - break unless File.exist?('tmp/spinach-rerun.txt') - - tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp) - puts '' - puts "Spinach tests for #{tags}: Retrying tests... #{tests}".color(:red) - puts '' - sleep(3) - success = run_spinach_command(tests) - end - - raise("spinach tests for #{tags} failed!") unless success -end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 20377dc81b2..608d2a584ba 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-05-09 09:24+0200\n" -"PO-Revision-Date: 2018-05-09 09:24+0200\n" +"POT-Creation-Date: 2018-05-21 12:38-0700\n" +"PO-Revision-Date: 2018-05-21 12:38-0700\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -53,6 +53,16 @@ msgid_plural "%d metrics" msgstr[0] "" msgstr[1] "" +msgid "%d staged change" +msgid_plural "%d staged changes" +msgstr[0] "" +msgstr[1] "" + +msgid "%d unstaged change" +msgid_plural "%d unstaged changes" +msgstr[0] "" +msgstr[1] "" + msgid "%s additional commit has been omitted to prevent performance issues." msgid_plural "%s additional commits have been omitted to prevent performance issues." msgstr[0] "" @@ -72,6 +82,12 @@ msgstr[1] "" msgid "%{loadingIcon} Started" msgstr "" +msgid "%{lock_path} is locked by GitLab User %{lock_user_id}" +msgstr "" + +msgid "%{nip_domain} can be used as an alternative to a custom domain." +msgstr "" + msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead" msgstr "" @@ -98,6 +114,9 @@ msgstr "" msgid "%{title} changes" msgstr "" +msgid "%{unstaged} unstaged and %{staged} staged changes" +msgstr "" + msgid "(checkout the %{link} for information on how to install it)." msgstr "" @@ -204,6 +223,9 @@ msgstr "" msgid "Active" msgstr "" +msgid "Active Sessions" +msgstr "" + msgid "Activity" msgstr "" @@ -312,6 +334,9 @@ msgstr "" msgid "An error occurred when toggling the notification subscription" msgstr "" +msgid "An error occurred while dismissing the alert. Refresh the page and try again." +msgstr "" + msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again." msgstr "" @@ -737,6 +762,9 @@ msgstr "" msgid "CI/CD settings" msgstr "" +msgid "CICD|A domain is required to use Auto Review Apps and Auto Deploy Stages." +msgstr "" + msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery." msgstr "" @@ -767,9 +795,6 @@ msgstr "" msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project." msgstr "" -msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages." -msgstr "" - msgid "Can run untagged jobs" msgstr "" @@ -1019,6 +1044,9 @@ msgstr "" msgid "ClusterIntegration|Environment scope" msgstr "" +msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab's Google Kubernetes Engine Integration." +msgstr "" + msgid "ClusterIntegration|GitLab Integration" msgstr "" @@ -1142,6 +1170,9 @@ msgstr "" msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration." msgstr "" +msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform" +msgstr "" + msgid "ClusterIntegration|Remove Kubernetes cluster integration" msgstr "" @@ -1232,6 +1263,9 @@ msgstr "" msgid "ClusterIntegration|properly configured" msgstr "" +msgid "ClusterIntegration|sign up" +msgstr "" + msgid "Collapse" msgstr "" @@ -1356,6 +1390,9 @@ msgstr "" msgid "Configure limits for web and API requests." msgstr "" +msgid "Configure push mirrors." +msgstr "" + msgid "Configure storage path and circuit breaker settings." msgstr "" @@ -1413,7 +1450,7 @@ msgstr "" msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images." msgstr "" -msgid "ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images." +msgid "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images." msgstr "" msgid "Continuous Integration and Deployment" @@ -1521,6 +1558,9 @@ msgstr "" msgid "CreateTokenToCloneLink|create a personal access token" msgstr "" +msgid "Created" +msgstr "" + msgid "Cron Timezone" msgstr "" @@ -1592,6 +1632,54 @@ msgstr[1] "" msgid "Deploy Keys" msgstr "" +msgid "DeployKeys|+%{count} others" +msgstr "" + +msgid "DeployKeys|Current project" +msgstr "" + +msgid "DeployKeys|Deploy key" +msgstr "" + +msgid "DeployKeys|Enabled deploy keys" +msgstr "" + +msgid "DeployKeys|Error enabling deploy key" +msgstr "" + +msgid "DeployKeys|Error getting deploy keys" +msgstr "" + +msgid "DeployKeys|Error removing deploy key" +msgstr "" + +msgid "DeployKeys|Expand %{count} other projects" +msgstr "" + +msgid "DeployKeys|Loading deploy keys" +msgstr "" + +msgid "DeployKeys|No deploy keys found. Create one with the form above." +msgstr "" + +msgid "DeployKeys|Privately accessible deploy keys" +msgstr "" + +msgid "DeployKeys|Project usage" +msgstr "" + +msgid "DeployKeys|Publicly accessible deploy keys" +msgstr "" + +msgid "DeployKeys|Read access only" +msgstr "" + +msgid "DeployKeys|Write access allowed" +msgstr "" + +msgid "DeployKeys|You are going to remove this deploy key. Are you sure?" +msgstr "" + msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})" msgstr "" @@ -1748,9 +1836,6 @@ msgstr "" msgid "Edit files in the editor and commit changes here" msgstr "" -msgid "Editing" -msgstr "" - msgid "Email" msgstr "" @@ -1790,6 +1875,12 @@ msgstr "" msgid "Enable the Performance Bar for a given group." msgstr "" +msgid "Ends at (UTC)" +msgstr "" + +msgid "Environments" +msgstr "" + msgid "Environments|An error occurred while fetching the environments." msgstr "" @@ -2002,6 +2093,9 @@ msgstr "" msgid "GPG Keys" msgstr "" +msgid "General" +msgstr "" + msgid "Generate a default set of labels" msgstr "" @@ -2050,6 +2144,9 @@ msgstr "" msgid "Got it!" msgstr "" +msgid "Graph" +msgstr "" + msgid "Group CI/CD settings" msgstr "" @@ -2163,6 +2260,18 @@ msgstr "" msgid "Housekeeping successfully started" msgstr "" +msgid "IDE|Commit" +msgstr "" + +msgid "IDE|Edit" +msgstr "" + +msgid "IDE|Go back" +msgstr "" + +msgid "IDE|Review" +msgstr "" + msgid "If you already have files you can push them using the %{link_to_cli} below." msgstr "" @@ -2196,6 +2305,9 @@ msgstr "" msgid "Instance does not support multiple Kubernetes clusters" msgstr "" +msgid "Integrations" +msgstr "" + msgid "Interested parties can even contribute by pushing commits if they want to." msgstr "" @@ -2333,6 +2445,9 @@ msgstr "" msgid "LastPushEvent|at" msgstr "" +msgid "Latest changes" +msgstr "" + msgid "Learn more" msgstr "" @@ -2357,6 +2472,9 @@ msgstr "" msgid "Leave project" msgstr "" +msgid "List" +msgstr "" + msgid "List your GitHub repositories" msgstr "" @@ -2372,6 +2490,9 @@ msgstr "" msgid "Lock %{issuableDisplayName}" msgstr "" +msgid "Lock not found" +msgstr "" + msgid "Lock to current projects" msgstr "" @@ -2459,6 +2580,9 @@ msgstr "" msgid "Milestone" msgstr "" +msgid "Milestones" +msgstr "" + msgid "Milestones|Delete milestone" msgstr "" @@ -2716,6 +2840,9 @@ msgstr "" msgid "Opens in a new window" msgstr "" +msgid "Operations" +msgstr "" + msgid "Options" msgstr "" @@ -2875,25 +3002,22 @@ msgstr "" msgid "Pipelines|This project is not currently set up to run pipelines." msgstr "" -msgid "Pipeline|Existing branch name, tag" +msgid "Pipeline|Create for" msgstr "" -msgid "Pipeline|Retry pipeline" +msgid "Pipeline|Create pipeline" msgstr "" -msgid "Pipeline|Retry pipeline #%{pipelineId}?" +msgid "Pipeline|Existing branch name or tag" msgstr "" msgid "Pipeline|Run Pipeline" msgstr "" -msgid "Pipeline|Run on" -msgstr "" - -msgid "Pipeline|Run pipeline" +msgid "Pipeline|Search branches" msgstr "" -msgid "Pipeline|Search branches" +msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default." msgstr "" msgid "Pipeline|Stop pipeline" @@ -2902,7 +3026,7 @@ msgstr "" msgid "Pipeline|Stop pipeline #%{pipelineId}?" msgstr "" -msgid "Pipeline|You’re about to retry pipeline %{pipelineId}." +msgid "Pipeline|Variables" msgstr "" msgid "Pipeline|You’re about to stop pipeline %{pipelineId}." @@ -3019,6 +3143,9 @@ msgstr "" msgid "Progress" msgstr "" +msgid "Project" +msgstr "" + msgid "Project '%{project_name}' is in the process of being deleted." msgstr "" @@ -3070,9 +3197,6 @@ msgstr "" msgid "ProjectLifecycle|Stage" msgstr "" -msgid "ProjectNetworkGraph|Graph" -msgstr "" - msgid "Projects" msgstr "" @@ -3223,6 +3347,9 @@ msgstr "" msgid "Register and see your runners for this group." msgstr "" +msgid "Registry" +msgstr "" + msgid "Related Commits" msgstr "" @@ -3265,6 +3392,9 @@ msgstr "" msgid "Repository maintenance" msgstr "" +msgid "Repository mirror settings" +msgstr "" + msgid "Repository storage" msgstr "" @@ -3283,6 +3413,9 @@ msgstr "" msgid "Reset runners registration token" msgstr "" +msgid "Resolve conflicts on source branch" +msgstr "" + msgid "Resolve discussion" msgstr "" @@ -3360,6 +3493,9 @@ msgstr "" msgid "Search" msgstr "" +msgid "Search branches" +msgstr "" + msgid "Search branches and tags" msgstr "" @@ -3402,6 +3538,9 @@ msgstr "" msgid "Select branch/tag" msgstr "" +msgid "Select source branch" +msgstr "" + msgid "Select target branch" msgstr "" @@ -3653,6 +3792,9 @@ msgstr "" msgid "Started" msgstr "" +msgid "Starts at (UTC)" +msgstr "" + msgid "Status" msgstr "" @@ -3934,6 +4076,9 @@ msgstr "" msgid "This merge request is locked." msgstr "" +msgid "This option is disabled while you still have unstaged changes" +msgstr "" + msgid "This page is unavailable because you are not allowed to read information across multiple projects." msgstr "" @@ -4142,6 +4287,9 @@ msgstr "" msgid "Toggle Sidebar" msgstr "" +msgid "Toggle discussion" +msgstr "" + msgid "Toggle sidebar" msgstr "" @@ -4250,9 +4398,6 @@ msgstr "" msgid "Verified" msgstr "" -msgid "View and edit lines" -msgstr "" - msgid "View file @ " msgstr "" @@ -4370,6 +4515,12 @@ msgstr "" msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?" msgstr "" +msgid "WikiPageConfirmDelete|Delete page" +msgstr "" + +msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?" +msgstr "" + msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs." msgstr "" @@ -4460,12 +4611,21 @@ msgstr "" msgid "You can only edit files when you are on a branch" msgstr "" +msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}" +msgstr "" + msgid "You cannot write to this read-only GitLab instance." msgstr "" +msgid "You have no permissions" +msgstr "" + msgid "You have reached your project limit" msgstr "" +msgid "You must have master access to force delete a lock" +msgstr "" + msgid "You must sign in to star a project" msgstr "" diff --git a/package.json b/package.json index 6d5932dcec6..13be495b702 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js" }, "dependencies": { - "@gitlab-org/gitlab-svgs": "^1.20.0", + "@gitlab-org/gitlab-svgs": "^1.23.0", "autosize": "^4.0.0", "axios": "^0.17.1", "babel-core": "^6.26.3", @@ -25,8 +25,9 @@ "babel-preset-latest": "^6.24.1", "babel-preset-stage-2": "^6.24.1", "blackst0ne-mermaid": "^7.1.0-fixed", - "bootstrap-sass": "^3.3.6", + "bootstrap": "4.1", "brace-expansion": "^1.1.8", + "cache-loader": "^1.2.2", "chart.js": "1.0.2", "classlist-polyfill": "^1.2.0", "clipboard": "^1.7.1", @@ -65,6 +66,7 @@ "monaco-editor": "0.10.0", "mousetrap": "^1.4.6", "pikaday": "^1.6.1", + "popper.js": "^1.14.3", "prismjs": "^1.6.0", "raphael": "^2.2.7", "raven-js": "^3.22.1", @@ -73,6 +75,7 @@ "select2": "3.5.2-browserify", "sha1": "^1.1.1", "sql.js": "^0.4.0", + "stickyfilljs": "^2.0.5", "style-loader": "^0.21.0", "svg4everybody": "2.1.9", "three": "^0.84.0", @@ -83,7 +86,7 @@ "url-loader": "^1.0.1", "visibilityjs": "^1.2.4", "vue": "^2.5.16", - "vue-loader": "^14.1.1", + "vue-loader": "^15.2.0", "vue-resource": "^1.5.0", "vue-router": "^3.0.1", "vue-template-compiler": "^2.5.16", diff --git a/qa/qa/page/menu/main.rb b/qa/qa/page/menu/main.rb index d3562effaab..644fedecc90 100644 --- a/qa/qa/page/menu/main.rb +++ b/qa/qa/page/menu/main.rb @@ -10,7 +10,7 @@ module QA view 'app/views/layouts/header/_default.html.haml' do element :navbar element :user_avatar - element :user_menu, '.dropdown-menu-nav' + element :user_menu, '.dropdown-menu' end view 'app/views/layouts/nav/_dashboard.html.haml' do @@ -70,7 +70,7 @@ module QA within_top_menu do click_element :user_avatar - page.within('.dropdown-menu-nav') do + page.within('.dropdown-menu') do yield end end diff --git a/qa/qa/specs/features/merge_request/create_spec.rb b/qa/qa/specs/features/merge_request/create_spec.rb index fbf9a4d17e5..0931e649e24 100644 --- a/qa/qa/specs/features/merge_request/create_spec.rb +++ b/qa/qa/specs/features/merge_request/create_spec.rb @@ -11,7 +11,7 @@ module QA expect(page).to have_content('This is a merge request') expect(page).to have_content('Great feature') - expect(page).to have_content('Opened less than a minute ago') + expect(page).to have_content(/Opened [\w\s]+ a minute ago/) end end end diff --git a/qa/qa/specs/features/project/deploy_key_clone_spec.rb b/qa/qa/specs/features/project/deploy_key_clone_spec.rb index bf8fa230244..442ac312b4d 100644 --- a/qa/qa/specs/features/project/deploy_key_clone_spec.rb +++ b/qa/qa/specs/features/project/deploy_key_clone_spec.rb @@ -33,13 +33,15 @@ module QA end keys = [ - Runtime::Key::RSA.new(8192), - Runtime::Key::ECDSA.new(521), - Runtime::Key::ED25519.new + [Runtime::Key::RSA, 8192], + [Runtime::Key::ECDSA, 521], + [Runtime::Key::ED25519] ] - keys.each do |key| - scenario "user sets up a deploy key with #{key.name}(#{key.bits}) to clone code using pipelines" do + keys.each do |(key_class, bits)| + scenario "user sets up a deploy key with #{key_class}(#{bits}) to clone code using pipelines" do + key = key_class.new(*bits) + login Factory::Resource::DeployKey.fabricate! do |resource| diff --git a/scripts/create_mysql_user.sh b/scripts/create_mysql_user.sh index 286b1325f1d..35f68c581f3 100644 --- a/scripts/create_mysql_user.sh +++ b/scripts/create_mysql_user.sh @@ -1,7 +1,6 @@ #!/bin/bash mysql --user=root --host=mysql <<EOF -CREATE DATABASE IF NOT EXISTS gitlabhq_test DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER IF NOT EXISTS 'gitlab'@'%'; GRANT ALL PRIVILEGES ON gitlabhq_test.* TO 'gitlab'@'%'; FLUSH PRIVILEGES; diff --git a/scripts/create_postgres_user.sh b/scripts/create_postgres_user.sh index 8a744df3226..8a049bcd7fb 100644 --- a/scripts/create_postgres_user.sh +++ b/scripts/create_postgres_user.sh @@ -1,8 +1,6 @@ #!/bin/bash psql -h postgres -U postgres postgres <<EOF -DROP DATABASE IF EXISTS gitlabhq_test; -CREATE DATABASE gitlabhq_test; CREATE USER gitlab; -GRANT ALL PRIVILEGES ON DATABASE gitlabhq_test TO gitlab; +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO gitlab; EOF diff --git a/scripts/gitaly-test-build b/scripts/gitaly-test-build index b42ae2a2595..374401caf89 100755 --- a/scripts/gitaly-test-build +++ b/scripts/gitaly-test-build @@ -2,28 +2,29 @@ require 'fileutils' +require_relative 'gitaly_test' + # This script assumes tmp/tests/gitaly already contains the correct # Gitaly version. We just have to compile it and run its 'bundle -# install'. We have this separate script for that because weird things -# were happening in CI when we have a 'bundle exec' process that later -# called 'bundle install' using a different Gemfile, as happens with -# gitlab-ce and gitaly. +# install'. We have this separate script for that to avoid bundle +# poisoning in CI. This script should only be run in CI. +class GitalyTestBuild + include GitalyTest -tmp_tests_gitaly_dir = File.expand_path('../tmp/tests/gitaly', __dir__) + def run + abort 'gitaly build failed' unless system(env, 'make', chdir: tmp_tests_gitaly_dir) -# Use the top-level bundle vendor folder so that we don't reinstall gems twice -bundle_vendor_path = File.expand_path('../vendor', __dir__) + check_gitaly_config! -env = { - # This ensure the `clean` config set in `scripts/prepare_build.sh` isn't taken into account - 'BUNDLE_IGNORE_CONFIG' => 'true', - 'BUNDLE_GEMFILE' => File.join(tmp_tests_gitaly_dir, 'ruby', 'Gemfile'), - 'BUNDLE_FLAGS' => "--jobs=4 --path=#{bundle_vendor_path} --retry=3" -} + # Starting gitaly further validates its configuration + pid = start_gitaly + Process.kill('TERM', pid) -abort 'gitaly build failed' unless system(env, 'make', chdir: tmp_tests_gitaly_dir) + # Make the 'gitaly' executable look newer than 'GITALY_SERVER_VERSION'. + # Without this a gitaly executable created in the setup-test-env job + # will look stale compared to GITALY_SERVER_VERSION. + FileUtils.touch(File.join(tmp_tests_gitaly_dir, 'gitaly'), mtime: Time.now + (1 << 24)) + end +end -# Make the 'gitaly' executable look newer than 'GITALY_SERVER_VERSION'. -# Without this a gitaly executable created in the setup-test-env job -# will look stale compared to GITALY_SERVER_VERSION. -FileUtils.touch(File.join(tmp_tests_gitaly_dir, 'gitaly'), mtime: Time.now + (1 << 24)) +GitalyTestBuild.new.run diff --git a/scripts/gitaly-test-spawn b/scripts/gitaly-test-spawn index ecb68c6acc6..e9f91f75650 100755 --- a/scripts/gitaly-test-spawn +++ b/scripts/gitaly-test-spawn @@ -1,9 +1,23 @@ #!/usr/bin/env ruby -gitaly_dir = 'tmp/tests/gitaly' -env = { 'HOME' => File.expand_path('tmp/tests'), - 'GEM_PATH' => Gem.path.join(':') } -args = %W[#{gitaly_dir}/gitaly #{gitaly_dir}/config.toml] +# This script is used both in CI and in local development 'rspec' runs. -# Print the PID of the spawned process -puts spawn(env, *args, [:out, :err] => 'log/gitaly-test.log') +require_relative 'gitaly_test' + +class GitalyTestSpawn + include GitalyTest + + def run + check_gitaly_config! + + # # Uncomment line below to see all gitaly logs merged into CI trace + # spawn('sleep 1; tail -f log/gitaly-test.log') + + pid = start_gitaly + + # In local development this pid file is used by rspec. + IO.write(File.expand_path('../tmp/tests/gitaly.pid', __dir__), pid) + end +end + +GitalyTestSpawn.new.run diff --git a/scripts/gitaly_test.rb b/scripts/gitaly_test.rb new file mode 100644 index 00000000000..dee4c2eba7e --- /dev/null +++ b/scripts/gitaly_test.rb @@ -0,0 +1,97 @@ +# This file contains environment settings for gitaly when it's running +# as part of the gitlab-ce/ee test suite. +# +# Please be careful when modifying this file. Your changes must work +# both for local development rspec runs, and in CI. + +require 'socket' + +module GitalyTest + def tmp_tests_gitaly_dir + File.expand_path('../tmp/tests/gitaly', __dir__) + end + + def gemfile + File.join(tmp_tests_gitaly_dir, 'ruby', 'Gemfile') + end + + def env + env_hash = { + 'HOME' => File.expand_path('tmp/tests'), + 'GEM_PATH' => Gem.path.join(':'), + 'BUNDLE_APP_CONFIG' => File.join(File.dirname(gemfile), '.bundle/config'), + 'BUNDLE_FLAGS' => "--jobs=4 --retry=3", + 'BUNDLE_INSTALL_FLAGS' => nil, + 'BUNDLE_GEMFILE' => gemfile, + 'RUBYOPT' => nil + } + + if ENV['CI'] + bundle_path = File.expand_path('../vendor/gitaly-ruby', __dir__) + env_hash['BUNDLE_FLAGS'] << " --path=#{bundle_path}" + end + + env_hash + end + + def config_path + File.join(tmp_tests_gitaly_dir, 'config.toml') + end + + def start_gitaly + args = %W[#{tmp_tests_gitaly_dir}/gitaly #{config_path}] + pid = spawn(env, *args, [:out, :err] => 'log/gitaly-test.log') + + begin + try_connect! + rescue + Process.kill('TERM', pid) + raise + end + + pid + end + + def check_gitaly_config! + puts 'Checking gitaly-ruby bundle...' + abort 'bundle check failed' unless system(env, 'bundle', 'check', chdir: File.dirname(gemfile)) + end + + def read_socket_path + # This code needs to work in an environment where we cannot use bundler, + # so we cannot easily use the toml-rb gem. This ad-hoc parser should be + # good enough. + config_text = IO.read(config_path) + + config_text.lines.each do |line| + match_data = line.match(/^\s*socket_path\s*=\s*"([^"]*)"$/) + + return match_data[1] if match_data + end + + raise "failed to find socket_path in #{config_path}" + end + + def try_connect! + print "Trying to connect to gitaly: " + timeout = 20 + delay = 0.1 + socket = read_socket_path + + Integer(timeout / delay).times do + begin + UNIXSocket.new(socket) + puts ' OK' + + return + rescue Errno::ENOENT, Errno::ECONNREFUSED + print '.' + sleep delay + end + end + + puts ' FAILED' + + raise "could not connect to #{socket}" + end +end diff --git a/scripts/no-ee-check b/scripts/no-ee-check new file mode 100755 index 00000000000..29d319dc822 --- /dev/null +++ b/scripts/no-ee-check @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +ee_path = File.join(File.expand_path(__dir__), '../ee') + +if Dir.exist?(ee_path) + puts 'The repository contains /ee directory. There should be no /ee directory in CE repo.' + exit 1 +end diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index 206d62dbc78..75a3cea0448 100644 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -11,7 +11,7 @@ fi # Only install knapsack after bundle install! Otherwise oddly some native # gems could not be found under some circumstance. No idea why, hours wasted. -retry gem install knapsack +retry gem install knapsack --no-ri --no-rdoc cp config/gitlab.yml.example config/gitlab.yml sed -i 's/bin_path: \/usr\/bin\/git/bin_path: \/usr\/local\/bin\/git/' config/gitlab.yml @@ -49,20 +49,8 @@ sed -i 's/localhost/redis/g' config/redis.queues.yml cp config/redis.shared_state.yml.example config/redis.shared_state.yml sed -i 's/localhost/redis/g' config/redis.shared_state.yml -# Some tasks (e.g. db:seed_fu) need to have a properly-configured database -# user but not necessarily a full schema loaded -if [ "$CREATE_DB_USER" != "false" ]; then - if [ "$GITLAB_DATABASE" = 'postgresql' ]; then - . scripts/create_postgres_user.sh - else - . scripts/create_mysql_user.sh - fi -fi - if [ "$SETUP_DB" != "false" ]; then - bundle exec rake db:drop db:create db:schema:load db:migrate - - if [ "$GITLAB_DATABASE" = "mysql" ]; then - bundle exec rake add_limits_mysql - fi + setup_db +elif getent hosts postgres || getent hosts mysql; then + setup_db_user_only fi diff --git a/scripts/trigger-build-cloud-native b/scripts/trigger-build-cloud-native new file mode 100755 index 00000000000..b6ca75a588d --- /dev/null +++ b/scripts/trigger-build-cloud-native @@ -0,0 +1,61 @@ +#!/usr/bin/env ruby + +require 'gitlab' + +# +# Configure credentials to be used with gitlab gem +# +Gitlab.configure do |config| + config.endpoint = 'https://gitlab.com/api/v4' +end + +# +# The remote project +# +GITLAB_CNG_REPO = 'gitlab-org/build/CNG'.freeze + +def ee? + ENV['CI_PROJECT_NAME'] == 'gitlab-ee' || File.exist?('CHANGELOG-EE.md') +end + +def read_file_version(filename) + raw_version = File.read(filename).strip + + # if the version matches semver format, treat it as a tag and prepend `v` + if raw_version =~ Regexp.compile(/^\d+\.\d+\.\d+(-rc\d+)?(-ee)?$/) + "v#{raw_version}" + else + raw_version + end +end + +def params + params = { + 'GITLAB_SHELL_VERSION' => read_file_version('GITLAB_SHELL_VERSION'), + 'GITALY_VERSION' => read_file_version('GITALY_SERVER_VERSION'), + 'TRIGGERED_USER' => ENV['GITLAB_USER_NAME'], + 'TRIGGER_SOURCE' => "https://gitlab.com/gitlab-org/#{ENV['CI_PROJECT_NAME']}/-/jobs/#{ENV['CI_JOB_ID']}" + } + + if ee? + params['EE_PIPELINE'] = 'true' + params['GITLAB_EE_VERSION'] = ENV['CI_COMMIT_REF_NAME'] + else + params['CE_PIPELINE'] = 'true' + params['GITLAB_CE_VERSION'] = ENV['CI_COMMIT_REF_NAME'] + end + + params +end + +# +# Trigger a pipeline +# +def trigger_pipeline + # Create the cross project pipeline using CI_JOB_TOKEN + pipeline = Gitlab.run_trigger(GITLAB_CNG_REPO, ENV['CI_JOB_TOKEN'], 'master', params) + + puts "Triggered https://gitlab.com/#{GITLAB_CNG_REPO}/pipelines/#{pipeline.id}" +end + +trigger_pipeline diff --git a/scripts/utils.sh b/scripts/utils.sh index 6faa701f0ce..2d2ba115563 100644 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -12,3 +12,21 @@ retry() { done return 1 } + +setup_db_user_only() { + if [ "$GITLAB_DATABASE" = "postgresql" ]; then + . scripts/create_postgres_user.sh + else + . scripts/create_mysql_user.sh + fi +} + +setup_db() { + setup_db_user_only + + bundle exec rake db:drop db:create db:schema:load db:migrate + + if [ "$GITLAB_DATABASE" = "mysql" ]; then + bundle exec rake add_limits_mysql + fi +} diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb index f4c99ea4064..58bb91a0c80 100644 --- a/spec/controllers/concerns/send_file_upload_spec.rb +++ b/spec/controllers/concerns/send_file_upload_spec.rb @@ -51,6 +51,21 @@ describe SendFileUpload do end end + context 'with attachment' do + subject { controller.send_upload(uploader, attachment: 'test.js') } + + it 'sends a file with content-type of text/plain' do + expected_params = { + content_type: 'text/plain', + filename: 'test.js', + disposition: 'attachment' + } + expect(controller).to receive(:send_file).with(uploader.path, expected_params) + + subject + end + end + context 'when remote file is used' do before do stub_uploads_object_storage(uploader: uploader_class) diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index 694c64ae1ad..003fec8ac68 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -79,41 +79,18 @@ describe Projects::CommitController do end describe "as diff" do - include_examples "export as", :diff - let(:format) { :diff } + it "triggers workhorse to serve the request" do + go(id: commit.id, format: :diff) - it "should really only be a git diff" do - go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format) - - expect(response.body).to start_with("diff --git") - end - - it "is only be a git diff without whitespace changes" do - go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format, w: 1) - - expect(response.body).to start_with("diff --git") - - # without whitespace option, there are more than 2 diff_splits for other formats - diff_splits = assigns(:diffs).diff_files.first.diff.diff.split("\n") - expect(diff_splits.length).to be <= 2 + expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-diff:") end end describe "as patch" do - include_examples "export as", :patch - let(:format) { :patch } - let(:commit2) { project.commit('498214de67004b1da3d820901307bed2a68a8ef6') } - - it "is a git email patch" do - go(id: commit2.id, format: format) - - expect(response.body).to start_with("From #{commit2.id}") - end - it "contains a git diff" do - go(id: commit2.id, format: format) + go(id: commit.id, format: :patch) - expect(response.body).to match(/^diff --git/) + expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-format-patch:") end end diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 2281cb420d9..a08fcea27a5 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -490,43 +490,43 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do id: job.id end - context 'when job has a trace artifact' do + context "when job has a trace artifact" do let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) } it 'returns a trace' do response = subject expect(response).to have_gitlab_http_status(:ok) - expect(response.content_type).to eq 'text/plain; charset=utf-8' - expect(response.body).to eq job.job_artifacts_trace.open.read + expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8") + expect(response.body).to eq(job.job_artifacts_trace.open.read) end end - context 'when job has a trace file' do + context "when job has a trace file" do let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) } - it 'send a trace file' do + it "send a trace file" do response = subject expect(response).to have_gitlab_http_status(:ok) - expect(response.content_type).to eq 'text/plain; charset=utf-8' - expect(response.body).to eq 'BUILD TRACE' + expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8") + expect(response.body).to eq("BUILD TRACE") end end - context 'when job has a trace in database' do + context "when job has a trace in database" do let(:job) { create(:ci_build, pipeline: pipeline) } before do - job.update_column(:trace, 'Sample trace') + job.update_column(:trace, "Sample trace") end - it 'send a trace file' do + it "send a trace file" do response = subject expect(response).to have_gitlab_http_status(:ok) - expect(response.content_type).to eq 'text/plain; charset=utf-8' - expect(response.body).to eq 'Sample trace' + expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8") + expect(response.body).to eq("Sample trace") end end diff --git a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb index 2d7647a6e12..397cc79bde4 100644 --- a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb @@ -5,7 +5,7 @@ describe Projects::MergeRequests::ConflictsController do let(:user) { project.owner } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } let(:merge_request_with_conflicts) do - create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) do |mr| + create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project, merge_status: :unchecked) do |mr| mr.mark_as_unmergeable end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index c8cc6b374f6..d3042be9e8b 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -7,7 +7,7 @@ describe Projects::MergeRequestsController do let(:user) { project.owner } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } let(:merge_request_with_conflicts) do - create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) do |mr| + create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project, merge_status: :unchecked) do |mr| mr.mark_as_unmergeable end end diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index a451bbb97b6..9e7bc20a6d1 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -35,10 +35,16 @@ describe Projects::PipelinesController do expect(json_response).to include('pipelines') expect(json_response['pipelines'].count).to eq 4 - expect(json_response['count']['all']).to eq 4 - expect(json_response['count']['running']).to eq 1 - expect(json_response['count']['pending']).to eq 1 - expect(json_response['count']['finished']).to eq 1 + expect(json_response['count']['all']).to eq '4' + expect(json_response['count']['running']).to eq '1' + expect(json_response['count']['pending']).to eq '1' + expect(json_response['count']['finished']).to eq '1' + end + + it 'does not include coverage data for the pipelines' do + subject + + expect(json_response['pipelines'][0]).not_to include('coverage') end context 'when performing gitaly calls', :request_store do diff --git a/spec/controllers/projects/prometheus/metrics_controller_spec.rb b/spec/controllers/projects/prometheus/metrics_controller_spec.rb index b2b245dba90..871dcf5c796 100644 --- a/spec/controllers/projects/prometheus/metrics_controller_spec.rb +++ b/spec/controllers/projects/prometheus/metrics_controller_spec.rb @@ -12,44 +12,67 @@ describe Projects::Prometheus::MetricsController do end describe 'GET #active_common' do - before do - allow(controller).to receive(:prometheus_adapter).and_return(prometheus_adapter) - end + context 'when prometheus_adapter can query' do + before do + allow(controller).to receive(:prometheus_adapter).and_return(prometheus_adapter) + end - context 'when prometheus metrics are enabled' do - context 'when data is not present' do - before do - allow(prometheus_adapter).to receive(:query).with(:matched_metrics).and_return({}) - end + context 'when prometheus metrics are enabled' do + context 'when data is not present' do + before do + allow(prometheus_adapter).to receive(:query).with(:matched_metrics).and_return({}) + end - it 'returns no content response' do - get :active_common, project_params(format: :json) + it 'returns no content response' do + get :active_common, project_params(format: :json) - expect(response).to have_gitlab_http_status(204) + expect(response).to have_gitlab_http_status(204) + end end - end - context 'when data is available' do - let(:sample_response) { { some_data: 1 } } + context 'when data is available' do + let(:sample_response) { { some_data: 1 } } + + before do + allow(prometheus_adapter).to receive(:query).with(:matched_metrics).and_return(sample_response) + end - before do - allow(prometheus_adapter).to receive(:query).with(:matched_metrics).and_return(sample_response) + it 'returns no content response' do + get :active_common, project_params(format: :json) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to eq(sample_response.deep_stringify_keys) + end end - it 'returns no content response' do - get :active_common, project_params(format: :json) + context 'when requesting non json response' do + it 'returns not found response' do + get :active_common, project_params - expect(response).to have_gitlab_http_status(200) - expect(json_response).to eq(sample_response.deep_stringify_keys) + expect(response).to have_gitlab_http_status(404) + end end end + end - context 'when requesting non json response' do - it 'returns not found response' do - get :active_common, project_params + context 'when prometheus_adapter cannot query' do + it 'renders 404' do + prometheus_adapter = double('prometheus_adapter', can_query?: false) - expect(response).to have_gitlab_http_status(404) - end + allow(controller).to receive(:prometheus_adapter).and_return(prometheus_adapter) + allow(prometheus_adapter).to receive(:query).with(:matched_metrics).and_return({}) + + get :active_common, project_params(format: :json) + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'when prometheus_adapter is disabled' do + it 'renders 404' do + get :active_common, project_params(format: :json) + + expect(response).to have_gitlab_http_status(404) end end end diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb index a91c868cbaf..f1810763d2d 100644 --- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb +++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb @@ -19,11 +19,11 @@ describe Projects::Settings::CiCdController do end context 'with group runners' do - let(:group_runner) { create(:ci_runner) } + let(:group_runner) { create(:ci_runner, runner_type: :group_type) } let(:parent_group) { create(:group) } let(:group) { create(:group, runners: [group_runner], parent: parent_group) } let(:other_project) { create(:project, group: group) } - let!(:project_runner) { create(:ci_runner, projects: [other_project]) } + let!(:project_runner) { create(:ci_runner, projects: [other_project], runner_type: :project_type) } let!(:shared_runner) { create(:ci_runner, :shared) } it 'sets assignable project runners only' do @@ -31,7 +31,7 @@ describe Projects::Settings::CiCdController do get :show, namespace_id: project.namespace, project_id: project - expect(assigns(:assignable_runners)).to eq [project_runner] + expect(assigns(:assignable_runners)).to contain_exactly(project_runner) end end end diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb index 98566f907f9..0430762c1ff 100644 --- a/spec/factories/clusters/clusters.rb +++ b/spec/factories/clusters/clusters.rb @@ -4,8 +4,8 @@ FactoryBot.define do name 'test-cluster' trait :project do - after(:create) do |cluster, evaluator| - cluster.projects << create(:project) + before(:create) do |cluster, evaluator| + cluster.projects << create(:project, :repository) end end diff --git a/spec/factories/gitaly/tag.rb b/spec/factories/gitaly/tag.rb new file mode 100644 index 00000000000..e99776d524a --- /dev/null +++ b/spec/factories/gitaly/tag.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :gitaly_tag, class: Gitaly::Tag do + skip_create + + name { 'v3.1.4' } + message { 'Pie release' } + target_commit factory: :gitaly_commit + end +end diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb index 978113a08a4..134eb25e4b1 100644 --- a/spec/fast_spec_helper.rb +++ b/spec/fast_spec_helper.rb @@ -3,14 +3,8 @@ require 'bundler/setup' ENV['GITLAB_ENV'] = 'test' ENV['IN_MEMORY_APPLICATION_SETTINGS'] = 'true' -unless Object.respond_to?(:require_dependency) - class Object - alias_method :require_dependency, :require - end -end - -# Defines Settings and Gitlab.config which are at the center of the app require_relative '../config/settings' -require_relative '../lib/gitlab' unless defined?(Gitlab.config) - require_relative 'support/rspec' +require 'active_support/all' + +ActiveSupport::Dependencies.autoload_paths << 'lib' diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb index 766cd4b0090..d8fcdebfc6d 100644 --- a/spec/features/admin/admin_abuse_reports_spec.rb +++ b/spec/features/admin/admin_abuse_reports_spec.rb @@ -45,7 +45,7 @@ describe "Admin::AbuseReports", :js do visit admin_abuse_reports_path expect(page).to have_selector('.pagination') - expect(page).to have_selector('.pagination .page', count: (report_count.to_f / AbuseReport.default_per_page).ceil) + expect(page).to have_selector('.pagination .js-pagination-page', count: (report_count.to_f / AbuseReport.default_per_page).ceil) end end end diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index 6d8350e99f1..328e8f25f89 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -34,7 +34,7 @@ describe "Admin::Projects" do expect(page).to have_content(project.name) expect(page).to have_content(archived_project.name) - expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived') + expect(page).to have_xpath("//span[@class='badge badge-warning']", text: 'archived') end it 'renders only archived projects', :js do diff --git a/spec/features/admin/admin_requests_profiles_spec.rb b/spec/features/admin/admin_requests_profiles_spec.rb index 380cd5d7703..2503fc9067d 100644 --- a/spec/features/admin/admin_requests_profiles_spec.rb +++ b/spec/features/admin/admin_requests_profiles_spec.rb @@ -33,12 +33,12 @@ describe 'Admin::RequestsProfilesController' do visit admin_requests_profiles_path - within('.panel', text: '/gitlab-org/gitlab-ce') do + within('.card', text: '/gitlab-org/gitlab-ce') do expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile1)}']", text: time1.to_s(:long)) expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile2)}']", text: time2.to_s(:long)) end - within('.panel', text: '/gitlab-com/infrastructure') do + within('.card', text: '/gitlab-com/infrastructure') do expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile3)}']", text: time3.to_s(:long)) end end diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index 3465ccfc423..c33014cbb31 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -68,7 +68,7 @@ describe "Admin Runners" do visit admin_runners_path within "#runner_#{runner.id}" do - expect(page).to have_selector '.label', text: 'group' + expect(page).to have_selector '.badge', text: 'group' expect(page).to have_text 'n/a' end end @@ -81,7 +81,7 @@ describe "Admin Runners" do visit admin_runners_path within "#runner_#{runner.id}" do - expect(page).to have_selector '.label', text: 'shared' + expect(page).to have_selector '.badge', text: 'shared' expect(page).to have_text 'n/a' end end @@ -95,7 +95,7 @@ describe "Admin Runners" do visit admin_runners_path within "#runner_#{runner.id}" do - expect(page).to have_selector '.label', text: 'specific' + expect(page).to have_selector '.badge', text: 'specific' expect(page).to have_text '1' end end diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index f2f9b734c39..dc025d82937 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -152,7 +152,7 @@ feature 'Admin updates settings' do scenario 'Change CI/CD settings' do page.within('.as-ci-cd') do - check 'Enabled Auto DevOps (Beta) for projects by default' + check 'Enabled Auto DevOps for projects by default' fill_in 'Auto devops domain', with: 'domain.com' click_button 'Save changes' end diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 8fc57f4b4c3..9e3221577c7 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -283,14 +283,14 @@ describe "Admin::Users" do end it "lists group projects" do - within(:css, '.append-bottom-default + .panel') do + within(:css, '.append-bottom-default + .card') do expect(page).to have_content 'Group projects' expect(page).to have_link group.name, href: admin_group_path(group) end end it 'allows navigation to the group details' do - within(:css, '.append-bottom-default + .panel') do + within(:css, '.append-bottom-default + .card') do click_link group.name end within(:css, 'h3.page-title') do @@ -300,7 +300,7 @@ describe "Admin::Users" do end it 'shows the group access level' do - within(:css, '.append-bottom-default + .panel') do + within(:css, '.append-bottom-default + .card') do expect(page).to have_content 'Developer' end end diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index 18901a954cc..7a14a441088 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -81,7 +81,7 @@ describe 'Issue Boards add issue modal', :js do expect(page).to have_content('2') end - expect(page).to have_selector('.card', count: 2) + expect(page).to have_selector('.board-card', count: 2) end end @@ -89,7 +89,7 @@ describe 'Issue Boards add issue modal', :js do page.within('.add-issues-modal') do click_link 'Selected issues' - expect(page).not_to have_selector('.card') + expect(page).not_to have_selector('.board-card') end end @@ -122,7 +122,7 @@ describe 'Issue Boards add issue modal', :js do wait_for_requests - expect(page).to have_selector('.card', count: 1) + expect(page).to have_selector('.board-card', count: 1) end end @@ -133,7 +133,7 @@ describe 'Issue Boards add issue modal', :js do wait_for_requests - expect(page).not_to have_selector('.card') + expect(page).not_to have_selector('.board-card') expect(page).not_to have_content("You haven't added any issues to your project yet") end end @@ -142,7 +142,7 @@ describe 'Issue Boards add issue modal', :js do context 'selecing issues' do it 'selects single issue' do page.within('.add-issues-modal') do - first('.card .card-number').click + first('.board-card .board-card-number').click page.within('.nav-links') do expect(page).to have_content('Selected issues 1') @@ -152,7 +152,7 @@ describe 'Issue Boards add issue modal', :js do it 'changes button text' do page.within('.add-issues-modal') do - first('.card .card-number').click + first('.board-card .board-card-number').click expect(first('.add-issues-footer .btn')).to have_content('Add 1 issue') end @@ -160,7 +160,7 @@ describe 'Issue Boards add issue modal', :js do it 'changes button text with plural' do page.within('.add-issues-modal') do - all('.card .card-number').each do |el| + all('.board-card .board-card-number').each do |el| el.click end @@ -170,11 +170,11 @@ describe 'Issue Boards add issue modal', :js do it 'shows only selected issues on selected tab' do page.within('.add-issues-modal') do - first('.card .card-number').click + first('.board-card .board-card-number').click click_link 'Selected issues' - expect(page).to have_selector('.card', count: 1) + expect(page).to have_selector('.board-card', count: 1) end end @@ -200,7 +200,7 @@ describe 'Issue Boards add issue modal', :js do it 'selects all that arent already selected' do page.within('.add-issues-modal') do - first('.card .card-number').click + first('.board-card .board-card-number').click expect(page).to have_selector('.is-active', count: 1) @@ -212,11 +212,11 @@ describe 'Issue Boards add issue modal', :js do it 'unselects from selected tab' do page.within('.add-issues-modal') do - first('.card .card-number').click + first('.board-card .board-card-number').click click_link 'Selected issues' - first('.card .card-number').click + first('.board-card .board-card-number').click expect(page).not_to have_selector('.is-active') end @@ -226,19 +226,19 @@ describe 'Issue Boards add issue modal', :js do context 'adding issues' do it 'adds to board' do page.within('.add-issues-modal') do - first('.card .card-number').click + first('.board-card .board-card-number').click click_button 'Add 1 issue' end page.within(find('.board:nth-child(2)')) do - expect(page).to have_selector('.card') + expect(page).to have_selector('.board-card') end end it 'adds to second list' do page.within('.add-issues-modal') do - first('.card .card-number').click + first('.board-card .board-card-number').click click_button planning.title @@ -248,7 +248,7 @@ describe 'Issue Boards add issue modal', :js do end page.within(find('.board:nth-child(3)')) do - expect(page).to have_selector('.card') + expect(page).to have_selector('.board-card') end end end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 52ff47d57f9..e414345ac23 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -92,9 +92,9 @@ describe 'Issue Boards', :js do wait_for_requests expect(page).to have_selector('.board', count: 4) - expect(find('.board:nth-child(2)')).to have_selector('.card') - expect(find('.board:nth-child(3)')).to have_selector('.card') - expect(find('.board:nth-child(4)')).to have_selector('.card') + expect(find('.board:nth-child(2)')).to have_selector('.board-card') + expect(find('.board:nth-child(3)')).to have_selector('.board-card') + expect(find('.board:nth-child(4)')).to have_selector('.board-card') end it 'shows description tooltip on list title' do @@ -120,9 +120,9 @@ describe 'Issue Boards', :js do wait_for_requests - expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) - expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) - expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1) + expect(find('.board:nth-child(2)')).to have_selector('.board-card', count: 0) + expect(find('.board:nth-child(3)')).to have_selector('.board-card', count: 0) + expect(find('.board:nth-child(4)')).to have_selector('.board-card', count: 1) end it 'search list' do @@ -131,9 +131,9 @@ describe 'Issue Boards', :js do wait_for_requests - expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1) - expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) - expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(2)')).to have_selector('.board-card', count: 1) + expect(find('.board:nth-child(3)')).to have_selector('.board-card', count: 0) + expect(find('.board:nth-child(4)')).to have_selector('.board-card', count: 0) end it 'allows user to delete board' do @@ -171,21 +171,21 @@ describe 'Issue Boards', :js do page.within(find('.board:nth-child(2)')) do expect(page.find('.board-header')).to have_content('58') - expect(page).to have_selector('.card', count: 20) + expect(page).to have_selector('.board-card', count: 20) expect(page).to have_content('Showing 20 of 58 issues') find('.board .board-list') evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") wait_for_requests - expect(page).to have_selector('.card', count: 40) + expect(page).to have_selector('.board-card', count: 40) expect(page).to have_content('Showing 40 of 58 issues') find('.board .board-list') evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") wait_for_requests - expect(page).to have_selector('.card', count: 58) + expect(page).to have_selector('.board-card', count: 58) expect(page).to have_content('Showing all issues') end end @@ -204,7 +204,7 @@ describe 'Issue Boards', :js do wait_for_board_cards(4, 2) expect(find('.board:nth-child(2)')).not_to have_content(issue9.title) - expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2) + expect(find('.board:nth-child(4)')).to have_selector('.board-card', count: 2) expect(find('.board:nth-child(4)')).to have_content(issue9.title) expect(find('.board:nth-child(4)')).not_to have_content(planning.title) end @@ -242,7 +242,7 @@ describe 'Issue Boards', :js do wait_for_board_cards(4, 1) expect(find('.board:nth-child(3)')).to have_content(issue6.title) - expect(find('.board:nth-child(3)').all('.card').last).to have_content(development.title) + expect(find('.board:nth-child(3)').all('.board-card').last).to have_content(development.title) end it 'issue moves between lists' do @@ -253,7 +253,7 @@ describe 'Issue Boards', :js do wait_for_board_cards(4, 1) expect(find('.board:nth-child(2)')).to have_content(issue7.title) - expect(find('.board:nth-child(2)').all('.card').first).to have_content(planning.title) + expect(find('.board:nth-child(2)').all('.board-card').first).to have_content(planning.title) end it 'issue moves from closed' do @@ -335,7 +335,7 @@ describe 'Issue Boards', :js do wait_for_requests - expect(page).to have_css('#js-add-list.open') + expect(page).to have_css('#js-add-list.show') end it 'creates new list from a new label' do @@ -425,12 +425,12 @@ describe 'Issue Boards', :js do page.within(find('.board:nth-child(2)')) do expect(page.find('.board-header')).to have_content('1') - expect(page).to have_selector('.card', count: 1) + expect(page).to have_selector('.board-card', count: 1) end page.within(find('.board:nth-child(3)')) do expect(page.find('.board-header')).to have_content('0') - expect(page).to have_selector('.card', count: 0) + expect(page).to have_selector('.board-card', count: 0) end end @@ -460,19 +460,19 @@ describe 'Issue Boards', :js do page.within(find('.board:nth-child(2)')) do expect(page.find('.board-header')).to have_content('51') - expect(page).to have_selector('.card', count: 20) + expect(page).to have_selector('.board-card', count: 20) expect(page).to have_content('Showing 20 of 51 issues') find('.board .board-list') evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") - expect(page).to have_selector('.card', count: 40) + expect(page).to have_selector('.board-card', count: 40) expect(page).to have_content('Showing 40 of 51 issues') find('.board .board-list') evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") - expect(page).to have_selector('.card', count: 51) + expect(page).to have_selector('.board-card', count: 51) expect(page).to have_content('Showing all issues') end end @@ -494,8 +494,8 @@ describe 'Issue Boards', :js do it 'filters by clicking label button on issue' do page.within(find('.board:nth-child(2)')) do - expect(page).to have_selector('.card', count: 8) - expect(find('.card', match: :first)).to have_content(bug.title) + expect(page).to have_selector('.board-card', count: 8) + expect(find('.board-card', match: :first)).to have_content(bug.title) click_button(bug.title) wait_for_requests end @@ -512,13 +512,13 @@ describe 'Issue Boards', :js do it 'removes label filter by clicking label button on issue' do page.within(find('.board:nth-child(2)')) do - page.within(find('.card', match: :first)) do + page.within(find('.board-card', match: :first)) do click_button(bug.title) end wait_for_requests - expect(page).to have_selector('.card', count: 1) + expect(page).to have_selector('.board-card', count: 1) end wait_for_requests @@ -589,7 +589,7 @@ describe 'Issue Boards', :js do def wait_for_board_cards(board_number, expected_cards) page.within(find(".board:nth-child(#{board_number})")) do expect(page.find('.board-header')).to have_content(expected_cards.to_s) - expect(page).to have_selector('.card', count: expected_cards) + expect(page).to have_selector('.board-card', count: expected_cards) end end diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb index 5abd02dbb48..193b1dfabbd 100644 --- a/spec/features/boards/issue_ordering_spec.rb +++ b/spec/features/boards/issue_ordering_spec.rb @@ -30,7 +30,7 @@ describe 'Issue Boards', :js do it 'has un-ordered issue as last issue' do page.within(find('.board:nth-child(2)')) do - expect(all('.card').last).to have_content(issue4.title) + expect(all('.board-card').last).to have_content(issue4.title) end end @@ -40,7 +40,7 @@ describe 'Issue Boards', :js do wait_for_requests page.within(find('.board:nth-child(2)')) do - expect(first('.card')).to have_content(issue4.title) + expect(first('.board-card')).to have_content(issue4.title) end end end @@ -58,7 +58,7 @@ describe 'Issue Boards', :js do wait_for_requests - expect(first('.card')).to have_content(issue2.title) + expect(first('.board-card')).to have_content(issue2.title) end it 'moves from middle to bottom' do @@ -66,7 +66,7 @@ describe 'Issue Boards', :js do wait_for_requests - expect(all('.card').last).to have_content(issue2.title) + expect(all('.board-card').last).to have_content(issue2.title) end it 'moves from top to bottom' do @@ -74,7 +74,7 @@ describe 'Issue Boards', :js do wait_for_requests - expect(all('.card').last).to have_content(issue3.title) + expect(all('.board-card').last).to have_content(issue3.title) end it 'moves from bottom to top' do @@ -82,7 +82,7 @@ describe 'Issue Boards', :js do wait_for_requests - expect(first('.card')).to have_content(issue1.title) + expect(first('.board-card')).to have_content(issue1.title) end it 'moves from top to middle' do @@ -90,7 +90,7 @@ describe 'Issue Boards', :js do wait_for_requests - expect(first('.card')).to have_content(issue2.title) + expect(first('.board-card')).to have_content(issue2.title) end it 'moves from bottom to middle' do @@ -98,7 +98,7 @@ describe 'Issue Boards', :js do wait_for_requests - expect(all('.card').last).to have_content(issue2.title) + expect(all('.board-card').last).to have_content(issue2.title) end end @@ -121,11 +121,11 @@ describe 'Issue Boards', :js do wait_for_requests - expect(find('.board:nth-child(2)')).to have_selector('.card', count: 2) - expect(all('.board')[2]).to have_selector('.card', count: 4) + expect(find('.board:nth-child(2)')).to have_selector('.board-card', count: 2) + expect(all('.board')[2]).to have_selector('.board-card', count: 4) page.within(all('.board')[2]) do - expect(first('.card')).to have_content(issue3.title) + expect(first('.board-card')).to have_content(issue3.title) end end @@ -134,11 +134,11 @@ describe 'Issue Boards', :js do wait_for_requests - expect(find('.board:nth-child(2)')).to have_selector('.card', count: 2) - expect(all('.board')[2]).to have_selector('.card', count: 4) + expect(find('.board:nth-child(2)')).to have_selector('.board-card', count: 2) + expect(all('.board')[2]).to have_selector('.board-card', count: 4) page.within(all('.board')[2]) do - expect(all('.card').last).to have_content(issue3.title) + expect(all('.board-card').last).to have_content(issue3.title) end end @@ -147,11 +147,11 @@ describe 'Issue Boards', :js do wait_for_requests - expect(find('.board:nth-child(2)')).to have_selector('.card', count: 2) - expect(all('.board')[2]).to have_selector('.card', count: 4) + expect(find('.board:nth-child(2)')).to have_selector('.board-card', count: 2) + expect(all('.board')[2]).to have_selector('.board-card', count: 4) page.within(all('.board')[2]) do - expect(all('.card')[1]).to have_content(issue3.title) + expect(all('.board-card')[1]).to have_content(issue3.title) end end end diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb index 5907bb0840f..be9c6a51c29 100644 --- a/spec/features/boards/modal_filter_spec.rb +++ b/spec/features/boards/modal_filter_spec.rb @@ -38,7 +38,7 @@ describe 'Issue Boards add issue modal filtering', :js do page.within('.add-issues-modal') do wait_for_requests - expect(page).to have_selector('.card', count: 0) + expect(page).to have_selector('.board-card', count: 0) click_button 'Cancel' end @@ -48,7 +48,7 @@ describe 'Issue Boards add issue modal filtering', :js do page.within('.add-issues-modal') do wait_for_requests - expect(page).to have_selector('.card', count: 1) + expect(page).to have_selector('.board-card', count: 1) end end @@ -62,13 +62,13 @@ describe 'Issue Boards add issue modal filtering', :js do page.within('.add-issues-modal') do wait_for_requests - expect(page).to have_selector('.card', count: 0) + expect(page).to have_selector('.board-card', count: 0) find('.clear-search').click wait_for_requests - expect(page).to have_selector('.card', count: 1) + expect(page).to have_selector('.board-card', count: 1) end end @@ -90,7 +90,7 @@ describe 'Issue Boards add issue modal filtering', :js do wait_for_requests expect(page).to have_selector('.js-visual-token', text: user2.name) - expect(page).to have_selector('.card', count: 1) + expect(page).to have_selector('.board-card', count: 1) end end end @@ -113,7 +113,7 @@ describe 'Issue Boards add issue modal filtering', :js do wait_for_requests expect(page).to have_selector('.js-visual-token', text: 'none') - expect(page).to have_selector('.card', count: 1) + expect(page).to have_selector('.board-card', count: 1) end end @@ -126,7 +126,7 @@ describe 'Issue Boards add issue modal filtering', :js do wait_for_requests expect(page).to have_selector('.js-visual-token', text: user2.name) - expect(page).to have_selector('.card', count: 1) + expect(page).to have_selector('.board-card', count: 1) end end end @@ -148,7 +148,7 @@ describe 'Issue Boards add issue modal filtering', :js do wait_for_requests expect(page).to have_selector('.js-visual-token', text: 'upcoming') - expect(page).to have_selector('.card', count: 0) + expect(page).to have_selector('.board-card', count: 0) end end @@ -161,7 +161,7 @@ describe 'Issue Boards add issue modal filtering', :js do wait_for_requests expect(page).to have_selector('.js-visual-token', text: milestone.name) - expect(page).to have_selector('.card', count: 1) + expect(page).to have_selector('.board-card', count: 1) end end end @@ -183,7 +183,7 @@ describe 'Issue Boards add issue modal filtering', :js do wait_for_requests expect(page).to have_selector('.js-visual-token', text: 'none') - expect(page).to have_selector('.card', count: 1) + expect(page).to have_selector('.board-card', count: 1) end end @@ -196,7 +196,7 @@ describe 'Issue Boards add issue modal filtering', :js do wait_for_requests expect(page).to have_selector('.js-visual-token', text: label.title) - expect(page).to have_selector('.card', count: 1) + expect(page).to have_selector('.board-card', count: 1) end end end diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index e880f0096c1..7a95f5cf871 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -64,7 +64,7 @@ describe 'Issue Boards new issue', :js do expect(page).to have_content('1') end - page.within(first('.card')) do + page.within(first('.board-card')) do issue = project.issues.find_by_title('bug') expect(page).to have_content(issue.to_reference) diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 4d31123a699..a03aa681827 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -15,7 +15,7 @@ describe 'Issue Boards', :js do let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) } let(:board) { create(:board, project: project) } let!(:list) { create(:list, board: board, label: development, position: 0) } - let(:card) { find('.board:nth-child(2)').first('.card') } + let(:card) { find('.board:nth-child(2)').first('.board-card') } around do |example| Timecop.freeze { example.run } @@ -75,7 +75,7 @@ describe 'Issue Boards', :js do wait_for_requests page.within(find('.board:nth-child(2)')) do - expect(page).to have_selector('.card', count: 1) + expect(page).to have_selector('.board-card', count: 1) end end @@ -86,11 +86,11 @@ describe 'Issue Boards', :js do visit project_board_path(project, board) wait_for_requests - click_card(find('.board:nth-child(1)').first('.card')) + click_card(find('.board:nth-child(1)').first('.board-card')) expect(find('.issue-boards-sidebar')).not_to have_button 'Remove from board' - click_card(find('.board:nth-child(3)').first('.card')) + click_card(find('.board:nth-child(3)').first('.board-card')) expect(find('.issue-boards-sidebar')).not_to have_button 'Remove from board' end @@ -117,7 +117,7 @@ describe 'Issue Boards', :js do end it 'removes the assignee' do - card_two = find('.board:nth-child(2)').find('.card:nth-child(2)') + card_two = find('.board:nth-child(2)').find('.board-card:nth-child(2)') click_card(card_two) page.within('.assignee') do @@ -171,7 +171,7 @@ describe 'Issue Boards', :js do end page.within(find('.board:nth-child(2)')) do - find('.card:nth-child(2)').click + find('.board-card:nth-child(2)').click end page.within('.assignee') do @@ -246,7 +246,7 @@ describe 'Issue Boards', :js do wait_for_requests page.within('.value') do - expect(page).to have_selector('.label', count: 2) + expect(page).to have_selector('.badge', count: 2) expect(page).to have_content(development.title) expect(page).to have_content(stretch.title) end @@ -268,12 +268,12 @@ describe 'Issue Boards', :js do find('.dropdown-menu-close-icon').click page.within('.value') do - expect(page).to have_selector('.label', count: 3) + expect(page).to have_selector('.badge', count: 3) expect(page).to have_content(bug.title) end end - expect(card).to have_selector('.label', count: 3) + expect(card).to have_selector('.badge', count: 3) expect(card).to have_content(bug.title) end @@ -293,13 +293,13 @@ describe 'Issue Boards', :js do find('.dropdown-menu-close-icon').click page.within('.value') do - expect(page).to have_selector('.label', count: 4) + expect(page).to have_selector('.badge', count: 4) expect(page).to have_content(bug.title) expect(page).to have_content(regression.title) end end - expect(card).to have_selector('.label', count: 4) + expect(card).to have_selector('.badge', count: 4) expect(card).to have_content(bug.title) expect(card).to have_content(regression.title) end @@ -321,12 +321,12 @@ describe 'Issue Boards', :js do find('.dropdown-menu-close-icon').click page.within('.value') do - expect(page).to have_selector('.label', count: 1) + expect(page).to have_selector('.badge', count: 1) expect(page).not_to have_content(stretch.title) end end - expect(card).to have_selector('.label', count: 1) + expect(card).to have_selector('.badge', count: 1) expect(card).not_to have_content(stretch.title) end diff --git a/spec/features/boards/sub_group_project_spec.rb b/spec/features/boards/sub_group_project_spec.rb index 5fdb8044db2..271c610dcc8 100644 --- a/spec/features/boards/sub_group_project_spec.rb +++ b/spec/features/boards/sub_group_project_spec.rb @@ -20,7 +20,7 @@ describe 'Sub-group project issue boards', :js do end it 'creates new label from sidebar' do - find('.card').click + find('.board-card').click page.within '.labels' do click_link 'Edit' diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb index 94133c62b5c..5ed20b02a6e 100644 --- a/spec/features/dashboard/todos/todos_spec.rb +++ b/spec/features/dashboard/todos/todos_spec.rb @@ -246,7 +246,7 @@ feature 'Dashboard Todos' do it 'is has the right number of pages' do visit dashboard_todos_path - expect(page).to have_selector('.gl-pagination .page', count: 2) + expect(page).to have_selector('.gl-pagination .js-pagination-page', count: 2) end describe 'mark all as done', :js do diff --git a/spec/features/explore/new_menu_spec.rb b/spec/features/explore/new_menu_spec.rb index 8d5233d0c0f..2516db5422f 100644 --- a/spec/features/explore/new_menu_spec.rb +++ b/spec/features/explore/new_menu_spec.rb @@ -66,7 +66,7 @@ feature 'Top Plus Menu', :js do page.within '.header-content' do find('.header-new-dropdown-toggle').click - expect(page).to have_selector('.header-new.dropdown.open', count: 1) + expect(page).to have_selector('.header-new.dropdown.show', count: 1) find('.header-new-project-snippet a').click end @@ -88,7 +88,7 @@ feature 'Top Plus Menu', :js do page.within '.header-content' do find('.header-new-dropdown-toggle').click - expect(page).to have_selector('.header-new.dropdown.open', count: 1) + expect(page).to have_selector('.header-new.dropdown.show', count: 1) find('.header-new-group-project a').click end @@ -156,7 +156,7 @@ feature 'Top Plus Menu', :js do def click_topmenuitem(item_name) page.within '.header-content' do find('.header-new-dropdown-toggle').click - expect(page).to have_selector('.header-new.dropdown.open', count: 1) + expect(page).to have_selector('.header-new.dropdown.show', count: 1) click_link item_name end end diff --git a/spec/features/groups/members/search_members_spec.rb b/spec/features/groups/members/search_members_spec.rb index 31fbbcf562c..e7efdf7dfef 100644 --- a/spec/features/groups/members/search_members_spec.rb +++ b/spec/features/groups/members/search_members_spec.rb @@ -22,7 +22,7 @@ describe 'Search group member' do find('.member-search-btn').click end - group_members_list = find(".panel .content-list") + group_members_list = find(".card .content-list") expect(group_members_list).to have_content(member.name) expect(group_members_list).not_to have_content(user.name) end diff --git a/spec/features/groups/settings/group_badges_spec.rb b/spec/features/groups/settings/group_badges_spec.rb index 92217294446..a99da4a119b 100644 --- a/spec/features/groups/settings/group_badges_spec.rb +++ b/spec/features/groups/settings/group_badges_spec.rb @@ -21,7 +21,7 @@ feature 'Group Badges' do page.within '.badge-settings' do wait_for_requests - rows = all('.panel-body > div') + rows = all('.card-body > div') expect(rows.length).to eq 2 expect(rows[0]).to have_content badge_1.link_url expect(rows[1]).to have_content badge_2.link_url @@ -48,7 +48,7 @@ feature 'Group Badges' do click_button 'Add badge' wait_for_requests - within '.panel-body' do + within '.card-body' do expect(find('a')[:href]).to eq badge_link_url expect(find('a img')[:src]).to eq badge_image_url end @@ -60,7 +60,7 @@ feature 'Group Badges' do it 'form is shown when clicking edit button in list' do page.within '.badge-settings' do wait_for_requests - rows = all('.panel-body > div') + rows = all('.card-body > div') expect(rows.length).to eq 2 rows[1].find('[aria-label="Edit"]').click @@ -74,7 +74,7 @@ feature 'Group Badges' do it 'updates a badge when submitting the edit form' do page.within '.badge-settings' do wait_for_requests - rows = all('.panel-body > div') + rows = all('.card-body > div') expect(rows.length).to eq 2 rows[1].find('[aria-label="Edit"]').click within 'form' do @@ -85,7 +85,7 @@ feature 'Group Badges' do wait_for_requests end - rows = all('.panel-body > div') + rows = all('.card-body > div') expect(rows.length).to eq 2 expect(rows[1]).to have_content badge_link_url end @@ -99,7 +99,7 @@ feature 'Group Badges' do it 'shows a modal when deleting a badge' do wait_for_requests - rows = all('.panel-body > div') + rows = all('.card-body > div') expect(rows.length).to eq 2 click_delete_button(rows[1]) @@ -109,14 +109,14 @@ feature 'Group Badges' do it 'deletes a badge when confirming the modal' do wait_for_requests - rows = all('.panel-body > div') + rows = all('.card-body > div') expect(rows.length).to eq 2 click_delete_button(rows[1]) find('.modal .btn-danger').click wait_for_requests - rows = all('.panel-body > div') + rows = all('.card-body > div') expect(rows.length).to eq 1 expect(rows[0]).to have_content badge_1.link_url end diff --git a/spec/features/groups/user_browse_projects_group_page_spec.rb b/spec/features/groups/user_browse_projects_group_page_spec.rb index e81c3180e78..916363c41dd 100644 --- a/spec/features/groups/user_browse_projects_group_page_spec.rb +++ b/spec/features/groups/user_browse_projects_group_page_spec.rb @@ -21,7 +21,7 @@ describe 'User browse group projects page' do visit projects_group_path(group) expect(page).to have_link project.name - expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived') + expect(page).to have_xpath("//span[@class='badge badge-warning']", text: 'archived') end end end diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb index e4be6193b8b..a986ddc4abc 100644 --- a/spec/features/invites_spec.rb +++ b/spec/features/invites_spec.rb @@ -5,18 +5,41 @@ describe 'Invites' do let(:owner) { create(:user, name: 'John Doe') } let(:group) { create(:group, name: 'Owned') } let(:project) { create(:project, :repository, namespace: group) } - let(:invite) { group.group_members.invite.last } + let(:group_invite) { group.group_members.invite.last } before do project.add_master(owner) group.add_user(owner, Gitlab::Access::OWNER) group.add_developer('user@example.com', owner) - invite.generate_invite_token! + group_invite.generate_invite_token! + end + + def confirm_email_and_sign_in(new_user) + new_user_token = User.find_by_email(new_user.email).confirmation_token + + visit user_confirmation_path(confirmation_token: new_user_token) + fill_in_sign_in_form(new_user) + end + + def fill_in_sign_up_form(new_user) + fill_in 'new_user_name', with: new_user.name + fill_in 'new_user_username', with: new_user.username + fill_in 'new_user_email', with: new_user.email + fill_in 'new_user_email_confirmation', with: new_user.email + fill_in 'new_user_password', with: new_user.password + click_button "Register" + end + + def fill_in_sign_in_form(user) + fill_in 'user_login', with: user.email + fill_in 'user_password', with: user.password + check 'user_remember_me' + click_button 'Sign in' end context 'when signed out' do before do - visit invite_path(invite.raw_invite_token) + visit invite_path(group_invite.raw_invite_token) end it 'renders sign in page with sign in notice' do @@ -25,12 +48,9 @@ describe 'Invites' do end it 'sign in and redirects to invitation page' do - fill_in 'user_login', with: user.email - fill_in 'user_password', with: user.password - check 'user_remember_me' - click_button 'Sign in' + fill_in_sign_in_form(user) - expect(current_path).to eq(invite_path(invite.raw_invite_token)) + expect(current_path).to eq(invite_path(group_invite.raw_invite_token)) expect(page).to have_content( 'You have been invited by John Doe to join group Owned as Developer.' ) @@ -45,7 +65,7 @@ describe 'Invites' do end it 'shows message user already a member' do - visit invite_path(invite.raw_invite_token) + visit invite_path(group_invite.raw_invite_token) expect(page).to have_content('However, you are already a member of this group.') end end @@ -53,7 +73,7 @@ describe 'Invites' do describe 'accepting the invitation' do before do sign_in(user) - visit invite_path(invite.raw_invite_token) + visit invite_path(group_invite.raw_invite_token) end it 'grants access and redirects to group page' do @@ -69,7 +89,7 @@ describe 'Invites' do context 'when signed in' do before do sign_in(user) - visit invite_path(invite.raw_invite_token) + visit invite_path(group_invite.raw_invite_token) end it 'declines application and redirects to dashboard' do @@ -83,7 +103,7 @@ describe 'Invites' do context 'when signed out' do before do - visit decline_invite_path(invite.raw_invite_token) + visit decline_invite_path(group_invite.raw_invite_token) end it 'declines application and redirects to sign in page' do @@ -94,4 +114,72 @@ describe 'Invites' do end end end + + describe 'invite an user using their email address' do + let(:new_user) { build_stubbed(:user) } + let(:invite_email) { new_user.email } + let(:group_invite) { create(:group_member, :invited, group: group, invite_email: invite_email) } + let!(:project_invite) { create(:project_member, :invited, project: project, invite_email: invite_email) } + + before do + stub_application_setting(send_user_confirmation_email: send_email_confirmation) + visit invite_path(group_invite.raw_invite_token) + end + + context 'email confirmation disabled' do + let(:send_email_confirmation) { false } + + it 'signs up and redirects to the dashboard page with all the projects/groups invitations automatically accepted' do + fill_in_sign_up_form(new_user) + + expect(current_path).to eq(dashboard_projects_path) + expect(page).to have_content(project.full_name) + visit group_path(group) + expect(page).to have_content(group.full_name) + end + + context 'the user sign-up using a different email address' do + let(:invite_email) { build_stubbed(:user).email } + + it 'signs up and redirects to the invitation page' do + fill_in_sign_up_form(new_user) + + expect(current_path).to eq(invite_path(group_invite.raw_invite_token)) + end + end + end + + context 'email confirmation enabled' do + let(:send_email_confirmation) { true } + + it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do + fill_in_sign_up_form(new_user) + confirm_email_and_sign_in(new_user) + + expect(current_path).to eq(root_path) + expect(page).to have_content(project.full_name) + visit group_path(group) + expect(page).to have_content(group.full_name) + end + + it "doesn't accept invitations until the user confirm his email" do + fill_in_sign_up_form(new_user) + sign_in(owner) + + visit project_project_members_path(project) + expect(page).to have_content 'Invited' + end + + context 'the user sign-up using a different email address' do + let(:invite_email) { build_stubbed(:user).email } + + it 'signs up and redirects to the invitation page' do + fill_in_sign_up_form(new_user) + confirm_email_and_sign_in(new_user) + + expect(current_path).to eq(invite_path(group_invite.raw_invite_token)) + end + end + end + end end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 08ba91a2682..483122ae463 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -248,7 +248,7 @@ describe 'Filter issues', :js do context 'issue label clicked' do it 'filters and displays in search bar' do - find('.issues-list .issue .issue-main-info .issuable-info a .label', text: multiple_words_label.title).click + find('.issues-list .issue .issue-main-info .issuable-info a .badge', text: multiple_words_label.title).click expect_issues_list_count(1) expect_tokens([label_token("\"#{multiple_words_label.title}\"")]) diff --git a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb index 539d7e9ff01..3dfcbc2fcb8 100644 --- a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb +++ b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb @@ -139,8 +139,8 @@ describe 'User creates branch and merge request on issue page', :js do end it 'disables the create branch button' do - expect(page).to have_css('.create-mr-dropdown-wrap .unavailable:not(.hide)') - expect(page).to have_css('.create-mr-dropdown-wrap .available.hide', visible: false) + expect(page).to have_css('.create-mr-dropdown-wrap .unavailable:not(.hidden)') + expect(page).to have_css('.create-mr-dropdown-wrap .available.hidden', visible: false) expect(page).to have_content /1 Related Merge Request/ end end diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb index ae41f611ddc..4700ada1aae 100644 --- a/spec/features/labels_hierarchy_spec.rb +++ b/spec/features/labels_hierarchy_spec.rb @@ -34,7 +34,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do wait_for_requests - expect(page).to have_selector('span.label', text: label.title) + expect(page).to have_selector('span.badge', text: label.title) end end @@ -45,7 +45,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do wait_for_requests - expect(page).not_to have_selector('span.label', text: child_group_label.title) + expect(page).not_to have_selector('span.badge', text: child_group_label.title) end end @@ -57,7 +57,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do wait_for_requests if board - expect(page).to have_selector('.card-title') do |card| + expect(page).to have_selector('.board-card-title') do |card| expect(card).to have_selector('a', text: labeled_issue.title) end else @@ -96,11 +96,11 @@ feature 'Labels Hierarchy', :js, :nested_groups do wait_for_requests if board - expect(page).to have_selector('.card-title') do |card| + expect(page).to have_selector('.board-card-title') do |card| expect(card).to have_selector('a', text: labeled_issue.title) end - expect(page).to have_selector('.card-title') do |card| + expect(page).to have_selector('.board-card-title') do |card| expect(card).to have_selector('a', text: labeled_issue_2.title) end else @@ -118,11 +118,11 @@ feature 'Labels Hierarchy', :js, :nested_groups do select_label_on_dropdown(group_label_3.title) if board - expect(page).to have_selector('.card-title') do |card| + expect(page).to have_selector('.board-card-title') do |card| expect(card).not_to have_selector('a', text: labeled_issue_2.title) end - expect(page).to have_selector('.card-title') do |card| + expect(page).to have_selector('.board-card-title') do |card| expect(card).to have_selector('a', text: labeled_issue_3.title) end else @@ -159,9 +159,9 @@ feature 'Labels Hierarchy', :js, :nested_groups do find('.btn-create').click expect(page.find('.issue-details h2.title')).to have_content('new created issue') - expect(page).to have_selector('span.label', text: grandparent_group_label.title) - expect(page).to have_selector('span.label', text: parent_group_label.title) - expect(page).to have_selector('span.label', text: project_label_1.title) + expect(page).to have_selector('span.badge', text: grandparent_group_label.title) + expect(page).to have_selector('span.badge', text: parent_group_label.title) + expect(page).to have_selector('span.badge', text: project_label_1.title) end end @@ -188,7 +188,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do wait_for_requests - find('.card').click + find('.board-card').click end it_behaves_like 'assigning labels from sidebar' @@ -204,7 +204,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do wait_for_requests - find('.card').click + find('.board-card').click end it_behaves_like 'assigning labels from sidebar' diff --git a/spec/features/merge_request/user_resolves_conflicts_spec.rb b/spec/features/merge_request/user_resolves_conflicts_spec.rb index 19995559fae..59aa90fc86f 100644 --- a/spec/features/merge_request/user_resolves_conflicts_spec.rb +++ b/spec/features/merge_request/user_resolves_conflicts_spec.rb @@ -10,7 +10,7 @@ describe 'Merge request > User resolves conflicts', :js do end def create_merge_request(source_branch) - create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start', source_project: project) do |mr| + create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start', source_project: project, merge_status: :unchecked) do |mr| mr.mark_as_unmergeable end end @@ -27,7 +27,7 @@ describe 'Merge request > User resolves conflicts', :js do end end - find_button('Commit conflict resolution').send_keys(:return) + find_button('Commit to source branch').send_keys(:return) expect(page).to have_content('All merge conflicts were resolved') merge_request.reload_diff @@ -71,7 +71,7 @@ describe 'Merge request > User resolves conflicts', :js do execute_script('ace.edit($(".files-wrapper .diff-file pre")[1]).setValue("Gregor Samsa woke from troubled dreams");') end - find_button('Commit conflict resolution').send_keys(:return) + find_button('Commit to source branch').send_keys(:return) expect(page).to have_content('All merge conflicts were resolved') merge_request.reload_diff @@ -145,7 +145,7 @@ describe 'Merge request > User resolves conflicts', :js do execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("Gregor Samsa woke from troubled dreams");') end - click_button 'Commit conflict resolution' + click_button 'Commit to source branch' expect(page).to have_content('All merge conflicts were resolved') diff --git a/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb new file mode 100644 index 00000000000..c40c720d168 --- /dev/null +++ b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +describe 'Merge request > User sees Check out branch modal', :js do + let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + let(:merge_request) { create(:merge_request, source_project: project) } + + before do + sign_in(user) + visit project_merge_request_path(project, merge_request) + wait_for_requests + click_button('Check out branch') + end + + it 'shows the check out branch modal' do + expect(page).to have_content('Check out, review, and merge locally') + end + + it 'closes the check out branch model with Escape keypress' do + find('#modal_merge_info').send_keys(:escape) + + expect(page).not_to have_content('Check out, review, and merge locally') + end +end diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb index 42c279af117..ed6e29335d1 100644 --- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb +++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb @@ -4,6 +4,12 @@ describe 'Merge request > User selects branches for new MR', :js do let(:project) { create(:project, :public, :repository) } let(:user) { project.creator } + def select_source_branch(branch_name) + find('.js-source-branch', match: :first).click + find('.js-source-branch-dropdown .dropdown-input-field').native.send_keys branch_name + find('.js-source-branch-dropdown .dropdown-content a', text: branch_name, match: :first).click + end + before do project.add_master(user) sign_in(user) @@ -43,8 +49,7 @@ describe 'Merge request > User selects branches for new MR', :js do it 'generates a diff for an orphaned branch' do visit project_new_merge_request_path(project) - find('.js-source-branch', match: :first).click - find('.js-source-branch-dropdown .dropdown-content a', text: 'orphaned-branch', match: :first).click + select_source_branch('orphaned-branch') click_button "Compare branches" click_link "Changes" @@ -169,4 +174,31 @@ describe 'Merge request > User selects branches for new MR', :js do end end end + + context 'with special characters in branch names' do + it 'escapes quotes in branch names' do + special_branch_name = '"with-quotes"' + CreateBranchService.new(project, user) + .execute(special_branch_name, 'add-pdf-file') + + visit project_new_merge_request_path(project) + select_source_branch(special_branch_name) + + source_branch_input = find('[name="merge_request[source_branch]"]', visible: false) + expect(source_branch_input.value).to eq special_branch_name + end + + it 'does not escape unicode in branch names' do + special_branch_name = 'ʕ•ᴥ•ʔ' + CreateBranchService.new(project, user) + .execute(special_branch_name, 'add-pdf-file') + + visit project_new_merge_request_path(project) + select_source_branch(special_branch_name) + + click_button "Compare branches" + + expect(page).to have_button("Submit merge request") + end + end end diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb index 6c51e4bbe26..b0db6870ddf 100644 --- a/spec/features/milestone_spec.rb +++ b/spec/features/milestone_spec.rb @@ -119,7 +119,7 @@ feature 'Milestone' do find('.milestone-deprecation-message .js-popover-link').click - expect(page).to have_selector('.milestone-deprecation-message .popover') + expect(page).to have_selector('.popover') end end end diff --git a/spec/features/projects/actve_tabs_spec.rb b/spec/features/projects/actve_tabs_spec.rb index 0bda68b83e7..ce5606b63ae 100644 --- a/spec/features/projects/actve_tabs_spec.rb +++ b/spec/features/projects/actve_tabs_spec.rb @@ -35,7 +35,7 @@ describe 'Project active tab' do visit project_path(project) end - it_behaves_like 'page has active tab', 'Overview' + it_behaves_like 'page has active tab', 'Project' it_behaves_like 'page has active sub tab', 'Details' context 'on project Home/Activity' do @@ -43,7 +43,7 @@ describe 'Project active tab' do click_tab('Activity') end - it_behaves_like 'page has active tab', 'Overview' + it_behaves_like 'page has active tab', 'Project' it_behaves_like 'page has active sub tab', 'Activity' end end diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index ac82f869f0f..e7b305925f7 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -14,6 +14,8 @@ feature 'File blob', :js do context 'Ruby file' do before do visit_blob('files/ruby/popen.rb') + + wait_for_requests end it 'displays the blob' do @@ -48,6 +50,8 @@ feature 'File blob', :js do context 'visiting directly' do before do visit_blob('files/markdown/ruby-style-guide.md') + + wait_for_requests end it 'displays the blob using the rich viewer' do @@ -159,6 +163,8 @@ feature 'File blob', :js do project.update_attribute(:lfs_enabled, true) visit_blob('files/lfs/file.md') + + wait_for_requests end it 'displays an error' do @@ -207,6 +213,8 @@ feature 'File blob', :js do context 'when LFS is disabled on the project' do before do visit_blob('files/lfs/file.md') + + wait_for_requests end it 'displays the blob' do @@ -242,6 +250,8 @@ feature 'File blob', :js do ).execute visit_blob('files/test.pdf') + + wait_for_requests end it 'displays the blob' do @@ -268,6 +278,8 @@ feature 'File blob', :js do project.update_attribute(:lfs_enabled, true) visit_blob('files/lfs/lfs_object.iso') + + wait_for_requests end it 'displays the blob' do @@ -290,6 +302,8 @@ feature 'File blob', :js do context 'when LFS is disabled on the project' do before do visit_blob('files/lfs/lfs_object.iso') + + wait_for_requests end it 'displays the blob' do @@ -313,6 +327,8 @@ feature 'File blob', :js do context 'ZIP file' do before do visit_blob('Gemfile.zip') + + wait_for_requests end it 'displays the blob' do @@ -347,6 +363,8 @@ feature 'File blob', :js do ).execute visit_blob('files/empty.md') + + wait_for_requests end it 'displays an error' do diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 90760c6e814..5071a87fa5b 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -144,6 +144,7 @@ feature 'Gcp Cluster', :js do it 'user sees a login page' do expect(page).to have_css('.signin-with-google') + expect(page).to have_link('Google account') end end diff --git a/spec/features/projects/commit/comments/user_adds_comment_spec.rb b/spec/features/projects/commit/comments/user_adds_comment_spec.rb new file mode 100644 index 00000000000..6397df086a7 --- /dev/null +++ b/spec/features/projects/commit/comments/user_adds_comment_spec.rb @@ -0,0 +1,170 @@ +require "spec_helper" + +describe "User adds a comment on a commit", :js do + include Spec::Support::Helpers::Features::NotesHelpers + include RepoHelpers + + let(:comment_text) { "XML attached" } + let(:another_comment_text) { "SVG attached" } + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + before do + sign_in(user) + project.add_developer(user) + end + + context "inline view" do + before do + visit(project_commit_path(project, sample_commit.id)) + end + + it "adds a comment" do + page.within(".js-main-target-form") do + expect(page).not_to have_link("Cancel") + + emoji = ":+1:" + + fill_in("note[note]", with: "#{comment_text} #{emoji}") + + # Check on `Preview` tab + click_link("Preview") + + expect(find(".js-md-preview")).to have_content(comment_text).and have_css("gl-emoji") + expect(page).not_to have_css(".js-note-text") + + # Check on the `Write` tab + click_link("Write") + + expect(page).to have_field("note[note]", with: "#{comment_text} #{emoji}") + + # Submit comment from the `Preview` tab to get rid of a separate `it` block + # which would specially tests if everything gets cleared from the note form. + click_link("Preview") + click_button("Comment") + end + + wait_for_requests + + page.within(".note") do + expect(page).to have_content(comment_text).and have_css("gl-emoji") + end + + page.within(".js-main-target-form") do + expect(page).to have_field("note[note]", with: "").and have_no_css(".js-md-preview") + end + end + + context "when commenting on diff" do + it "adds a comment" do + page.within(".diff-file:nth-of-type(1)") do + # Open a form for a comment and check UI elements are visible and acting as expecting. + click_diff_line(sample_commit.line_code) + + expect(page).to have_css(".js-temp-notes-holder form.new-note") + .and have_css(".js-close-discussion-note-form", text: "Cancel") + + # The `Cancel` button closes the current form. The page should not have any open forms after that. + find(".js-close-discussion-note-form").click + + expect(page).not_to have_css("form.new_note") + + # Try to open the same form twice. There should be only one form opened. + click_diff_line(sample_commit.line_code) + click_diff_line(sample_commit.line_code) + + expect(page).to have_css("form.new-note", count: 1) + + # Fill in a form. + page.within("form[data-line-code='#{sample_commit.line_code}']") do + fill_in("note[note]", with: "#{comment_text} :smile:") + end + + # Open another form and check we have two forms now (because the first one is filled in). + click_diff_line(sample_commit.del_line_code) + + expect(page).to have_field("note[note]", with: "#{comment_text} :smile:") + .and have_field("note[note]", with: "") + + # Test Preview feature for both forms. + page.within("form[data-line-code='#{sample_commit.line_code}']") do + click_link("Preview") + end + + page.within("form[data-line-code='#{sample_commit.del_line_code}']") do + fill_in("note[note]", with: another_comment_text) + + click_link("Preview") + end + + expect(page).to have_css(".js-md-preview", visible: true, count: 2) + .and have_content(comment_text) + .and have_content(another_comment_text) + .and have_xpath("//gl-emoji[@data-name='smile']") + + # Test UI elements, then submit. + page.within("form[data-line-code='#{sample_commit.line_code}']") do + expect(find(".js-note-text", visible: false).text).to eq("") + expect(page).to have_css('.js-md-write-button') + + click_button("Comment") + end + + expect(page).to have_button("Reply...").and have_no_css("form.new_note") + end + + # A comment should be added and visible. + page.within(".diff-file:nth-of-type(1) .note") do + expect(page).to have_content(comment_text).and have_xpath("//gl-emoji[@data-name='smile']") + end + end + end + end + + context "side-by-side view" do + before do + visit(project_commit_path(project, sample_commit.id, view: "parallel")) + end + + it "adds a comment" do + new_comment = "New comment" + old_comment = "Old comment" + + # Left side. + click_parallel_diff_line(sample_commit.del_line_code) + + page.within(".diff-file:nth-of-type(1) form[data-line-code='#{sample_commit.del_line_code}']") do + fill_in("note[note]", with: old_comment) + click_button("Comment") + end + + page.within(".diff-file:nth-of-type(1) .notes_content.parallel.old") do + expect(page).to have_content(old_comment) + end + + # Right side. + click_parallel_diff_line(sample_commit.line_code) + + page.within(".diff-file:nth-of-type(1) form[data-line-code='#{sample_commit.line_code}']") do + fill_in("note[note]", with: new_comment) + click_button("Comment") + end + + wait_for_requests + + expect(all(".diff-file:nth-of-type(1) .notes_content.parallel.new")[1].text).to have_content(new_comment) + end + end + + private + + def click_diff_line(line) + find(".line_holder[id='#{line}'] td:nth-of-type(1)").hover + find(".line_holder[id='#{line}'] button").click + end + + def click_parallel_diff_line(line) + find(".line_holder.parallel td[id='#{line}']").find(:xpath, 'preceding-sibling::*[1][self::td]').hover + find(".line_holder.parallel button[data-line-code='#{line}']").click + end +end diff --git a/spec/features/projects/commit/comments/user_deletes_comments_spec.rb b/spec/features/projects/commit/comments/user_deletes_comments_spec.rb new file mode 100644 index 00000000000..a727cab4ac7 --- /dev/null +++ b/spec/features/projects/commit/comments/user_deletes_comments_spec.rb @@ -0,0 +1,37 @@ +require "spec_helper" + +describe "User deletes comments on a commit", :js do + include Spec::Support::Helpers::Features::NotesHelpers + include RepoHelpers + + let(:comment_text) { "XML attached" } + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + before do + sign_in(user) + project.add_developer(user) + + visit(project_commit_path(project, sample_commit.id)) + + add_note(comment_text) + end + + it "deletes comment" do + page.within(".note") do + expect(page).to have_content(comment_text) + end + + page.within(".main-notes-list") do + note = find(".note") + note.hover + + find(".more-actions").click + find(".more-actions .dropdown-menu li", match: :first) + + accept_confirm { find(".js-note-delete").click } + end + + expect(page).not_to have_css(".note") + end +end diff --git a/spec/features/projects/commit/comments/user_edits_comments_spec.rb b/spec/features/projects/commit/comments/user_edits_comments_spec.rb new file mode 100644 index 00000000000..75bccd99f59 --- /dev/null +++ b/spec/features/projects/commit/comments/user_edits_comments_spec.rb @@ -0,0 +1,42 @@ +require "spec_helper" + +describe "User edits a comment on a commit", :js do + include Spec::Support::Helpers::Features::NotesHelpers + include RepoHelpers + + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + before do + sign_in(user) + project.add_developer(user) + + visit(project_commit_path(project, sample_commit.id)) + + add_note("XML attached") + end + + it "edits comment" do + NEW_COMMENT_TEXT = "+1 Awesome!".freeze + + page.within(".main-notes-list") do + note = find(".note") + note.hover + + note.find(".js-note-edit").click + end + + page.find(".current-note-edit-form textarea") + + page.within(".current-note-edit-form") do + fill_in("note[note]", with: NEW_COMMENT_TEXT) + click_button("Save comment") + end + + wait_for_requests + + page.within(".note") do + expect(page).to have_content(NEW_COMMENT_TEXT) + end + end +end diff --git a/spec/features/projects/commit/user_comments_on_commit_spec.rb b/spec/features/projects/commit/user_comments_on_commit_spec.rb deleted file mode 100644 index 5174f793367..00000000000 --- a/spec/features/projects/commit/user_comments_on_commit_spec.rb +++ /dev/null @@ -1,110 +0,0 @@ -require "spec_helper" - -describe "User comments on commit", :js do - include Spec::Support::Helpers::Features::NotesHelpers - include RepoHelpers - - let(:project) { create(:project, :repository) } - let(:user) { create(:user) } - - COMMENT_TEXT = "XML attached".freeze - - before do - sign_in(user) - project.add_developer(user) - - visit(project_commit_path(project, sample_commit.id)) - end - - context "when adding new comment" do - it "adds comment" do - EMOJI = ":+1:".freeze - - page.within(".js-main-target-form") do - expect(page).not_to have_link("Cancel") - - fill_in("note[note]", with: "#{COMMENT_TEXT} #{EMOJI}") - - # Check on `Preview` tab - click_link("Preview") - - expect(find(".js-md-preview")).to have_content(COMMENT_TEXT).and have_css("gl-emoji") - expect(page).not_to have_css(".js-note-text") - - # Check on `Write` tab - click_link("Write") - - expect(page).to have_field("note[note]", with: "#{COMMENT_TEXT} #{EMOJI}") - - # Submit comment from the `Preview` tab to get rid of a separate `it` block - # which would specially tests if everything gets cleared from the note form. - click_link("Preview") - click_button("Comment") - end - - wait_for_requests - - page.within(".note") do - expect(page).to have_content(COMMENT_TEXT).and have_css("gl-emoji") - end - - page.within(".js-main-target-form") do - expect(page).to have_field("note[note]", with: "").and have_no_css(".js-md-preview") - end - end - end - - context "when editing comment" do - before do - add_note(COMMENT_TEXT) - end - - it "edits comment" do - NEW_COMMENT_TEXT = "+1 Awesome!".freeze - - page.within(".main-notes-list") do - note = find(".note") - note.hover - - note.find(".js-note-edit").click - end - - page.find(".current-note-edit-form textarea") - - page.within(".current-note-edit-form") do - fill_in("note[note]", with: NEW_COMMENT_TEXT) - click_button("Save comment") - end - - wait_for_requests - - page.within(".note") do - expect(page).to have_content(NEW_COMMENT_TEXT) - end - end - end - - context "when deleting comment" do - before do - add_note(COMMENT_TEXT) - end - - it "deletes comment" do - page.within(".note") do - expect(page).to have_content(COMMENT_TEXT) - end - - page.within(".main-notes-list") do - note = find(".note") - note.hover - - find(".more-actions").click - find(".more-actions .dropdown-menu li", match: :first) - - accept_confirm { find(".js-note-delete").click } - end - - expect(page).not_to have_css(".note") - end - end -end diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 5248a783db4..f9defa22d35 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -42,6 +42,22 @@ feature 'Environments page', :js do expect(page).to have_content('You don\'t have any environments right now') end end + + context 'when cluster is not reachable' do + let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } + let!(:application_prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) } + + before do + allow_any_instance_of(Kubeclient::Client).to receive(:proxy_url).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil)) + end + + it 'should show one environment without error' do + visit_environments(project, scope: 'available') + + expect(page).to have_css('.environments-container') + expect(page.all('.environment-name').length).to eq(1) + end + end end describe 'with one stopped environment' do diff --git a/spec/features/projects/issues/user_sorts_issues_spec.rb b/spec/features/projects/issues/user_sorts_issues_spec.rb index c3d63000dac..db5936a30cb 100644 --- a/spec/features/projects/issues/user_sorts_issues_spec.rb +++ b/spec/features/projects/issues/user_sorts_issues_spec.rb @@ -18,7 +18,7 @@ describe "User sorts issues" do it "sorts by popularity" do find("button.dropdown-toggle").click - page.within(".content ul.dropdown-menu.dropdown-menu-align-right li") do + page.within(".content ul.dropdown-menu.dropdown-menu-right li") do click_link("Popularity") end diff --git a/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb b/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb index f285c6c8783..1f21ef7b382 100644 --- a/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb +++ b/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb @@ -1,32 +1,84 @@ -require 'spec_helper' +require "spec_helper" -describe 'User creates a merge request', :js do +describe "User creates a merge request", :js do + include ProjectForksHelper + + let(:title) { "Some feature" } let(:project) { create(:project, :repository) } let(:user) { create(:user) } before do project.add_master(user) sign_in(user) + end + it "creates a merge request" do visit(project_new_merge_request_path(project)) - end - it 'creates a merge request' do - find('.js-source-branch').click - click_link('fix') + find(".js-source-branch").click + click_link("fix") - find('.js-target-branch').click - click_link('feature') + find(".js-target-branch").click + click_link("feature") - click_button('Compare branches') + click_button("Compare branches") - fill_in('merge_request_title', with: 'Wiki Feature') - click_button('Submit merge request') + fill_in("Title", with: title) + click_button("Submit merge request") - page.within('.merge-request') do - expect(page).to have_content('Wiki Feature') + page.within(".merge-request") do + expect(page).to have_content(title) end + end + + context "to a forked project" do + let(:forked_project) { fork_project(project, user, namespace: user.namespace, repository: true) } + + it "creates a merge request" do + visit(project_new_merge_request_path(forked_project)) + + expect(page).to have_content("Source branch").and have_content("Target branch") + expect(find("#merge_request_target_project_id", visible: false).value).to eq(project.id.to_s) + + click_button("Compare branches and continue") + + expect(page).to have_content("You must select source and target branch") + + first(".js-source-project").click + first(".dropdown-source-project a", text: forked_project.full_path) + + first(".js-target-project").click + first(".dropdown-target-project a", text: project.full_path) + + first(".js-source-branch").click - wait_for_requests + wait_for_requests + + source_branch = "fix" + + first(".js-source-branch-dropdown .dropdown-content a", text: source_branch).click + + click_button("Compare branches and continue") + + expect(page).to have_css("h3.page-title", text: "New Merge Request") + + page.within("form#new_merge_request") do + fill_in("Title", with: title) + end + + click_button("Assignee") + + expect(find(".js-assignee-search")["data-project-id"]).to eq(project.id.to_s) + + page.within(".dropdown-menu-user") do + expect(page).to have_content("Unassigned") + .and have_content(user.name) + .and have_content(project.users.first.name) + end + + click_button("Submit merge request") + + expect(page).to have_content(title).and have_content("Request to merge #{user.namespace.name}:#{source_branch} into master") + end end end diff --git a/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb index d8d9f7e2a8c..305658f1b5d 100644 --- a/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb +++ b/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb @@ -18,7 +18,7 @@ describe 'User sorts merge requests' do it 'keeps the sort option' do find('button.dropdown-toggle').click - page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do + page.within('.content ul.dropdown-menu.dropdown-menu-right li') do click_link('Last updated') end @@ -43,7 +43,7 @@ describe 'User sorts merge requests' do it 'sorts by popularity' do find('button.dropdown-toggle').click - page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do + page.within('.content ul.dropdown-menu.dropdown-menu-right li') do click_link('Popularity') end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index a29c21f6fef..af2a9567a47 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -165,7 +165,7 @@ describe 'Pipeline', :js do end it 'shows Pipeline tab as active' do - expect(page).to have_css('.js-pipeline-tab-link.active') + expect(page).to have_css('.js-pipeline-tab-link .active') end context 'without permission to access builds' do @@ -271,7 +271,7 @@ describe 'Pipeline', :js do end it 'shows Jobs tab as active' do - expect(page).to have_css('li.js-builds-tab-link.active') + expect(page).to have_css('li.js-builds-tab-link .active') end end diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb index e875a88a52b..cfdae246c09 100644 --- a/spec/features/projects/settings/pipelines_settings_spec.rb +++ b/spec/features/projects/settings/pipelines_settings_spec.rb @@ -75,6 +75,29 @@ describe "Projects > Settings > Pipelines settings" do expect(project.auto_devops).not_to be_enabled expect(project.auto_devops.domain).to eq('test.com') end + + context 'when there is a cluster with ingress and external_ip' do + before do + cluster = create(:cluster, projects: [project]) + cluster.create_application_ingress!(external_ip: '192.168.1.100') + end + + it 'shows the help text with the nip.io domain as an alternative to custom domain' do + visit project_settings_ci_cd_path(project) + expect(page).to have_content('192.168.1.100.nip.io can be used as an alternative to a custom domain') + end + end + + context 'when there is no ingress' do + before do + create(:cluster, projects: [project]) + end + + it 'alternative to custom domain is not shown' do + visit project_settings_ci_cd_path(project) + expect(page).not_to have_content('can be used as an alternative to a custom domain') + end + end end end end diff --git a/spec/features/projects/settings/project_badges_spec.rb b/spec/features/projects/settings/project_badges_spec.rb index cc3551a4c21..4893bef8884 100644 --- a/spec/features/projects/settings/project_badges_spec.rb +++ b/spec/features/projects/settings/project_badges_spec.rb @@ -22,7 +22,7 @@ feature 'Project Badges' do page.within '.badge-settings' do wait_for_requests - rows = all('.panel-body > div') + rows = all('.card-body > div') expect(rows.length).to eq 2 expect(rows[0]).to have_content group_badge.link_url expect(rows[1]).to have_content project_badge.link_url @@ -49,7 +49,7 @@ feature 'Project Badges' do click_button 'Add badge' wait_for_requests - within '.panel-body' do + within '.card-body' do expect(find('a')[:href]).to eq badge_link_url expect(find('a img')[:src]).to eq badge_image_url end @@ -61,7 +61,7 @@ feature 'Project Badges' do it 'form is shown when clicking edit button in list' do page.within '.badge-settings' do wait_for_requests - rows = all('.panel-body > div') + rows = all('.card-body > div') expect(rows.length).to eq 2 rows[1].find('[aria-label="Edit"]').click @@ -75,7 +75,7 @@ feature 'Project Badges' do it 'updates a badge when submitting the edit form' do page.within '.badge-settings' do wait_for_requests - rows = all('.panel-body > div') + rows = all('.card-body > div') expect(rows.length).to eq 2 rows[1].find('[aria-label="Edit"]').click within 'form' do @@ -86,7 +86,7 @@ feature 'Project Badges' do wait_for_requests end - rows = all('.panel-body > div') + rows = all('.card-body > div') expect(rows.length).to eq 2 expect(rows[1]).to have_content badge_link_url end @@ -100,7 +100,7 @@ feature 'Project Badges' do it 'shows a modal when deleting a badge' do wait_for_requests - rows = all('.panel-body > div') + rows = all('.card-body > div') expect(rows.length).to eq 2 click_delete_button(rows[1]) @@ -110,14 +110,14 @@ feature 'Project Badges' do it 'deletes a badge when confirming the modal' do wait_for_requests - rows = all('.panel-body > div') + rows = all('.card-body > div') expect(rows.length).to eq 2 click_delete_button(rows[1]) find('.modal .btn-danger').click wait_for_requests - rows = all('.panel-body > div') + rows = all('.card-body > div') expect(rows.length).to eq 1 expect(rows[0]).to have_content group_badge.link_url end diff --git a/spec/features/projects/user_sees_sidebar_spec.rb b/spec/features/projects/user_sees_sidebar_spec.rb index cf80517b934..8a9255db9e8 100644 --- a/spec/features/projects/user_sees_sidebar_spec.rb +++ b/spec/features/projects/user_sees_sidebar_spec.rb @@ -37,7 +37,7 @@ describe 'Projects > User sees sidebar' do visit project_path(project) within('.nav-sidebar') do - expect(page).to have_content 'Overview' + expect(page).to have_content 'Project' expect(page).to have_content 'Issues' expect(page).to have_content 'Wiki' diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb index 47c5a8161d9..495a010b32c 100644 --- a/spec/features/projects/user_uses_shortcuts_spec.rb +++ b/spec/features/projects/user_uses_shortcuts_spec.rb @@ -13,6 +13,8 @@ describe 'User uses shortcuts', :js do context 'when navigating to the Project pages' do it 'redirects to the details page' do + visit project_issues_path(project) + find('body').native.send_key('g') find('body').native.send_key('p') @@ -22,7 +24,7 @@ describe 'User uses shortcuts', :js do it 'redirects to the activity page' do find('body').native.send_key('g') - find('body').native.send_key('e') + find('body').native.send_key('v') expect(page).to have_active_navigation('Project') expect(page).to have_active_sub_navigation('Activity') @@ -72,10 +74,19 @@ describe 'User uses shortcuts', :js do expect(page).to have_active_sub_navigation('List') end + it 'redirects to the issue board page' do + find('body').native.send_key('g') + find('body').native.send_key('b') + + expect(page).to have_active_navigation('Issues') + expect(page).to have_active_sub_navigation('Board') + end + it 'redirects to the new issue page' do find('body').native.send_key('i') expect(page).to have_content(project.title) + expect(page).to have_content('New Issue') end end @@ -88,6 +99,34 @@ describe 'User uses shortcuts', :js do end end + context 'when navigating to the CI / CD pages' do + it 'redirects to the Jobs page' do + find('body').native.send_key('g') + find('body').native.send_key('j') + + expect(page).to have_active_navigation('CI / CD') + expect(page).to have_active_sub_navigation('Jobs') + end + end + + context 'when navigating to the Operations pages' do + it 'redirects to the Environments page' do + find('body').native.send_key('g') + find('body').native.send_key('e') + + expect(page).to have_active_navigation('Operations') + expect(page).to have_active_sub_navigation('Environments') + end + + it 'redirects to the Kubernetes page' do + find('body').native.send_key('g') + find('body').native.send_key('k') + + expect(page).to have_active_navigation('Operations') + expect(page).to have_active_sub_navigation('Kubernetes') + end + end + context 'when navigating to the Snippets pages' do it 'redirects to the snippets page' do find('body').native.send_key('g') diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb index 6586ccaa400..e473739a6aa 100644 --- a/spec/features/projects/wiki/markdown_preview_spec.rb +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -155,4 +155,27 @@ feature 'Projects > Wiki > User previews markdown changes', :js do end end end + + it "does not linkify double brackets inside code blocks as expected" do + click_link 'New page' + page.within '#modal-new-wiki' do + fill_in :new_wiki_path, with: 'linkify_test' + click_button 'Create page' + end + + page.within '.wiki-form' do + fill_in :wiki_content, with: <<-HEREDOC + `[[do_not_linkify]]` + ``` + [[also_do_not_linkify]] + ``` + HEREDOC + click_on "Preview" + end + + expect(page).to have_content("do_not_linkify") + + expect(page.html).to include('[[do_not_linkify]]') + expect(page.html).to include('[[also_do_not_linkify]]') + end end diff --git a/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb b/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb index ab9420fc38f..2c67cec6b67 100644 --- a/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'User deletes wiki page' do +feature 'User deletes wiki page', :js do let(:user) { create(:user) } let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } let(:wiki_page) { create(:wiki_page, wiki: project.wiki) } @@ -13,6 +13,7 @@ feature 'User deletes wiki page' do it 'deletes a page' do click_on('Edit') click_on('Delete') + find('.js-modal-primary-action').click expect(page).to have_content('Page was successfully deleted') end diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index 43cabd3b9f2..0c28a853b54 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -68,8 +68,10 @@ feature 'Protected Branches', :js do within form do find(".js-allowed-to-merge").click + wait_for_requests click_link 'No one' find(".js-allowed-to-push").click + wait_for_requests click_link 'Developers + Masters' end diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb index 6088b831c14..db141ef7096 100644 --- a/spec/features/signed_commits_spec.rb +++ b/spec/features/signed_commits_spec.rb @@ -96,10 +96,11 @@ describe 'GPG signed commits', :js do within(find('.commit', text: 'signed commit by bette cartwright')) do click_on 'Unverified' - within '.popover' do - expect(page).to have_content 'This commit was signed with an unverified signature.' - expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}" - end + end + + within '.popover' do + expect(page).to have_content 'This commit was signed with an unverified signature.' + expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}" end end @@ -110,12 +111,13 @@ describe 'GPG signed commits', :js do within(find('.commit', text: 'signed and authored commit by bette cartwright, different email')) do click_on 'Unverified' - within '.popover' do - expect(page).to have_content 'This commit was signed with a verified signature, but the committer email is not verified to belong to the same user.' - expect(page).to have_content 'Bette Cartwright' - expect(page).to have_content '@bette.cartwright' - expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}" - end + end + + within '.popover' do + expect(page).to have_content 'This commit was signed with a verified signature, but the committer email is not verified to belong to the same user.' + expect(page).to have_content 'Bette Cartwright' + expect(page).to have_content '@bette.cartwright' + expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}" end end @@ -126,12 +128,13 @@ describe 'GPG signed commits', :js do within(find('.commit', text: 'signed commit by bette cartwright')) do click_on 'Unverified' - within '.popover' do - expect(page).to have_content "This commit was signed with a different user's verified signature." - expect(page).to have_content 'Bette Cartwright' - expect(page).to have_content '@bette.cartwright' - expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}" - end + end + + within '.popover' do + expect(page).to have_content "This commit was signed with a different user's verified signature." + expect(page).to have_content 'Bette Cartwright' + expect(page).to have_content '@bette.cartwright' + expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}" end end @@ -142,12 +145,13 @@ describe 'GPG signed commits', :js do within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do click_on 'Verified' - within '.popover' do - expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.' - expect(page).to have_content 'Nannie Bernhard' - expect(page).to have_content '@nannie.bernhard' - expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}" - end + end + + within '.popover' do + expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.' + expect(page).to have_content 'Nannie Bernhard' + expect(page).to have_content '@nannie.bernhard' + expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}" end end @@ -167,12 +171,13 @@ describe 'GPG signed commits', :js do within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do click_on 'Verified' - within '.popover' do - expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.' - expect(page).to have_content 'Nannie Bernhard' - expect(page).to have_content 'nannie.bernhard@example.com' - expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}" - end + end + + within '.popover' do + expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.' + expect(page).to have_content 'Nannie Bernhard' + expect(page).to have_content 'nannie.bernhard@example.com' + expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}" end end end diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb index 19784120108..6be2606fd0d 100644 --- a/spec/features/triggers_spec.rb +++ b/spec/features/triggers_spec.rb @@ -23,7 +23,7 @@ feature 'Triggers', :js do click_button 'Add trigger' # See if input has error due to empty value - expect(page.find('form.gl-show-field-errors .gl-field-error')['style']).to eq 'display: block;' + expect(page.find('form.gl-show-field-errors .gl-field-error')).to be_visible end scenario 'adds new trigger with description' do diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb index 6f968a2c590..1f8d31a5c88 100644 --- a/spec/features/users/login_spec.rb +++ b/spec/features/users/login_spec.rb @@ -394,7 +394,7 @@ feature 'Login' do end def ensure_one_active_tab - expect(page).to have_selector('ul.new-session-tabs > li.active', count: 1) + expect(page).to have_selector('ul.new-session-tabs > li > a.active', count: 1) end def ensure_one_active_pane diff --git a/spec/features/users/terms_spec.rb b/spec/features/users/terms_spec.rb index bf6b5fa3d6a..f9469adbfe3 100644 --- a/spec/features/users/terms_spec.rb +++ b/spec/features/users/terms_spec.rb @@ -81,4 +81,22 @@ describe 'Users > Terms' do expect(find_field('issue_description').value).to eq("We don't want to lose what the user typed") end end + + context 'when the terms are enforced' do + before do + enforce_terms + end + + context 'signing out', :js do + it 'allows the user to sign out without a response' do + visit terms_path + + find('.header-user-dropdown-toggle').click + click_link('Sign out') + + expect(page).to have_content('Sign in') + expect(page).to have_content('Register') + end + end + end end diff --git a/spec/features/users/user_browses_projects_on_user_page_spec.rb b/spec/features/users/user_browses_projects_on_user_page_spec.rb index a70637c8370..7bede0b0d48 100644 --- a/spec/features/users/user_browses_projects_on_user_page_spec.rb +++ b/spec/features/users/user_browses_projects_on_user_page_spec.rb @@ -27,8 +27,8 @@ describe 'Users > User browses projects on user page', :js do end it 'paginates projects', :js do - project = create(:project, namespace: user.namespace) - project2 = create(:project, namespace: user.namespace) + project = create(:project, namespace: user.namespace, updated_at: 2.minutes.since) + project2 = create(:project, namespace: user.namespace, updated_at: 1.minute.since) allow(Project).to receive(:default_per_page).and_return(1) sign_in(user) @@ -41,11 +41,11 @@ describe 'Users > User browses projects on user page', :js do wait_for_requests - expect(page).to have_content(project2.name) + expect(page).to have_content(project.name) click_link('Next') - expect(page).to have_content(project.name) + expect(page).to have_content(project2.name) end context 'when not signed in' do diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 45439640ea3..74e91b02f0f 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -372,7 +372,7 @@ describe IssuesFinder do end context 'personal scope' do - let(:scope) { 'assigned-to-me' } + let(:scope) { 'assigned_to_me' } it 'returns issue assigned to the user' do expect(issues).to contain_exactly(issue1, issue2) diff --git a/spec/finders/personal_projects_finder_spec.rb b/spec/finders/personal_projects_finder_spec.rb index 5e52898e9c0..00c551a1f65 100644 --- a/spec/finders/personal_projects_finder_spec.rb +++ b/spec/finders/personal_projects_finder_spec.rb @@ -4,14 +4,16 @@ describe PersonalProjectsFinder do let(:source_user) { create(:user) } let(:current_user) { create(:user) } let(:finder) { described_class.new(source_user) } - let!(:public_project) { create(:project, :public, namespace: source_user.namespace) } + let!(:public_project) do + create(:project, :public, namespace: source_user.namespace, updated_at: 1.hour.ago) + end let!(:private_project) do - create(:project, :private, namespace: source_user.namespace, path: 'mepmep') + create(:project, :private, namespace: source_user.namespace, updated_at: 3.hours.ago, path: 'mepmep') end let!(:internal_project) do - create(:project, :internal, namespace: source_user.namespace, path: 'C') + create(:project, :internal, namespace: source_user.namespace, updated_at: 2.hours.ago, path: 'C') end before do @@ -28,7 +30,7 @@ describe PersonalProjectsFinder do subject { finder.execute(current_user) } context 'normal user' do - it { is_expected.to eq([internal_project, private_project, public_project]) } + it { is_expected.to eq([public_project, internal_project, private_project]) } end context 'external' do @@ -36,7 +38,7 @@ describe PersonalProjectsFinder do current_user.update_attributes(external: true) end - it { is_expected.to eq([private_project, public_project]) } + it { is_expected.to eq([public_project, private_project]) } end end end diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json index 622a1e40d07..05922df6b81 100644 --- a/spec/fixtures/api/schemas/list.json +++ b/spec/fixtures/api/schemas/list.json @@ -17,6 +17,7 @@ "required": [ "id", "color", + "text_color", "description", "title", "priority" @@ -29,6 +30,7 @@ }, "description": { "type": ["string", "null"] }, "title": { "type": "string" }, + "title": { "text_color": "string" }, "priority": { "type": ["integer", "null"] } } }, diff --git a/spec/fixtures/api/schemas/public_api/v4/projects.json b/spec/fixtures/api/schemas/public_api/v4/projects.json index d89eeea89a5..17ad8d8c48d 100644 --- a/spec/fixtures/api/schemas/public_api/v4/projects.json +++ b/spec/fixtures/api/schemas/public_api/v4/projects.json @@ -20,6 +20,7 @@ "ssh_url_to_repo": { "type": "string" }, "http_url_to_repo": { "type": "string" }, "web_url": { "type": "string" }, + "readme_url": { "type": ["string", "null"] }, "avatar_url": { "type": ["string", "null"] }, "star_count": { "type": "integer" }, "forks_count": { "type": "integer" }, diff --git a/spec/fixtures/emails/valid_new_issue_with_quote.eml b/spec/fixtures/emails/valid_new_issue_with_quote.eml new file mode 100644 index 00000000000..0caf8ed4e9e --- /dev/null +++ b/spec/fixtures/emails/valid_new_issue_with_quote.eml @@ -0,0 +1,25 @@ +Return-Path: <jake@adventuretime.ooo> +Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 +Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700 +Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 +Date: Thu, 13 Jun 2013 17:03:48 -0400 +From: Jake the Dog <jake@adventuretime.ooo> +To: incoming+gitlabhq/gitlabhq+auth_token@appmail.adventuretime.ooo +Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> +Subject: New Issue by email +Mime-Version: 1.0 +Content-Type: text/plain; + charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +X-Sieve: CMU Sieve 2.2 +X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu, + 13 Jun 2013 14:03:48 -0700 (PDT) +X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 + +The reply by email functionality should be extended to allow creating a new issue by email. +even when the email is forwarded to the project which may include lines that begin with ">" + +there should be a quote below this line: + +> this is a quote
\ No newline at end of file diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index 8de615ad8c2..a3e010c3206 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -258,13 +258,19 @@ describe BlobHelper do it 'returns full IDE path' do Rails.application.routes.default_url_options[:script_name] = nil - expect(helper.ide_edit_path(project, "master", "")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master/") + expect(helper.ide_edit_path(project, "master", "")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master") + end + + it 'returns full IDE path with second -' do + Rails.application.routes.default_url_options[:script_name] = nil + + expect(helper.ide_edit_path(project, "testing/slashes", "readme.md")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/testing/slashes/-/readme.md") end it 'returns IDE path without relative_url_root' do Rails.application.routes.default_url_options[:script_name] = "/gitlab" - expect(helper.ide_edit_path(project, "master", "")).to eq("/gitlab/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master/") + expect(helper.ide_edit_path(project, "master", "")).to eq("/gitlab/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master") end end end diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 7b59fde999d..cddb49b320f 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -45,22 +45,22 @@ describe IssuablesHelper do it 'returns "Open" when state is :opened' do expect(helper.issuables_state_counter_text(:issues, :opened, true)) - .to eq('<span>Open</span> <span class="badge">42</span>') + .to eq('<span>Open</span> <span class="badge badge-pill">42</span>') end it 'returns "Closed" when state is :closed' do expect(helper.issuables_state_counter_text(:issues, :closed, true)) - .to eq('<span>Closed</span> <span class="badge">42</span>') + .to eq('<span>Closed</span> <span class="badge badge-pill">42</span>') end it 'returns "Merged" when state is :merged' do expect(helper.issuables_state_counter_text(:merge_requests, :merged, true)) - .to eq('<span>Merged</span> <span class="badge">42</span>') + .to eq('<span>Merged</span> <span class="badge badge-pill">42</span>') end it 'returns "All" when state is :all' do expect(helper.issuables_state_counter_text(:merge_requests, :all, true)) - .to eq('<span>All</span> <span class="badge">42</span>') + .to eq('<span>All</span> <span class="badge badge-pill">42</span>') end end end diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js index 3d7ccf432be..e8435116221 100644 --- a/spec/javascripts/api_spec.js +++ b/spec/javascripts/api_spec.js @@ -341,4 +341,25 @@ describe('Api', () => { .catch(done.fail); }); }); + + describe('commitPipelines', () => { + it('fetches pipelines for a given commit', done => { + const projectId = 'example/foobar'; + const commitSha = 'abc123def'; + const expectedUrl = `${dummyUrlRoot}/${projectId}/commit/${commitSha}/pipelines`; + mock.onGet(expectedUrl).reply(200, [ + { + name: 'test', + }, + ]); + + Api.commitPipelines(projectId, commitSha) + .then(({ data }) => { + expect(data.length).toBe(1); + expect(data[0].name).toBe('test'); + }) + .then(done) + .catch(done.fail); + }); + }); }); diff --git a/spec/javascripts/badges/components/badge_list_spec.js b/spec/javascripts/badges/components/badge_list_spec.js index 9439c578973..02e59ae0843 100644 --- a/spec/javascripts/badges/components/badge_list_spec.js +++ b/spec/javascripts/badges/components/badge_list_spec.js @@ -33,7 +33,7 @@ describe('BadgeList component', () => { }); it('renders a header with the badge count', () => { - const header = vm.$el.querySelector('.panel-heading'); + const header = vm.$el.querySelector('.card-header'); expect(header).toHaveText(new RegExp(`Your badges\\s+${numberOfDummyBadges}`)); }); diff --git a/spec/javascripts/badges/components/badge_settings_spec.js b/spec/javascripts/badges/components/badge_settings_spec.js index 3db02982ad4..59367c85125 100644 --- a/spec/javascripts/badges/components/badge_settings_spec.js +++ b/spec/javascripts/badges/components/badge_settings_spec.js @@ -60,7 +60,7 @@ describe('BadgeSettings component', () => { }); it('displays badge list', () => { - const badgeListElement = vm.$el.querySelector('.panel'); + const badgeListElement = vm.$el.querySelector('.card'); expect(badgeListElement).not.toBe(null); expect(badgeListElement).toBeVisible(); expect(badgeListElement).toContainText('Your badges'); @@ -87,7 +87,7 @@ describe('BadgeSettings component', () => { }); it('displays no badge list', () => { - const badgeListElement = vm.$el.querySelector('.panel'); + const badgeListElement = vm.$el.querySelector('.card'); expect(badgeListElement).toBeHidden(); }); }); diff --git a/spec/javascripts/behaviors/secret_values_spec.js b/spec/javascripts/behaviors/secret_values_spec.js index 38d9bba6868..95122fcf30f 100644 --- a/spec/javascripts/behaviors/secret_values_spec.js +++ b/spec/javascripts/behaviors/secret_values_spec.js @@ -9,7 +9,7 @@ function generateValueMarkup( <div class="${placeholderClass}"> *** </div> - <div class="hide ${valueClass}"> + <div class="hidden ${valueClass}"> ${secret} </div> `; diff --git a/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js b/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js index aa87956109f..cb0f2ba686d 100644 --- a/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js +++ b/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js @@ -257,9 +257,9 @@ describe('BalsamiqViewer', () => { name = 'name'; resource = 'resource'; template = ` - <div class="panel panel-default"> - <div class="panel-heading">name</div> - <div class="panel-body"> + <div class="card"> + <div class="card-header">name</div> + <div class="card-body"> <img class="img-thumbnail" src="data:image/png;base64,image"/> </div> </div> diff --git a/spec/javascripts/blob/sketch/index_spec.js b/spec/javascripts/blob/sketch/index_spec.js index 79f40559817..e062a068a92 100644 --- a/spec/javascripts/blob/sketch/index_spec.js +++ b/spec/javascripts/blob/sketch/index_spec.js @@ -82,7 +82,7 @@ describe('Sketch viewer', () => { const img = document.querySelector('#js-sketch-viewer img'); expect(img).not.toBeNull(); - expect(img.classList.contains('img-responsive')).toBeTruthy(); + expect(img.classList.contains('img-fluid')).toBeTruthy(); }); it('renders link to image', () => { diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index 03df6c06691..c06b2f60813 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -83,13 +83,13 @@ describe('Board list component', () => { it('renders issues', () => { expect( - component.$el.querySelectorAll('.card').length, + component.$el.querySelectorAll('.board-card').length, ).toBe(1); }); it('sets data attribute with issue id', () => { expect( - component.$el.querySelector('.card').getAttribute('data-issue-id'), + component.$el.querySelector('.board-card').getAttribute('data-issue-id'), ).toBe('1'); }); diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js index be1ea0b57b4..abeef272c68 100644 --- a/spec/javascripts/boards/issue_card_spec.js +++ b/spec/javascripts/boards/issue_card_spec.js @@ -70,19 +70,19 @@ describe('Issue card component', () => { it('renders issue title', () => { expect( - component.$el.querySelector('.card-title').textContent, + component.$el.querySelector('.board-card-title').textContent, ).toContain(issue.title); }); it('includes issue base in link', () => { expect( - component.$el.querySelector('.card-title a').getAttribute('href'), + component.$el.querySelector('.board-card-title a').getAttribute('href'), ).toContain('/test'); }); it('includes issue title on link', () => { expect( - component.$el.querySelector('.card-title a').getAttribute('title'), + component.$el.querySelector('.board-card-title a').getAttribute('title'), ).toBe(issue.title); }); @@ -105,14 +105,14 @@ describe('Issue card component', () => { it('renders issue ID with #', () => { expect( - component.$el.querySelector('.card-number').textContent, + component.$el.querySelector('.board-card-number').textContent, ).toContain(`#${issue.id}`); }); describe('assignee', () => { it('does not render assignee', () => { expect( - component.$el.querySelector('.card-assignee .avatar'), + component.$el.querySelector('.board-card-assignee .avatar'), ).toBeNull(); }); @@ -125,25 +125,25 @@ describe('Issue card component', () => { it('renders assignee', () => { expect( - component.$el.querySelector('.card-assignee .avatar'), + component.$el.querySelector('.board-card-assignee .avatar'), ).not.toBeNull(); }); it('sets title', () => { expect( - component.$el.querySelector('.card-assignee img').getAttribute('data-original-title'), + component.$el.querySelector('.board-card-assignee img').getAttribute('data-original-title'), ).toContain(`Assigned to ${user.name}`); }); it('sets users path', () => { expect( - component.$el.querySelector('.card-assignee a').getAttribute('href'), + component.$el.querySelector('.board-card-assignee a').getAttribute('href'), ).toBe('/test'); }); it('renders avatar', () => { expect( - component.$el.querySelector('.card-assignee img'), + component.$el.querySelector('.board-card-assignee img'), ).not.toBeNull(); }); }); @@ -161,10 +161,10 @@ describe('Issue card component', () => { it('displays defaults avatar if users avatar is null', () => { expect( - component.$el.querySelector('.card-assignee img'), + component.$el.querySelector('.board-card-assignee img'), ).not.toBeNull(); expect( - component.$el.querySelector('.card-assignee img').getAttribute('src'), + component.$el.querySelector('.board-card-assignee img').getAttribute('src'), ).toBe('default_avatar'); }); }); @@ -197,7 +197,7 @@ describe('Issue card component', () => { }); it('renders all four assignees', () => { - expect(component.$el.querySelectorAll('.card-assignee .avatar').length).toEqual(4); + expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(4); }); describe('more than four assignees', () => { @@ -213,11 +213,11 @@ describe('Issue card component', () => { }); it('renders more avatar counter', () => { - expect(component.$el.querySelector('.card-assignee .avatar-counter').innerText).toEqual('+2'); + expect(component.$el.querySelector('.board-card-assignee .avatar-counter').innerText).toEqual('+2'); }); it('renders three assignees', () => { - expect(component.$el.querySelectorAll('.card-assignee .avatar').length).toEqual(3); + expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(3); }); it('renders 99+ avatar counter', (done) => { @@ -232,7 +232,7 @@ describe('Issue card component', () => { } Vue.nextTick(() => { - expect(component.$el.querySelector('.card-assignee .avatar-counter').innerText).toEqual('99+'); + expect(component.$el.querySelector('.board-card-assignee .avatar-counter').innerText).toEqual('99+'); done(); }); }); @@ -248,13 +248,13 @@ describe('Issue card component', () => { it('renders list label', () => { expect( - component.$el.querySelectorAll('.label').length, + component.$el.querySelectorAll('.badge').length, ).toBe(2); }); it('renders label', () => { const nodes = []; - component.$el.querySelectorAll('.label').forEach((label) => { + component.$el.querySelectorAll('.badge').forEach((label) => { nodes.push(label.title); }); @@ -265,13 +265,13 @@ describe('Issue card component', () => { it('sets label description as title', () => { expect( - component.$el.querySelector('.label').getAttribute('title'), + component.$el.querySelector('.badge').getAttribute('title'), ).toContain(label1.description); }); it('sets background color of button', () => { const nodes = []; - component.$el.querySelectorAll('.label').forEach((label) => { + component.$el.querySelectorAll('.badge').forEach((label) => { nodes.push(label.style.backgroundColor); }); @@ -288,7 +288,7 @@ describe('Issue card component', () => { Vue.nextTick() .then(() => { expect( - component.$el.querySelectorAll('.label').length, + component.$el.querySelectorAll('.badge').length, ).toBe(2); expect( component.$el.textContent, diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js index e4c3bf2bef1..615168645b4 100644 --- a/spec/javascripts/environments/environments_app_spec.js +++ b/spec/javascripts/environments/environments_app_spec.js @@ -1,8 +1,8 @@ -import _ from 'underscore'; import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import environmentsComponent from '~/environments/components/environments_app.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import { headersInterceptor } from 'spec/helpers/vue_resource_helper'; import { environment, folder } from './mock_data'; describe('Environment', () => { @@ -18,103 +18,76 @@ describe('Environment', () => { let EnvironmentsComponent; let component; + let mock; beforeEach(() => { + mock = new MockAdapter(axios); + EnvironmentsComponent = Vue.extend(environmentsComponent); }); + afterEach(() => { + component.$destroy(); + mock.restore(); + }); + describe('successfull request', () => { describe('without environments', () => { - const environmentsEmptyResponseInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { - status: 200, - })); - }; - - beforeEach(() => { - Vue.http.interceptors.push(environmentsEmptyResponseInterceptor); - Vue.http.interceptors.push(headersInterceptor); - }); - - afterEach(() => { - Vue.http.interceptors = _.without( - Vue.http.interceptors, environmentsEmptyResponseInterceptor, - ); - Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor); - }); + beforeEach((done) => { + mock.onGet(mockData.endpoint).reply(200, { environments: [] }); - it('should render the empty state', (done) => { component = mountComponent(EnvironmentsComponent, mockData); setTimeout(() => { - expect( - component.$el.querySelector('.js-new-environment-button').textContent, - ).toContain('New environment'); - - expect( - component.$el.querySelector('.js-blank-state-title').textContent, - ).toContain('You don\'t have any environments right now.'); - done(); }, 0); }); + + it('should render the empty state', () => { + expect( + component.$el.querySelector('.js-new-environment-button').textContent, + ).toContain('New environment'); + + expect( + component.$el.querySelector('.js-blank-state-title').textContent, + ).toContain('You don\'t have any environments right now.'); + }); }); describe('with paginated environments', () => { - let backupInterceptors; - const environmentsResponseInterceptor = (request, next) => { - next((response) => { - response.headers.set('X-nExt-pAge', '2'); - }); - - next(request.respondWith(JSON.stringify({ + beforeEach((done) => { + mock.onGet(mockData.endpoint).reply(200, { environments: [environment], stopped_count: 1, available_count: 0, - }), { - status: 200, - headers: { - 'X-nExt-pAge': '2', - 'x-page': '1', - 'X-Per-Page': '1', - 'X-Prev-Page': '', - 'X-TOTAL': '37', - 'X-Total-Pages': '2', - }, - })); - }; - - beforeEach(() => { - backupInterceptors = Vue.http.interceptors; - Vue.http.interceptors = [ - environmentsResponseInterceptor, - headersInterceptor, - ]; - component = mountComponent(EnvironmentsComponent, mockData); - }); + }, { + 'X-nExt-pAge': '2', + 'x-page': '1', + 'X-Per-Page': '1', + 'X-Prev-Page': '', + 'X-TOTAL': '37', + 'X-Total-Pages': '2', + }); - afterEach(() => { - Vue.http.interceptors = backupInterceptors; - }); + component = mountComponent(EnvironmentsComponent, mockData); - it('should render a table with environments', (done) => { setTimeout(() => { - expect(component.$el.querySelectorAll('table')).not.toBeNull(); - expect( - component.$el.querySelector('.environment-name').textContent.trim(), - ).toEqual(environment.name); done(); }, 0); }); + it('should render a table with environments', () => { + expect(component.$el.querySelectorAll('table')).not.toBeNull(); + expect( + component.$el.querySelector('.environment-name').textContent.trim(), + ).toEqual(environment.name); + }); + describe('pagination', () => { - it('should render pagination', (done) => { - setTimeout(() => { - expect( - component.$el.querySelectorAll('.gl-pagination li').length, - ).toEqual(5); - done(); - }, 0); + it('should render pagination', () => { + expect( + component.$el.querySelectorAll('.gl-pagination li').length, + ).toEqual(5); }); it('should make an API request when page is clicked', (done) => { @@ -133,50 +106,39 @@ describe('Environment', () => { expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' }); done(); - }); + }, 0); }); }); }); }); describe('unsuccessfull request', () => { - const environmentsErrorResponseInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { - status: 500, - })); - }; - - beforeEach(() => { - Vue.http.interceptors.push(environmentsErrorResponseInterceptor); - }); + beforeEach((done) => { + mock.onGet(mockData.endpoint).reply(500, {}); - afterEach(() => { - Vue.http.interceptors = _.without( - Vue.http.interceptors, environmentsErrorResponseInterceptor, - ); - }); - - it('should render empty state', (done) => { component = mountComponent(EnvironmentsComponent, mockData); setTimeout(() => { - expect( - component.$el.querySelector('.js-blank-state-title').textContent, - ).toContain('You don\'t have any environments right now.'); done(); }, 0); }); + + it('should render empty state', () => { + expect( + component.$el.querySelector('.js-blank-state-title').textContent, + ).toContain('You don\'t have any environments right now.'); + }); }); describe('expandable folders', () => { - const environmentsResponseInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify({ - environments: [folder], - stopped_count: 0, - available_count: 1, - }), { - status: 200, - headers: { + beforeEach(() => { + mock.onGet(mockData.endpoint).reply(200, + { + environments: [folder], + stopped_count: 0, + available_count: 1, + }, + { 'X-nExt-pAge': '2', 'x-page': '1', 'X-Per-Page': '1', @@ -184,18 +146,11 @@ describe('Environment', () => { 'X-TOTAL': '37', 'X-Total-Pages': '2', }, - })); - }; + ); - beforeEach(() => { - Vue.http.interceptors.push(environmentsResponseInterceptor); - component = mountComponent(EnvironmentsComponent, mockData); - }); + mock.onGet(environment.folder_path).reply(200, { environments: [environment] }); - afterEach(() => { - Vue.http.interceptors = _.without( - Vue.http.interceptors, environmentsResponseInterceptor, - ); + component = mountComponent(EnvironmentsComponent, mockData); }); it('should open a closed folder', (done) => { @@ -211,7 +166,7 @@ describe('Environment', () => { ).not.toContain('display: none'); done(); }); - }); + }, 0); }); it('should close an opened folder', (done) => { @@ -233,7 +188,7 @@ describe('Environment', () => { done(); }); }); - }); + }, 0); }); it('should show children environments and a button to show all environments', (done) => { @@ -242,49 +197,32 @@ describe('Environment', () => { component.$el.querySelector('.folder-name').click(); Vue.nextTick(() => { - const folderInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify({ - environments: [environment], - }), { status: 200 })); - }; - - Vue.http.interceptors.push(folderInterceptor); - // wait for next async request setTimeout(() => { expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1); expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain('Show all'); - - Vue.http.interceptors = _.without(Vue.http.interceptors, folderInterceptor); done(); }); }); - }); + }, 0); }); }); describe('methods', () => { - const environmentsEmptyResponseInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { - status: 200, - })); - }; - beforeEach(() => { - Vue.http.interceptors.push(environmentsEmptyResponseInterceptor); - Vue.http.interceptors.push(headersInterceptor); + mock.onGet(mockData.endpoint).reply(200, + { + environments: [], + stopped_count: 0, + available_count: 1, + }, + {}, + ); component = mountComponent(EnvironmentsComponent, mockData); spyOn(history, 'pushState').and.stub(); }); - afterEach(() => { - Vue.http.interceptors = _.without( - Vue.http.interceptors, environmentsEmptyResponseInterceptor, - ); - Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor); - }); - describe('updateContent', () => { it('should set given parameters', (done) => { component.updateContent({ scope: 'stopped', page: '3' }) diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index 906a1116974..f5ce4df0bfe 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -1,13 +1,15 @@ -import _ from 'underscore'; import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue'; -import { headersInterceptor } from 'spec/helpers/vue_resource_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { environmentsList } from '../mock_data'; describe('Environments Folder View', () => { let Component; let component; + let mock; + const mockData = { endpoint: 'environments.json', folderName: 'review', @@ -17,46 +19,35 @@ describe('Environments Folder View', () => { }; beforeEach(() => { + mock = new MockAdapter(axios); + Component = Vue.extend(environmentsFolderViewComponent); }); afterEach(() => { + mock.restore(); + component.$destroy(); }); describe('successfull request', () => { - const environmentsResponseInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify({ + beforeEach(() => { + mock.onGet(mockData.endpoint).reply(200, { environments: environmentsList, stopped_count: 1, available_count: 0, - }), { - status: 200, - headers: { - 'X-nExt-pAge': '2', - 'x-page': '1', - 'X-Per-Page': '2', - 'X-Prev-Page': '', - 'X-TOTAL': '20', - 'X-Total-Pages': '10', - }, - })); - }; - - beforeEach(() => { - Vue.http.interceptors.push(environmentsResponseInterceptor); - Vue.http.interceptors.push(headersInterceptor); + }, { + 'X-nExt-pAge': '2', + 'x-page': '1', + 'X-Per-Page': '2', + 'X-Prev-Page': '', + 'X-TOTAL': '20', + 'X-Total-Pages': '10', + }); component = mountComponent(Component, mockData); }); - afterEach(() => { - Vue.http.interceptors = _.without( - Vue.http.interceptors, environmentsResponseInterceptor, - ); - Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor); - }); - it('should render a table with environments', (done) => { setTimeout(() => { expect(component.$el.querySelectorAll('table')).not.toBeNull(); @@ -135,25 +126,15 @@ describe('Environments Folder View', () => { }); describe('unsuccessfull request', () => { - const environmentsErrorResponseInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { - status: 500, - })); - }; - beforeEach(() => { - Vue.http.interceptors.push(environmentsErrorResponseInterceptor); - }); + mock.onGet(mockData.endpoint).reply(500, { + environments: [], + }); - afterEach(() => { - Vue.http.interceptors = _.without( - Vue.http.interceptors, environmentsErrorResponseInterceptor, - ); + component = mountComponent(Component, mockData); }); it('should not render a table', (done) => { - component = mountComponent(Component, mockData); - setTimeout(() => { expect( component.$el.querySelector('table'), @@ -190,27 +171,15 @@ describe('Environments Folder View', () => { }); describe('methods', () => { - const environmentsEmptyResponseInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { - status: 200, - })); - }; - beforeEach(() => { - Vue.http.interceptors.push(environmentsEmptyResponseInterceptor); - Vue.http.interceptors.push(headersInterceptor); + mock.onGet(mockData.endpoint).reply(200, { + environments: [], + }); component = mountComponent(Component, mockData); spyOn(history, 'pushState').and.stub(); }); - afterEach(() => { - Vue.http.interceptors = _.without( - Vue.http.interceptors, environmentsEmptyResponseInterceptor, - ); - Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor); - }); - describe('updateContent', () => { it('should set given parameters', (done) => { component.updateContent({ scope: 'stopped', page: '4' }) diff --git a/spec/javascripts/environments/mock_data.js b/spec/javascripts/environments/mock_data.js index 15e11aa686b..8a1e26935d9 100644 --- a/spec/javascripts/environments/mock_data.js +++ b/spec/javascripts/environments/mock_data.js @@ -82,6 +82,7 @@ export const environment = { stop_path: '/root/review-app/environments/7/stop', created_at: '2017-01-31T10:53:46.894Z', updated_at: '2017-01-31T10:53:46.894Z', + folder_path: '/root/review-app/environments/7', }, }; diff --git a/spec/javascripts/fixtures/event_filter.html.haml b/spec/javascripts/fixtures/event_filter.html.haml index 5477c6075f0..aa7af61c7eb 100644 --- a/spec/javascripts/fixtures/event_filter.html.haml +++ b/spec/javascripts/fixtures/event_filter.html.haml @@ -1,4 +1,4 @@ -%ul.nav-links.event-filter.scrolling-tabs +%ul.nav-links.event-filter.scrolling-tabs.nav.nav-tabs %li.active %a.event-filter-link{ id: "all_event_filter", title: "Filter by all", href: "/dashboard/activity"} %span diff --git a/spec/javascripts/fixtures/issue_sidebar_label.html.haml b/spec/javascripts/fixtures/issue_sidebar_label.html.haml index 397bdc85c67..06ce248dc9c 100644 --- a/spec/javascripts/fixtures/issue_sidebar_label.html.haml +++ b/spec/javascripts/fixtures/issue_sidebar_label.html.haml @@ -1,7 +1,7 @@ .block.labels .sidebar-collapsed-icon.js-sidebar-labels-tooltip .title.hide-collapsed - %a.edit-link.pull-right{ href: "#" } + %a.edit-link.float-right{ href: "#" } Edit .selectbox.hide-collapsed{ style: "display: none;" } .dropdown diff --git a/spec/javascripts/fixtures/linked_tabs.html.haml b/spec/javascripts/fixtures/linked_tabs.html.haml index c38fe8b1f25..632606e0536 100644 --- a/spec/javascripts/fixtures/linked_tabs.html.haml +++ b/spec/javascripts/fixtures/linked_tabs.html.haml @@ -1,9 +1,9 @@ -%ul.nav-links.new-session-tabs.linked-tabs - %li - %a{ href: 'foo/bar/1', data: { target: 'div#tab1', action: 'tab1', toggle: 'tab' } } +%ul.nav.nav-tabs.new-session-tabs.linked-tabs + %li.nav-item + %a.nav-link{ href: 'foo/bar/1', data: { target: 'div#tab1', action: 'tab1', toggle: 'tab' } } Tab 1 - %li - %a{ href: 'foo/bar/1/context', data: { target: 'div#tab2', action: 'tab2', toggle: 'tab' } } + %li.nav-item + %a.nav-link{ href: 'foo/bar/1/context', data: { target: 'div#tab2', action: 'tab2', toggle: 'tab' } } Tab 2 .tab-content diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js index 7f9c4811fba..175f386b60e 100644 --- a/spec/javascripts/gl_dropdown_spec.js +++ b/spec/javascripts/gl_dropdown_spec.js @@ -70,9 +70,9 @@ describe('glDropdown', function describeDropdown() { it('should open on click', () => { initDropDown.call(this, false); - expect(this.dropdownContainerElement).not.toHaveClass('open'); + expect(this.dropdownContainerElement).not.toHaveClass('show'); this.dropdownButtonElement.click(); - expect(this.dropdownContainerElement).toHaveClass('open'); + expect(this.dropdownContainerElement).toHaveClass('show'); }); it('escapes HTML as text', () => { @@ -134,12 +134,12 @@ describe('glDropdown', function describeDropdown() { }); it('should click the selected item on ENTER keypress', () => { - expect(this.dropdownContainerElement).toHaveClass('open'); + expect(this.dropdownContainerElement).toHaveClass('show'); const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0; navigateWithKeys('down', randomIndex, () => { const visitUrl = spyOnDependency(GLDropdown, 'visitUrl').and.stub(); navigateWithKeys('enter', null, () => { - expect(this.dropdownContainerElement).not.toHaveClass('open'); + expect(this.dropdownContainerElement).not.toHaveClass('show'); const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement); expect(link).toHaveClass('is-active'); const linkedLocation = link.attr('href'); @@ -149,13 +149,13 @@ describe('glDropdown', function describeDropdown() { }); it('should close on ESC keypress', () => { - expect(this.dropdownContainerElement).toHaveClass('open'); + expect(this.dropdownContainerElement).toHaveClass('show'); this.dropdownContainerElement.trigger({ type: 'keyup', which: ARROW_KEYS.ESC, keyCode: ARROW_KEYS.ESC }); - expect(this.dropdownContainerElement).not.toHaveClass('open'); + expect(this.dropdownContainerElement).not.toHaveClass('show'); }); }); diff --git a/spec/javascripts/helpers/vue_mount_component_helper.js b/spec/javascripts/helpers/vue_mount_component_helper.js index effacbcff4e..a34a1add4e0 100644 --- a/spec/javascripts/helpers/vue_mount_component_helper.js +++ b/spec/javascripts/helpers/vue_mount_component_helper.js @@ -1,14 +1,30 @@ +import Vue from 'vue'; + +const mountComponent = (Component, props = {}, el = null) => new Component({ + propsData: props, +}).$mount(el); + export const createComponentWithStore = (Component, store, propsData = {}) => new Component({ store, propsData, }); +export const createComponentWithMixin = (mixins = [], state = {}, props = {}, template = '<div></div>') => { + const Component = Vue.extend({ + template, + mixins, + data() { + return props; + }, + }); + + return mountComponent(Component, props); +}; + export const mountComponentWithStore = (Component, { el, props, store }) => new Component({ store, propsData: props || { }, }).$mount(el); -export default (Component, props = {}, el = null) => new Component({ - propsData: props, -}).$mount(el); +export default mountComponent; diff --git a/spec/javascripts/helpers/vuex_action_helper.js b/spec/javascripts/helpers/vuex_action_helper.js index 83f29d1b0c2..d6ab0aeeed7 100644 --- a/spec/javascripts/helpers/vuex_action_helper.js +++ b/spec/javascripts/helpers/vuex_action_helper.js @@ -55,7 +55,7 @@ export default (action, payload, state, expectedMutations, expectedActions, done }; // call the action with mocked store and arguments - action({ commit, state, dispatch }, payload); + action({ commit, state, dispatch, rootState: state }, payload); // check if no mutations should have been dispatched if (expectedMutations.length === 0) { diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js index 6f580e1f7af..045a60e56a0 100644 --- a/spec/javascripts/ide/components/ide_spec.js +++ b/spec/javascripts/ide/components/ide_spec.js @@ -107,5 +107,11 @@ describe('ide component', () => { vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'), ).toBe(true); }); + + it('stops callback in monaco editor', () => { + setFixtures('<div class="inputarea"></div>'); + + expect(vm.mousetrapStopCallback(null, document.querySelector('.inputarea'), 't')).toBe(true); + }); }); }); diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js index ff500acd849..d3f80e6f9c0 100644 --- a/spec/javascripts/ide/components/repo_editor_spec.js +++ b/spec/javascripts/ide/components/repo_editor_spec.js @@ -346,4 +346,24 @@ describe('RepoEditor', () => { }); }); }); + + it('calls removePendingTab when old file is pending', done => { + spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true); + spyOn(vm, 'removePendingTab'); + + vm.file.pending = true; + + vm + .$nextTick() + .then(() => { + vm.file = file('testing'); + + return vm.$nextTick(); + }) + .then(() => { + expect(vm.removePendingTab).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); }); diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js index c059862b9d1..c68ae050641 100644 --- a/spec/javascripts/ide/mock_data.js +++ b/spec/javascripts/ide/mock_data.js @@ -1,4 +1,3 @@ -// eslint-disable-next-line import/prefer-default-export export const projectData = { id: 1, name: 'abcproject', @@ -14,3 +13,83 @@ export const projectData = { mergeRequests: {}, merge_requests_enabled: true, }; + +export const pipelines = [ + { + id: 1, + ref: 'master', + sha: '123', + status: 'failed', + }, + { + id: 2, + ref: 'master', + sha: '213', + status: 'success', + }, +]; + +export const jobs = [ + { + id: 1, + name: 'test', + status: 'failed', + stage: 'test', + duration: 1, + }, + { + id: 2, + name: 'test 2', + status: 'failed', + stage: 'test', + duration: 1, + }, + { + id: 3, + name: 'test 3', + status: 'failed', + stage: 'test', + duration: 1, + }, + { + id: 4, + name: 'test 3', + status: 'failed', + stage: 'build', + duration: 1, + }, +]; + +export const fullPipelinesResponse = { + data: { + count: { + all: 2, + }, + pipelines: [ + { + id: '51', + commit: { + id: 'xxxxxxxxxxxxxxxxxxxx', + }, + details: { + status: { + icon: 'status_failed', + text: 'failed', + }, + }, + }, + { + id: '50', + commit: { + id: 'abc123def456ghi789jkl', + }, + details: { + status: { + icon: 'status_passed', + text: 'passed', + }, + }, + }, + ], + }, +}; diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js index ebd08d95810..8e078ae7138 100644 --- a/spec/javascripts/ide/stores/actions/project_spec.js +++ b/spec/javascripts/ide/stores/actions/project_spec.js @@ -1,14 +1,33 @@ -import { - refreshLastCommitData, -} from '~/ide/stores/actions'; +import Visibility from 'visibilityjs'; +import MockAdapter from 'axios-mock-adapter'; +import { refreshLastCommitData, pollSuccessCallBack } from '~/ide/stores/actions'; import store from '~/ide/stores'; import service from '~/ide/services'; +import axios from '~/lib/utils/axios_utils'; +import { fullPipelinesResponse } from '../../mock_data'; import { resetStore } from '../../helpers'; import testAction from '../../../helpers/vuex_action_helper'; describe('IDE store project actions', () => { + const setProjectState = () => { + store.state.currentProjectId = 'abc/def'; + store.state.currentBranchId = 'master'; + store.state.projects['abc/def'] = { + id: 4, + path_with_namespace: 'abc/def', + branches: { + master: { + commit: { + id: 'abc123def456ghi789jkl', + title: 'example', + }, + }, + }, + }; + }; + beforeEach(() => { - store.state.projects.abcproject = {}; + store.state.projects['abc/def'] = {}; }); afterEach(() => { @@ -17,18 +36,16 @@ describe('IDE store project actions', () => { describe('refreshLastCommitData', () => { beforeEach(() => { - store.state.currentProjectId = 'abcproject'; + store.state.currentProjectId = 'abc/def'; store.state.currentBranchId = 'master'; - store.state.projects.abcproject = { + store.state.projects['abc/def'] = { + id: 4, branches: { master: { commit: null, }, }, }; - }); - - it('calls the service', done => { spyOn(service, 'getBranchData').and.returnValue( Promise.resolve({ data: { @@ -36,14 +53,16 @@ describe('IDE store project actions', () => { }, }), ); + }); + it('calls the service', done => { store .dispatch('refreshLastCommitData', { projectId: store.state.currentProjectId, branchId: store.state.currentBranchId, }) .then(() => { - expect(service.getBranchData).toHaveBeenCalledWith('abcproject', 'master'); + expect(service.getBranchData).toHaveBeenCalledWith('abc/def', 'master'); done(); }) @@ -53,16 +72,118 @@ describe('IDE store project actions', () => { it('commits getBranchData', done => { testAction( refreshLastCommitData, - {}, - {}, - [{ - type: 'SET_BRANCH_COMMIT', - payload: { - projectId: 'abcproject', - branchId: 'master', - commit: { id: '123' }, + { + projectId: store.state.currentProjectId, + branchId: store.state.currentBranchId, + }, + store.state, + [ + { + type: 'SET_BRANCH_COMMIT', + payload: { + projectId: 'abc/def', + branchId: 'master', + commit: { id: '123' }, + }, + }, + ], // mutations + [ + { + type: 'getLastCommitPipeline', + payload: { + projectId: 'abc/def', + projectIdNumber: store.state.projects['abc/def'].id, + branchId: 'master', + }, + }, + ], // action + done, + ); + }); + }); + + describe('pipelinePoll', () => { + let mock; + + beforeEach(() => { + setProjectState(); + jasmine.clock().install(); + mock = new MockAdapter(axios); + mock + .onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines') + .reply(200, { data: { foo: 'bar' } }, { 'poll-interval': '10000' }); + }); + + afterEach(() => { + jasmine.clock().uninstall(); + mock.restore(); + store.dispatch('stopPipelinePolling'); + }); + + it('calls service periodically', done => { + spyOn(axios, 'get').and.callThrough(); + spyOn(Visibility, 'hidden').and.returnValue(false); + + store + .dispatch('pipelinePoll') + .then(() => { + jasmine.clock().tick(1000); + + expect(axios.get).toHaveBeenCalled(); + expect(axios.get.calls.count()).toBe(1); + }) + .then(() => new Promise(resolve => requestAnimationFrame(resolve))) + .then(() => { + jasmine.clock().tick(10000); + expect(axios.get.calls.count()).toBe(2); + }) + .then(() => new Promise(resolve => requestAnimationFrame(resolve))) + .then(() => { + jasmine.clock().tick(10000); + expect(axios.get.calls.count()).toBe(3); + }) + .then(() => new Promise(resolve => requestAnimationFrame(resolve))) + .then(() => { + jasmine.clock().tick(10000); + expect(axios.get.calls.count()).toBe(4); + }) + + .then(done) + .catch(done.fail); + }); + }); + + describe('pollSuccessCallBack', () => { + beforeEach(() => { + setProjectState(); + }); + + it('commits correct pipeline', done => { + testAction( + pollSuccessCallBack, + fullPipelinesResponse, + store.state, + [ + { + type: 'SET_LAST_COMMIT_PIPELINE', + payload: { + projectId: 'abc/def', + branchId: 'master', + pipeline: { + id: '50', + commit: { + id: 'abc123def456ghi789jkl', + }, + details: { + status: { + icon: 'status_passed', + text: 'passed', + }, + }, + }, + }, }, - }], // mutations + ], // mutations [], // action done, ); diff --git a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js new file mode 100644 index 00000000000..85fbcf8084b --- /dev/null +++ b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js @@ -0,0 +1,289 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import actions, { + requestLatestPipeline, + receiveLatestPipelineError, + receiveLatestPipelineSuccess, + fetchLatestPipeline, + requestJobs, + receiveJobsError, + receiveJobsSuccess, + fetchJobs, +} from '~/ide/stores/modules/pipelines/actions'; +import state from '~/ide/stores/modules/pipelines/state'; +import * as types from '~/ide/stores/modules/pipelines/mutation_types'; +import testAction from '../../../../helpers/vuex_action_helper'; +import { pipelines, jobs } from '../../../mock_data'; + +describe('IDE pipelines actions', () => { + let mockedState; + let mock; + + beforeEach(() => { + mockedState = state(); + mock = new MockAdapter(axios); + + gon.api_version = 'v4'; + mockedState.currentProjectId = 'test/project'; + }); + + afterEach(() => { + mock.restore(); + }); + + describe('requestLatestPipeline', () => { + it('commits request', done => { + testAction( + requestLatestPipeline, + null, + mockedState, + [{ type: types.REQUEST_LATEST_PIPELINE }], + [], + done, + ); + }); + }); + + describe('receiveLatestPipelineError', () => { + it('commits error', done => { + testAction( + receiveLatestPipelineError, + null, + mockedState, + [{ type: types.RECEIVE_LASTEST_PIPELINE_ERROR }], + [], + done, + ); + }); + + it('creates flash message', () => { + const flashSpy = spyOnDependency(actions, 'flash'); + + receiveLatestPipelineError({ commit() {} }); + + expect(flashSpy).toHaveBeenCalled(); + }); + }); + + describe('receiveLatestPipelineSuccess', () => { + it('commits pipeline', done => { + testAction( + receiveLatestPipelineSuccess, + pipelines[0], + mockedState, + [{ type: types.RECEIVE_LASTEST_PIPELINE_SUCCESS, payload: pipelines[0] }], + [], + done, + ); + }); + }); + + describe('fetchLatestPipeline', () => { + describe('success', () => { + beforeEach(() => { + mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(200, pipelines); + }); + + it('dispatches request', done => { + testAction( + fetchLatestPipeline, + '123', + mockedState, + [], + [{ type: 'requestLatestPipeline' }, { type: 'receiveLatestPipelineSuccess' }], + done, + ); + }); + + it('dispatches success with latest pipeline', done => { + testAction( + fetchLatestPipeline, + '123', + mockedState, + [], + [ + { type: 'requestLatestPipeline' }, + { type: 'receiveLatestPipelineSuccess', payload: pipelines[0] }, + ], + done, + ); + }); + + it('calls axios with correct params', () => { + const apiSpy = spyOn(axios, 'get').and.callThrough(); + + fetchLatestPipeline({ dispatch() {}, rootState: state }, '123'); + + expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), { + params: { + sha: '123', + per_page: '1', + }, + }); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(500); + }); + + it('dispatches error', done => { + testAction( + fetchLatestPipeline, + '123', + mockedState, + [], + [{ type: 'requestLatestPipeline' }, { type: 'receiveLatestPipelineError' }], + done, + ); + }); + }); + }); + + describe('requestJobs', () => { + it('commits request', done => { + testAction(requestJobs, null, mockedState, [{ type: types.REQUEST_JOBS }], [], done); + }); + }); + + describe('receiveJobsError', () => { + it('commits error', done => { + testAction( + receiveJobsError, + null, + mockedState, + [{ type: types.RECEIVE_JOBS_ERROR }], + [], + done, + ); + }); + + it('creates flash message', () => { + const flashSpy = spyOnDependency(actions, 'flash'); + + receiveJobsError({ commit() {} }); + + expect(flashSpy).toHaveBeenCalled(); + }); + }); + + describe('receiveJobsSuccess', () => { + it('commits jobs', done => { + testAction( + receiveJobsSuccess, + jobs, + mockedState, + [{ type: types.RECEIVE_JOBS_SUCCESS, payload: jobs }], + [], + done, + ); + }); + }); + + describe('fetchJobs', () => { + let page = ''; + + beforeEach(() => { + mockedState.latestPipeline = pipelines[0]; + }); + + describe('success', () => { + beforeEach(() => { + mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines\/(.*)\/jobs/).replyOnce(() => [ + 200, + jobs, + { + 'x-next-page': page, + }, + ]); + }); + + it('dispatches request', done => { + testAction( + fetchJobs, + null, + mockedState, + [], + [{ type: 'requestJobs' }, { type: 'receiveJobsSuccess' }], + done, + ); + }); + + it('dispatches success with latest pipeline', done => { + testAction( + fetchJobs, + null, + mockedState, + [], + [{ type: 'requestJobs' }, { type: 'receiveJobsSuccess', payload: jobs }], + done, + ); + }); + + it('dispatches twice for both pages', done => { + page = '2'; + + testAction( + fetchJobs, + null, + mockedState, + [], + [ + { type: 'requestJobs' }, + { type: 'receiveJobsSuccess', payload: jobs }, + { type: 'fetchJobs', payload: '2' }, + { type: 'requestJobs' }, + { type: 'receiveJobsSuccess', payload: jobs }, + ], + done, + ); + }); + + it('calls axios with correct URL', () => { + const apiSpy = spyOn(axios, 'get').and.callThrough(); + + fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState }); + + expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', { + params: { page: '1' }, + }); + }); + + it('calls axios with page next page', () => { + const apiSpy = spyOn(axios, 'get').and.callThrough(); + + fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState }); + + expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', { + params: { page: '1' }, + }); + + page = '2'; + + fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState }, page); + + expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', { + params: { page: '2' }, + }); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(500); + }); + + it('dispatches error', done => { + testAction( + fetchJobs, + null, + mockedState, + [], + [{ type: 'requestJobs' }, { type: 'receiveJobsError' }], + done, + ); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js b/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js new file mode 100644 index 00000000000..b2a7e8a9025 --- /dev/null +++ b/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js @@ -0,0 +1,71 @@ +import * as getters from '~/ide/stores/modules/pipelines/getters'; +import state from '~/ide/stores/modules/pipelines/state'; + +describe('IDE pipeline getters', () => { + let mockedState; + + beforeEach(() => { + mockedState = state(); + }); + + describe('hasLatestPipeline', () => { + it('returns false when loading is true', () => { + mockedState.isLoadingPipeline = true; + + expect(getters.hasLatestPipeline(mockedState)).toBe(false); + }); + + it('returns false when pipelines is null', () => { + mockedState.latestPipeline = null; + + expect(getters.hasLatestPipeline(mockedState)).toBe(false); + }); + + it('returns false when loading is true & pipelines is null', () => { + mockedState.latestPipeline = null; + mockedState.isLoadingPipeline = true; + + expect(getters.hasLatestPipeline(mockedState)).toBe(false); + }); + + it('returns true when loading is false & pipelines is an object', () => { + mockedState.latestPipeline = { + id: 1, + }; + mockedState.isLoadingPipeline = false; + + expect(getters.hasLatestPipeline(mockedState)).toBe(true); + }); + }); + + describe('failedJobs', () => { + it('returns array of failed jobs', () => { + mockedState.stages = [ + { + title: 'test', + jobs: [{ id: 1, status: 'failed' }, { id: 2, status: 'success' }], + }, + { + title: 'build', + jobs: [{ id: 3, status: 'failed' }, { id: 4, status: 'failed' }], + }, + ]; + + expect(getters.failedJobs(mockedState).length).toBe(3); + expect(getters.failedJobs(mockedState)).toEqual([ + { + id: 1, + status: jasmine.anything(), + }, + { + id: 3, + status: jasmine.anything(), + }, + { + id: 4, + status: jasmine.anything(), + }, + ]); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js new file mode 100644 index 00000000000..8262e916243 --- /dev/null +++ b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js @@ -0,0 +1,120 @@ +import mutations from '~/ide/stores/modules/pipelines/mutations'; +import state from '~/ide/stores/modules/pipelines/state'; +import * as types from '~/ide/stores/modules/pipelines/mutation_types'; +import { pipelines, jobs } from '../../../mock_data'; + +describe('IDE pipelines mutations', () => { + let mockedState; + + beforeEach(() => { + mockedState = state(); + }); + + describe(types.REQUEST_LATEST_PIPELINE, () => { + it('sets loading to true', () => { + mutations[types.REQUEST_LATEST_PIPELINE](mockedState); + + expect(mockedState.isLoadingPipeline).toBe(true); + }); + }); + + describe(types.RECEIVE_LASTEST_PIPELINE_ERROR, () => { + it('sets loading to false', () => { + mutations[types.RECEIVE_LASTEST_PIPELINE_ERROR](mockedState); + + expect(mockedState.isLoadingPipeline).toBe(false); + }); + }); + + describe(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, () => { + it('sets loading to false on success', () => { + mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, pipelines[0]); + + expect(mockedState.isLoadingPipeline).toBe(false); + }); + + it('sets latestPipeline', () => { + mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, pipelines[0]); + + expect(mockedState.latestPipeline).toEqual({ + id: pipelines[0].id, + status: pipelines[0].status, + }); + }); + + it('does not set latest pipeline if pipeline is null', () => { + mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, null); + + expect(mockedState.latestPipeline).toEqual(null); + }); + }); + + describe(types.REQUEST_JOBS, () => { + it('sets jobs loading to true', () => { + mutations[types.REQUEST_JOBS](mockedState); + + expect(mockedState.isLoadingJobs).toBe(true); + }); + }); + + describe(types.RECEIVE_JOBS_ERROR, () => { + it('sets jobs loading to false', () => { + mutations[types.RECEIVE_JOBS_ERROR](mockedState); + + expect(mockedState.isLoadingJobs).toBe(false); + }); + }); + + describe(types.RECEIVE_JOBS_SUCCESS, () => { + it('sets jobs loading to false on success', () => { + mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs); + + expect(mockedState.isLoadingJobs).toBe(false); + }); + + it('sets stages', () => { + mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs); + + expect(mockedState.stages.length).toBe(2); + expect(mockedState.stages).toEqual([ + { + title: 'test', + jobs: jasmine.anything(), + }, + { + title: 'build', + jobs: jasmine.anything(), + }, + ]); + }); + + it('sets jobs in stages', () => { + mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs); + + expect(mockedState.stages[0].jobs.length).toBe(3); + expect(mockedState.stages[1].jobs.length).toBe(1); + expect(mockedState.stages).toEqual([ + { + title: jasmine.anything(), + jobs: jobs.filter(job => job.stage === 'test').map(job => ({ + id: job.id, + name: job.name, + status: job.status, + stage: job.stage, + duration: job.duration, + })), + }, + { + title: jasmine.anything(), + jobs: jobs.filter(job => job.stage === 'build').map(job => ({ + id: job.id, + name: job.name, + status: job.status, + stage: job.stage, + duration: job.duration, + })), + }, + ]); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/mutations/branch_spec.js b/spec/javascripts/ide/stores/mutations/branch_spec.js index 29eb859ddaf..f2f1f2a9a2e 100644 --- a/spec/javascripts/ide/stores/mutations/branch_spec.js +++ b/spec/javascripts/ide/stores/mutations/branch_spec.js @@ -37,4 +37,40 @@ describe('Multi-file store branch mutations', () => { expect(localState.projects.Example.branches.master.commit.title).toBe('Example commit'); }); }); + + describe('SET_LAST_COMMIT_PIPELINE', () => { + it('sets the pipeline for the last commit on current project', () => { + localState.projects = { + Example: { + branches: { + master: { + commit: {}, + }, + }, + }, + }; + + mutations.SET_LAST_COMMIT_PIPELINE(localState, { + projectId: 'Example', + branchId: 'master', + pipeline: { + id: '50', + details: { + status: { + icon: 'status_passed', + text: 'passed', + }, + }, + }, + }); + + expect(localState.projects.Example.branches.master.commit.pipeline.id).toBe('50'); + expect(localState.projects.Example.branches.master.commit.pipeline.details.status.text).toBe( + 'passed', + ); + expect(localState.projects.Example.branches.master.commit.pipeline.details.status.icon).toBe( + 'status_passed', + ); + }); + }); }); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 0952356c2f4..648fb3e9bd3 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -974,7 +974,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; ).toBeFalsy(); expect( $tempNoteHeader - .find('.hidden-xs') + .find('.d-none.d-sm-block') .text() .trim(), ).toEqual(currentUserFullname); @@ -1020,7 +1020,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; const $tempNoteHeader = $tempNote.find('.note-header'); expect( $tempNoteHeader - .find('.hidden-xs') + .find('.d-none.d-sm-block') .text() .trim(), ).toEqual('Foo <script>alert("XSS")</script>'); diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js index 71f77e5f42e..1e41a7bfe7c 100644 --- a/spec/javascripts/pipelines/empty_state_spec.js +++ b/spec/javascripts/pipelines/empty_state_spec.js @@ -29,7 +29,7 @@ describe('Pipelines Empty State', () => { expect( component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '), - ).toContain('Continous Integration can help catch bugs by running your tests automatically,'); + ).toContain('Continuous Integration can help catch bugs by running your tests automatically,'); expect( component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '), diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js index d646bef96f5..568e679abe9 100644 --- a/spec/javascripts/pipelines/graph/action_component_spec.js +++ b/spec/javascripts/pipelines/graph/action_component_spec.js @@ -1,13 +1,19 @@ import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import actionComponent from '~/pipelines/components/graph/action_component.vue'; -import eventHub from '~/pipelines/event_hub'; import mountComponent from '../../helpers/vue_mount_component_helper'; describe('pipeline graph action component', () => { let component; + let mock; beforeEach(done => { const ActionComponent = Vue.extend(actionComponent); + mock = new MockAdapter(axios); + + mock.onPost('foo.json').reply(200); + component = mountComponent(ActionComponent, { tooltipText: 'bar', link: 'foo', @@ -18,15 +24,10 @@ describe('pipeline graph action component', () => { }); afterEach(() => { + mock.restore(); component.$destroy(); }); - it('should emit an event with the provided link', () => { - eventHub.$on('postAction', link => { - expect(link).toEqual('foo'); - }); - }); - it('should render the provided title as a bootstrap tooltip', () => { expect(component.$el.getAttribute('data-original-title')).toEqual('bar'); }); @@ -34,10 +35,12 @@ describe('pipeline graph action component', () => { it('should update bootstrap tooltip when title changes', done => { component.tooltipText = 'changed'; - setTimeout(() => { + component.$nextTick() + .then(() => { expect(component.$el.getAttribute('data-original-title')).toBe('changed'); - done(); - }); + }) + .then(done) + .catch(done.fail); }); it('should render an svg', () => { @@ -45,44 +48,18 @@ describe('pipeline graph action component', () => { expect(component.$el.querySelector('svg')).toBeDefined(); }); - it('disables the button when clicked', done => { - component.$el.click(); + describe('on click', () => { + it('emits `pipelineActionRequestComplete` after a successfull request', done => { + spyOn(component, '$emit'); - component.$nextTick(() => { - expect(component.$el.getAttribute('disabled')).toEqual('disabled'); - done(); - }); - }); - - it('re-enabled the button when `requestFinishedFor` matches `linkRequested`', done => { - component.$el.click(); - - component - .$nextTick() - .then(() => { - expect(component.$el.getAttribute('disabled')).toEqual('disabled'); - component.requestFinishedFor = 'foo'; - }) - .then(() => { - expect(component.$el.getAttribute('disabled')).toBeNull(); - }) - .then(done) - .catch(done.fail); - }); - - it('does not re-enable the button when `requestFinishedFor` does not matches `linkRequested`', done => { - component.$el.click(); + component.$el.click(); - component - .$nextTick() - .then(() => { - expect(component.$el.getAttribute('disabled')).toEqual('disabled'); - component.requestFinishedFor = 'bar'; - }) - .then(() => { - expect(component.$el.getAttribute('disabled')).toEqual('disabled'); - }) - .then(done) - .catch(done.fail); + component.$nextTick() + .then(() => { + expect(component.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete'); + }) + .then(done) + .catch(done.fail); + }); }); }); diff --git a/spec/javascripts/pipelines/mock_data.js b/spec/javascripts/pipelines/mock_data.js index a5a200973d7..03ead6cd8ba 100644 --- a/spec/javascripts/pipelines/mock_data.js +++ b/spec/javascripts/pipelines/mock_data.js @@ -217,7 +217,7 @@ export const pipelineWithStages = { browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411442/artifacts/browse', }, { - name: 'codequality', + name: 'code_quality', expired: false, expire_at: '2018-04-18T14:16:24.484Z', path: '/gitlab-org/gitlab-ee/-/jobs/62411441/artifacts/download', diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js index 05ca4cb9044..68043b91bd0 100644 --- a/spec/javascripts/pipelines/pipelines_table_row_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js @@ -182,7 +182,16 @@ describe('Pipelines Table Row', () => { }); component.$el.querySelector('.js-pipelines-cancel-button').click(); - expect(component.isCancelling).toEqual(true); + }); + + it('renders a loading icon when `cancelingPipeline` matches pipeline id', done => { + component.cancelingPipeline = pipeline.id; + component.$nextTick() + .then(() => { + expect(component.isCancelling).toEqual(true); + }) + .then(done) + .catch(done.fail); }); }); }); diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js index 75156e7bdfd..16f6db39d6a 100644 --- a/spec/javascripts/pipelines/stage_spec.js +++ b/spec/javascripts/pipelines/stage_spec.js @@ -102,4 +102,31 @@ describe('Pipelines stage component', () => { }); }); }); + + describe('pipelineActionRequestComplete', () => { + beforeEach(() => { + mock.onGet('path.json').reply(200, stageReply); + + mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200); + }); + + describe('within pipeline table', () => { + it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', done => { + spyOn(eventHub, '$emit'); + + component.type = 'PIPELINES_TABLE'; + component.$el.querySelector('button').click(); + + setTimeout(() => { + component.$el.querySelector('.js-ci-action').click(); + component.$nextTick() + .then(() => { + expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable'); + }) + .then(done) + .catch(done.fail); + }, 0); + }); + }); + }); }); diff --git a/spec/javascripts/projects_dropdown/components/search_spec.js b/spec/javascripts/projects_dropdown/components/search_spec.js index 601264258c2..427f5024e3a 100644 --- a/spec/javascripts/projects_dropdown/components/search_spec.js +++ b/spec/javascripts/projects_dropdown/components/search_spec.js @@ -92,7 +92,6 @@ describe('SearchComponent', () => { const inputEl = vm.$el.querySelector('input.form-control'); expect(vm.$el.classList.contains('search-input-container')).toBeTruthy(); - expect(vm.$el.classList.contains('hidden-xs')).toBeTruthy(); expect(inputEl).not.toBe(null); expect(inputEl.getAttribute('placeholder')).toBe('Search your projects'); expect(vm.$el.querySelector('.search-icon')).toBeDefined(); diff --git a/spec/javascripts/sidebar/sidebar_move_issue_spec.js b/spec/javascripts/sidebar/sidebar_move_issue_spec.js index 00847df4b60..8f35b9ca437 100644 --- a/spec/javascripts/sidebar/sidebar_move_issue_spec.js +++ b/spec/javascripts/sidebar/sidebar_move_issue_spec.js @@ -14,7 +14,9 @@ describe('SidebarMoveIssue', function () { this.$content = $(` <div class="dropdown"> <div class="js-toggle"></div> - <div class="dropdown-content"></div> + <div class="dropdown-menu"> + <div class="dropdown-content"></div> + </div> <div class="js-confirm-button"></div> </div> `); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js index db27aa144d6..00f4f2d7c39 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js @@ -1,12 +1,12 @@ import Vue from 'vue'; -import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author.vue'; +import MrWidgetAuthor from '~/vue_merge_request_widget/components/mr_widget_author.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; -describe('MRWidgetAuthor', () => { +describe('MrWidgetAuthor', () => { let vm; beforeEach(() => { - const Component = Vue.extend(authorComponent); + const Component = Vue.extend(MrWidgetAuthor); vm = mountComponent(Component, { author: { diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_maintainer_edit_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_maintainer_edit_spec.js deleted file mode 100644 index cee22d5342a..00000000000 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_maintainer_edit_spec.js +++ /dev/null @@ -1,40 +0,0 @@ -import Vue from 'vue'; -import maintainerEditComponent from '~/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -describe('RWidgetMaintainerEdit', () => { - let Component; - let vm; - - beforeEach(() => { - Component = Vue.extend(maintainerEditComponent); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('when a maintainer is allowed to edit', () => { - beforeEach(() => { - vm = mountComponent(Component, { - maintainerEditAllowed: true, - }); - }); - - it('it renders the message', () => { - expect(vm.$el.textContent.trim()).toEqual('Allows edits from maintainers'); - }); - }); - - describe('when a maintainer is not allowed to edit', () => { - beforeEach(() => { - vm = mountComponent(Component, { - maintainerEditAllowed: false, - }); - }); - - it('hides the message', () => { - expect(vm.$el.textContent.trim()).toEqual(''); - }); - }); -}); diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index e55c7649d40..30918428da2 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options'; +import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; import notify from '~/lib/utils/notify'; import { stateKey } from '~/vue_merge_request_widget/stores/state_maps'; diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb index a547988d631..c73c6023b60 100644 --- a/spec/lib/api/helpers/pagination_spec.rb +++ b/spec/lib/api/helpers/pagination_spec.rb @@ -7,7 +7,203 @@ describe API::Helpers::Pagination do Class.new.include(described_class).new end - describe '#paginate' do + describe '#paginate (keyset pagination)' do + let(:value) { spy('return value') } + + before do + allow(value).to receive(:to_query).and_return(value) + + allow(subject).to receive(:header).and_return(value) + allow(subject).to receive(:params).and_return(value) + allow(subject).to receive(:request).and_return(value) + end + + context 'when resource can be paginated' do + let!(:projects) do + [ + create(:project, name: 'One'), + create(:project, name: 'Two'), + create(:project, name: 'Three') + ].sort_by { |e| -e.id } # sort by id desc (this is the default sort order for the API) + end + + describe 'first page' do + before do + allow(subject).to receive(:params) + .and_return({ pagination: 'keyset', per_page: 2 }) + end + + it 'returns appropriate amount of resources' do + expect(subject.paginate(resource).count).to eq 2 + end + + it 'returns the first two records (by id desc)' do + expect(subject.paginate(resource)).to eq(projects[0..1]) + end + + it 'adds appropriate headers' do + expect_header('X-Per-Page', '2') + expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[1].id}&pagination=keyset&per_page=2") + + expect_header('Link', anything) do |_key, val| + expect(val).to include('rel="next"') + end + + subject.paginate(resource) + end + end + + describe 'second page' do + before do + allow(subject).to receive(:params) + .and_return({ pagination: 'keyset', per_page: 2, ks_prev_id: projects[1].id }) + end + + it 'returns appropriate amount of resources' do + expect(subject.paginate(resource).count).to eq 1 + end + + it 'returns the third record' do + expect(subject.paginate(resource)).to eq(projects[2..2]) + end + + it 'adds appropriate headers' do + expect_header('X-Per-Page', '2') + expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[2].id}&pagination=keyset&per_page=2") + + expect_header('Link', anything) do |_key, val| + expect(val).to include('rel="next"') + end + + subject.paginate(resource) + end + end + + describe 'third page' do + before do + allow(subject).to receive(:params) + .and_return({ pagination: 'keyset', per_page: 2, ks_prev_id: projects[2].id }) + end + + it 'returns appropriate amount of resources' do + expect(subject.paginate(resource).count).to eq 0 + end + + it 'adds appropriate headers' do + expect_header('X-Per-Page', '2') + expect(subject).not_to receive(:header).with('Link') + + subject.paginate(resource) + end + end + + context 'if order' do + context 'is not present' do + before do + allow(subject).to receive(:params) + .and_return({ pagination: 'keyset', per_page: 2 }) + end + + it 'is not present it adds default order(:id) desc' do + resource.order_values = [] + + paginated_relation = subject.paginate(resource) + + expect(resource.order_values).to be_empty + expect(paginated_relation.order_values).to be_present + expect(paginated_relation.order_values.size).to eq(1) + expect(paginated_relation.order_values.first).to be_descending + expect(paginated_relation.order_values.first.expr.name).to eq :id + end + end + + context 'is present' do + let(:resource) { Project.all.order(name: :desc) } + let!(:projects) do + [ + create(:project, name: 'One'), + create(:project, name: 'Two'), + create(:project, name: 'Three'), + create(:project, name: 'Three'), # Note the duplicate name + create(:project, name: 'Four'), + create(:project, name: 'Five'), + create(:project, name: 'Six') + ] + + # if we sort this by name descending, id descending, this yields: + # { + # 2 => "Two", + # 4 => "Three", + # 3 => "Three", + # 7 => "Six", + # 1 => "One", + # 5 => "Four", + # 6 => "Five" + # } + # + # (key is the id) + end + + it 'it also orders by primary key' do + allow(subject).to receive(:params) + .and_return({ pagination: 'keyset', per_page: 2 }) + paginated_relation = subject.paginate(resource) + + expect(paginated_relation.order_values).to be_present + expect(paginated_relation.order_values.size).to eq(2) + expect(paginated_relation.order_values.first).to be_descending + expect(paginated_relation.order_values.first.expr.name).to eq :name + expect(paginated_relation.order_values.second).to be_descending + expect(paginated_relation.order_values.second.expr.name).to eq :id + end + + it 'it returns the right records (first page)' do + allow(subject).to receive(:params) + .and_return({ pagination: 'keyset', per_page: 2 }) + result = subject.paginate(resource) + + expect(result.first).to eq(projects[1]) + expect(result.second).to eq(projects[3]) + end + + it 'it returns the right records (second page)' do + allow(subject).to receive(:params) + .and_return({ pagination: 'keyset', ks_prev_id: projects[3].id, ks_prev_name: projects[3].name, per_page: 2 }) + result = subject.paginate(resource) + + expect(result.first).to eq(projects[2]) + expect(result.second).to eq(projects[6]) + end + + it 'it returns the right records (third page), note increased per_page' do + allow(subject).to receive(:params) + .and_return({ pagination: 'keyset', ks_prev_id: projects[6].id, ks_prev_name: projects[6].name, per_page: 5 }) + result = subject.paginate(resource) + + expect(result.size).to eq(3) + expect(result.first).to eq(projects[0]) + expect(result.second).to eq(projects[4]) + expect(result.last).to eq(projects[5]) + end + + it 'it returns the right link to the next page' do + allow(subject).to receive(:params) + .and_return({ pagination: 'keyset', ks_prev_id: projects[3].id, ks_prev_name: projects[3].name, per_page: 2 }) + expect_header('X-Per-Page', '2') + expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[6].id}&ks_prev_name=#{projects[6].name}&pagination=keyset&per_page=2") + + expect_header('Link', anything) do |_key, val| + expect(val).to include('rel="next"') + end + + subject.paginate(resource) + end + end + end + end + end + + describe '#paginate (default offset-based pagination)' do let(:value) { spy('return value') } before do @@ -146,14 +342,14 @@ describe API::Helpers::Pagination do end end end + end - def expect_header(*args, &block) - expect(subject).to receive(:header).with(*args, &block) - end + def expect_header(*args, &block) + expect(subject).to receive(:header).with(*args, &block) + end - def expect_message(method) - expect(subject).to receive(method) - .at_least(:once).and_return(value) - end + def expect_message(method) + expect(subject).to receive(method) + .at_least(:once).and_return(value) end end diff --git a/spec/lib/api/helpers/related_resources_helpers_spec.rb b/spec/lib/api/helpers/related_resources_helpers_spec.rb index b918301f1cb..66af7f81535 100644 --- a/spec/lib/api/helpers/related_resources_helpers_spec.rb +++ b/spec/lib/api/helpers/related_resources_helpers_spec.rb @@ -9,9 +9,9 @@ describe API::Helpers::RelatedResourcesHelpers do let(:path) { '/api/v4/awesome_endpoint' } subject(:url) { helpers.expose_url(path) } - def stub_default_url_options(protocol: 'http', host: 'example.com', port: nil) + def stub_default_url_options(protocol: 'http', host: 'example.com', port: nil, script_name: '') expect(Gitlab::Application.routes).to receive(:default_url_options) - .and_return(protocol: protocol, host: host, port: port) + .and_return(protocol: protocol, host: host, port: port, script_name: script_name) end it 'respects the protocol if it is HTTP' do @@ -37,5 +37,17 @@ describe API::Helpers::RelatedResourcesHelpers do is_expected.to start_with('http://example.com:8080/') end + + it 'includes the relative_url before the path if it is set' do + stub_default_url_options(script_name: '/gitlab') + + is_expected.to start_with('http://example.com/gitlab/api/v4') + end + + it 'includes the path after the host' do + stub_default_url_options + + is_expected.to start_with('http://example.com/api/v4') + end end end diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb index 84688845fa5..23c04a1a101 100644 --- a/spec/lib/backup/manager_spec.rb +++ b/spec/lib/backup/manager_spec.rb @@ -5,6 +5,8 @@ describe Backup::Manager do let(:progress) { StringIO.new } + subject { described_class.new(progress) } + before do allow(progress).to receive(:puts) allow(progress).to receive(:print) diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb index b1ea9c0b622..a44243ac82d 100644 --- a/spec/lib/backup/repository_spec.rb +++ b/spec/lib/backup/repository_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe Backup::Repository do let(:progress) { StringIO.new } let!(:project) { create(:project, :wiki_repo) } + subject { described_class.new(progress) } before do allow(progress).to receive(:puts) @@ -24,14 +25,12 @@ describe Backup::Repository do end it 'does not raise error' do - expect { described_class.new.dump }.not_to raise_error + expect { subject.dump }.not_to raise_error end end end describe '#restore' do - subject { described_class.new } - let(:timestamp) { Time.utc(2017, 3, 22) } let(:temp_dirs) do Gitlab.config.repositories.storages.map do |name, storage| @@ -49,14 +48,14 @@ describe Backup::Repository do describe 'command failure' do before do - allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1]) + allow_any_instance_of(Gitlab::Shell).to receive(:create_repository).and_return(false) end context 'hashed storage' do it 'shows the appropriate error' do subject.restore - expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} (#{project.disk_path}) - error") + expect(progress).to have_received(:puts).with("[Failed] restoring #{project.full_path} repository") end end @@ -66,33 +65,10 @@ describe Backup::Repository do it 'shows the appropriate error' do subject.restore - expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error") + expect(progress).to have_received(:puts).with("[Failed] restoring #{project.full_path} repository") end end end - - describe 'folders without permissions' do - before do - allow(FileUtils).to receive(:mv).and_raise(Errno::EACCES) - end - - it 'shows error message' do - expect(subject).to receive(:access_denied_error) - subject.restore - end - end - - describe 'folder that is a mountpoint' do - before do - allow(FileUtils).to receive(:mv).and_raise(Errno::EBUSY) - end - - it 'shows error message' do - expect(subject).to receive(:resource_busy_error).and_call_original - - expect { subject.restore }.to raise_error(/is a mountpoint/) - end - end end describe '#empty_repo?' do @@ -102,20 +78,20 @@ describe Backup::Repository do it 'invalidates the emptiness cache' do expect(wiki.repository).to receive(:expire_emptiness_caches).once - described_class.new.send(:empty_repo?, wiki) + subject.send(:empty_repo?, wiki) end context 'wiki repo has content' do let!(:wiki_page) { create(:wiki_page, wiki: wiki) } it 'returns true, regardless of bad cache value' do - expect(described_class.new.send(:empty_repo?, wiki)).to be(false) + expect(subject.send(:empty_repo?, wiki)).to be(false) end end context 'wiki repo does not have content' do it 'returns true, regardless of bad cache value' do - expect(described_class.new.send(:empty_repo?, wiki)).to be_truthy + expect(subject.send(:empty_repo?, wiki)).to be_truthy end end end diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb index ca76d6f0881..0e178b859c4 100644 --- a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb +++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb @@ -91,6 +91,12 @@ describe Banzai::Filter::GollumTagsFilter do expect(doc.at_css('a').text).to eq 'link-text' expect(doc.at_css('a')['href']).to eq expected_path end + + it "inside back ticks will be exempt from linkification" do + doc = filter('<code>[[link-in-backticks]]</code>', project_wiki: project_wiki) + + expect(doc.at_css('code').text).to eq '[[link-in-backticks]]' + end end context 'table of contents' do diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index 392905076dc..b30f3661e70 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -59,7 +59,7 @@ describe Banzai::Filter::LabelReferenceFilter do describe 'label span element' do it 'includes default classes' do doc = reference_filter("Label #{reference}") - expect(doc.css('a span').first.attr('class')).to eq 'label color-label has-tooltip' + expect(doc.css('a span').first.attr('class')).to eq 'badge color-label has-tooltip' end it 'includes a style attribute' do diff --git a/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb b/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb index 726a3c1c83a..43b68e69131 100644 --- a/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb +++ b/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb @@ -17,12 +17,8 @@ describe Gitlab::Auth::BlockedUserTracker do end context 'failed login due to blocked user' do - let(:env) do - { - 'warden.options' => { message: User::BLOCKED_MESSAGE }, - described_class::ACTIVE_RECORD_REQUEST_PARAMS => { 'user' => { 'login' => user.username } } - } - end + let(:base_env) { { 'warden.options' => { message: User::BLOCKED_MESSAGE } } } + let(:env) { base_env.merge(request_env) } subject { described_class.log_if_user_blocked(env) } @@ -30,23 +26,37 @@ describe Gitlab::Auth::BlockedUserTracker do expect_any_instance_of(SystemHooksService).to receive(:execute_hooks_for).with(user, :failed_login) end - it 'logs a blocked user' do - user.block! + context 'via GitLab login' do + let(:request_env) { { described_class::ACTIVE_RECORD_REQUEST_PARAMS => { 'user' => { 'login' => user.username } } } } - expect(subject).to be_truthy - end + it 'logs a blocked user' do + user.block! + + expect(subject).to be_truthy + end - it 'logs a blocked user by e-mail' do - user.block! - env[described_class::ACTIVE_RECORD_REQUEST_PARAMS]['user']['login'] = user.email + it 'logs a blocked user by e-mail' do + user.block! + env[described_class::ACTIVE_RECORD_REQUEST_PARAMS]['user']['login'] = user.email - expect(subject).to be_truthy + expect(subject).to be_truthy + end end - it 'logs a LDAP blocked user' do - user.ldap_block! + context 'via LDAP login' do + let(:request_env) { { described_class::ACTIVE_RECORD_REQUEST_PARAMS => { 'username' => user.username } } } + + it 'logs a blocked user' do + user.block! + + expect(subject).to be_truthy + end + + it 'logs a LDAP blocked user' do + user.ldap_block! - expect(subject).to be_truthy + expect(subject).to be_truthy + end end end end diff --git a/spec/lib/gitlab/auth/ldap/access_spec.rb b/spec/lib/gitlab/auth/ldap/access_spec.rb index 6b251d824f7..eff21985108 100644 --- a/spec/lib/gitlab/auth/ldap/access_spec.rb +++ b/spec/lib/gitlab/auth/ldap/access_spec.rb @@ -8,6 +8,7 @@ describe Gitlab::Auth::LDAP::Access do describe '.allowed?' do it 'updates the users `last_credential_check_at' do + allow(access).to receive(:update_user) expect(access).to receive(:allowed?) { true } expect(described_class).to receive(:open).and_yield(access) @@ -16,12 +17,21 @@ describe Gitlab::Auth::LDAP::Access do end end + describe '#find_ldap_user' do + it 'finds a user by dn first' do + expect(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(:ldap_user) + + access.find_ldap_user + end + end + describe '#allowed?' do subject { access.allowed? } context 'when the user cannot be found' do before do allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(nil) + allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_email).and_return(nil) end it { is_expected.to be_falsey } @@ -54,7 +64,7 @@ describe Gitlab::Auth::LDAP::Access do end end - context 'and has no disabled flag in active diretory' do + context 'and has no disabled flag in active directory' do before do allow(Gitlab::Auth::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(false) end @@ -100,6 +110,7 @@ describe Gitlab::Auth::LDAP::Access do context 'when user cannot be found' do before do allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(nil) + allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_email).and_return(nil) end it { is_expected.to be_falsey } diff --git a/spec/lib/gitlab/auth/ldap/config_spec.rb b/spec/lib/gitlab/auth/ldap/config_spec.rb index 82587e2ba55..d3ab599d5a0 100644 --- a/spec/lib/gitlab/auth/ldap/config_spec.rb +++ b/spec/lib/gitlab/auth/ldap/config_spec.rb @@ -23,7 +23,7 @@ describe Gitlab::Auth::LDAP::Config do end it 'raises an error if a unknown provider is used' do - expect { described_class.new 'unknown' }.to raise_error(RuntimeError) + expect { described_class.new 'unknown' }.to raise_error(described_class::InvalidProvider) end end @@ -370,4 +370,38 @@ describe Gitlab::Auth::LDAP::Config do }) end end + + describe '#base' do + context 'when the configured base is not normalized' do + it 'returns the normalized base' do + stub_ldap_config(options: { 'base' => 'DC=example, DC= com' }) + + expect(config.base).to eq('dc=example,dc=com') + end + end + + context 'when the configured base is normalized' do + it 'returns the base unaltered' do + stub_ldap_config(options: { 'base' => 'dc=example,dc=com' }) + + expect(config.base).to eq('dc=example,dc=com') + end + end + + context 'when the configured base is malformed' do + it 'returns the base unaltered' do + stub_ldap_config(options: { 'base' => 'invalid,dc=example,dc=com' }) + + expect(config.base).to eq('invalid,dc=example,dc=com') + end + end + + context 'when the configured base is blank' do + it 'returns the base unaltered' do + stub_ldap_config(options: { 'base' => '' }) + + expect(config.base).to eq('') + end + end + end end diff --git a/spec/lib/gitlab/auth/ldap/user_spec.rb b/spec/lib/gitlab/auth/ldap/user_spec.rb index 653c19942ea..44bb9d20e47 100644 --- a/spec/lib/gitlab/auth/ldap/user_spec.rb +++ b/spec/lib/gitlab/auth/ldap/user_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Gitlab::Auth::LDAP::User do + include LdapHelpers + let(:ldap_user) { described_class.new(auth_hash) } let(:gl_user) { ldap_user.gl_user } let(:info) do @@ -177,8 +179,7 @@ describe Gitlab::Auth::LDAP::User do describe 'blocking' do def configure_block(value) - allow_any_instance_of(Gitlab::Auth::LDAP::Config) - .to receive(:block_auto_created_users).and_return(value) + stub_ldap_config(block_auto_created_users: value) end context 'signup' do diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb index 08718c382b9..83d39b82068 100644 --- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb @@ -111,7 +111,15 @@ describe Gitlab::Ci::Config::Entry::Policy do context 'when specifying invalid variables expressions token' do let(:config) { { variables: ['$MY_VAR == 123'] } } - it 'reports an error about invalid statement' do + it 'reports an error about invalid expression' do + expect(entry.errors).to include /invalid expression syntax/ + end + end + + context 'when using invalid variables expressions regexp' do + let(:config) { { variables: ['$MY_VAR =~ /some ( thing/'] } } + + it 'reports an error about invalid expression' do expect(entry.errors).to include /invalid expression syntax/ end end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb new file mode 100644 index 00000000000..49e5af52f4d --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb @@ -0,0 +1,80 @@ +require 'fast_spec_helper' +require_dependency 're2' + +describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do + let(:left) { double('left') } + let(:right) { double('right') } + + describe '.build' do + it 'creates a new instance of the token' do + expect(described_class.build('=~', left, right)) + .to be_a(described_class) + end + end + + describe '.type' do + it 'is an operator' do + expect(described_class.type).to eq :operator + end + end + + describe '#evaluate' do + it 'returns false when left and right do not match' do + allow(left).to receive(:evaluate).and_return('my-string') + allow(right).to receive(:evaluate) + .and_return(Gitlab::UntrustedRegexp.new('something')) + + operator = described_class.new(left, right) + + expect(operator.evaluate).to eq false + end + + it 'returns true when left and right match' do + allow(left).to receive(:evaluate).and_return('my-awesome-string') + allow(right).to receive(:evaluate) + .and_return(Gitlab::UntrustedRegexp.new('awesome.string$')) + + operator = described_class.new(left, right) + + expect(operator.evaluate).to eq true + end + + it 'supports matching against a nil value' do + allow(left).to receive(:evaluate).and_return(nil) + allow(right).to receive(:evaluate) + .and_return(Gitlab::UntrustedRegexp.new('pattern')) + + operator = described_class.new(left, right) + + expect(operator.evaluate).to eq false + end + + it 'supports multiline strings' do + allow(left).to receive(:evaluate).and_return <<~TEXT + My awesome contents + + My-text-string! + TEXT + + allow(right).to receive(:evaluate) + .and_return(Gitlab::UntrustedRegexp.new('text-string')) + + operator = described_class.new(left, right) + + expect(operator.evaluate).to eq true + end + + it 'supports regexp flags' do + allow(left).to receive(:evaluate).and_return <<~TEXT + My AWESOME content + TEXT + + allow(right).to receive(:evaluate) + .and_return(Gitlab::UntrustedRegexp.new('(?i)awesome')) + + operator = described_class.new(left, right) + + expect(operator.evaluate).to eq true + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb new file mode 100644 index 00000000000..3ebc2e94727 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb @@ -0,0 +1,96 @@ +require 'fast_spec_helper' + +describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do + describe '.build' do + it 'creates a new instance of the token' do + expect(described_class.build('/.*/')) + .to be_a(described_class) + end + + it 'raises an error if pattern is invalid' do + expect { described_class.build('/ some ( thin/i') } + .to raise_error(Gitlab::Ci::Pipeline::Expression::Lexer::SyntaxError) + end + end + + describe '.type' do + it 'is a value lexeme' do + expect(described_class.type).to eq :value + end + end + + describe '.scan' do + it 'correctly identifies a pattern token' do + scanner = StringScanner.new('/pattern/') + + token = described_class.scan(scanner) + + expect(token).not_to be_nil + expect(token.build.evaluate) + .to eq Gitlab::UntrustedRegexp.new('pattern') + end + + it 'is a greedy scanner for regexp boundaries' do + scanner = StringScanner.new('/some .* / pattern/') + + token = described_class.scan(scanner) + + expect(token).not_to be_nil + expect(token.build.evaluate) + .to eq Gitlab::UntrustedRegexp.new('some .* / pattern') + end + + it 'does not allow to use an empty pattern' do + scanner = StringScanner.new(%(//)) + + token = described_class.scan(scanner) + + expect(token).to be_nil + end + + it 'support single flag' do + scanner = StringScanner.new('/pattern/i') + + token = described_class.scan(scanner) + + expect(token).not_to be_nil + expect(token.build.evaluate) + .to eq Gitlab::UntrustedRegexp.new('(?i)pattern') + end + + it 'support multiple flags' do + scanner = StringScanner.new('/pattern/im') + + token = described_class.scan(scanner) + + expect(token).not_to be_nil + expect(token.build.evaluate) + .to eq Gitlab::UntrustedRegexp.new('(?im)pattern') + end + + it 'does not support arbitrary flags' do + scanner = StringScanner.new('/pattern/x') + + token = described_class.scan(scanner) + + expect(token).to be_nil + end + end + + describe '#evaluate' do + it 'returns a regular expression' do + regexp = described_class.new('/abc/') + + expect(regexp.evaluate).to eq Gitlab::UntrustedRegexp.new('abc') + end + + it 'raises error if evaluated regexp is not valid' do + allow(Gitlab::UntrustedRegexp).to receive(:valid?).and_return(true) + + regexp = described_class.new('/invalid ( .*/') + + expect { regexp.evaluate } + .to raise_error(Gitlab::Ci::Pipeline::Expression::RuntimeError) + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb index 230ceeb07f8..3f11b3f7673 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do end describe '#tokens' do - it 'tokenss single value' do + it 'returns single value' do tokens = described_class.new('$VARIABLE').tokens expect(tokens).to be_one @@ -20,14 +20,14 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do expect(tokens).to all(be_an_instance_of(token_class)) end - it 'tokenss multiple values of the same token' do + it 'returns multiple values of the same token' do tokens = described_class.new("$VARIABLE1 $VARIABLE2").tokens expect(tokens.size).to eq 2 expect(tokens).to all(be_an_instance_of(token_class)) end - it 'tokenss multiple values with different tokens' do + it 'returns multiple values with different tokens' do tokens = described_class.new('$VARIABLE "text" "value"').tokens expect(tokens.size).to eq 3 @@ -36,7 +36,7 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do expect(tokens.third.value).to eq '"value"' end - it 'tokenss tokens and operators' do + it 'returns tokens and operators' do tokens = described_class.new('$VARIABLE == "text"').tokens expect(tokens.size).to eq 3 diff --git a/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb index e8e6f585310..2b78b1dd4a7 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require 'fast_spec_helper' describe Gitlab::Ci::Pipeline::Expression::Parser do describe '#tree' do diff --git a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb index 6685bf5385b..11e73294f18 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb @@ -1,4 +1,5 @@ -require 'spec_helper' +require 'fast_spec_helper' +require 'rspec-parameterized' describe Gitlab::Ci::Pipeline::Expression::Statement do subject do @@ -36,7 +37,7 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do '== "123"', # invalid left side '"some string"', # only string provided '$VAR ==', # invalid right side - '12345', # unknown syntax + 'null', # missing lexemes '' # empty statement ] @@ -44,7 +45,7 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do context "when expression grammar is #{syntax.inspect}" do let(:text) { syntax } - it 'aises a statement error exception' do + it 'raises a statement error exception' do expect { subject.parse_tree } .to raise_error described_class::StatementError end @@ -82,48 +83,66 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do end describe '#evaluate' do - statements = [ - ['$PRESENT_VARIABLE == "my variable"', true], - ["$PRESENT_VARIABLE == 'my variable'", true], - ['"my variable" == $PRESENT_VARIABLE', true], - ['$PRESENT_VARIABLE == null', false], - ['$EMPTY_VARIABLE == null', false], - ['"" == $EMPTY_VARIABLE', true], - ['$EMPTY_VARIABLE', ''], - ['$UNDEFINED_VARIABLE == null', true], - ['null == $UNDEFINED_VARIABLE', true], - ['$PRESENT_VARIABLE', 'my variable'], - ['$UNDEFINED_VARIABLE', nil] - ] - - statements.each do |expression, value| - context "when using expression `#{expression}`" do - let(:text) { expression } - - it "evaluates to `#{value.inspect}`" do - expect(subject.evaluate).to eq value - end + using RSpec::Parameterized::TableSyntax + + where(:expression, :value) do + '$PRESENT_VARIABLE == "my variable"' | true + '"my variable" == $PRESENT_VARIABLE' | true + '$PRESENT_VARIABLE == null' | false + '$EMPTY_VARIABLE == null' | false + '"" == $EMPTY_VARIABLE' | true + '$EMPTY_VARIABLE' | '' + '$UNDEFINED_VARIABLE == null' | true + 'null == $UNDEFINED_VARIABLE' | true + '$PRESENT_VARIABLE' | 'my variable' + '$UNDEFINED_VARIABLE' | nil + "$PRESENT_VARIABLE =~ /var.*e$/" | true + "$PRESENT_VARIABLE =~ /^var.*/" | false + "$EMPTY_VARIABLE =~ /var.*/" | false + "$UNDEFINED_VARIABLE =~ /var.*/" | false + "$PRESENT_VARIABLE =~ /VAR.*/i" | true + end + + with_them do + let(:text) { expression } + + it "evaluates to `#{params[:value].inspect}`" do + expect(subject.evaluate).to eq value end end end describe '#truthful?' do - statements = [ - ['$PRESENT_VARIABLE == "my variable"', true], - ["$PRESENT_VARIABLE == 'no match'", false], - ['$UNDEFINED_VARIABLE == null', true], - ['$PRESENT_VARIABLE', true], - ['$UNDEFINED_VARIABLE', false], - ['$EMPTY_VARIABLE', false] - ] - - statements.each do |expression, value| - context "when using expression `#{expression}`" do - let(:text) { expression } - - it "returns `#{value.inspect}`" do - expect(subject.truthful?).to eq value - end + using RSpec::Parameterized::TableSyntax + + where(:expression, :value) do + '$PRESENT_VARIABLE == "my variable"' | true + "$PRESENT_VARIABLE == 'no match'" | false + '$UNDEFINED_VARIABLE == null' | true + '$PRESENT_VARIABLE' | true + '$UNDEFINED_VARIABLE' | false + '$EMPTY_VARIABLE' | false + '$INVALID = 1' | false + "$PRESENT_VARIABLE =~ /var.*/" | true + "$UNDEFINED_VARIABLE =~ /var.*/" | false + end + + with_them do + let(:text) { expression } + + it "returns `#{params[:value].inspect}`" do + expect(subject.truthful?).to eq value + end + end + + context 'when evaluating expression raises an error' do + let(:text) { '$PRESENT_VARIABLE' } + + it 'returns false' do + allow(subject).to receive(:evaluate) + .and_raise(described_class::StatementError) + + expect(subject.truthful?).to be_falsey end end end diff --git a/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb index 6d7453f0de5..cedfe270f9d 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require 'fast_spec_helper' describe Gitlab::Ci::Pipeline::Expression::Token do let(:value) { '$VARIABLE' } diff --git a/spec/lib/gitlab/ci/pipeline/preloader_spec.rb b/spec/lib/gitlab/ci/pipeline/preloader_spec.rb new file mode 100644 index 00000000000..477c7477df0 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/preloader_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Pipeline::Preloader do + describe '.preload' do + it 'preloads the author of every pipeline commit' do + commit = double(:commit) + pipeline = double(:pipeline, commit: commit) + + expect(commit) + .to receive(:lazy_author) + + expect(pipeline) + .to receive(:number_of_warnings) + + described_class.preload([pipeline]) + end + end +end diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb index 4ddcbd7eb66..19028495f52 100644 --- a/spec/lib/gitlab/current_settings_spec.rb +++ b/spec/lib/gitlab/current_settings_spec.rb @@ -1,17 +1,15 @@ require 'spec_helper' describe Gitlab::CurrentSettings do - include StubENV - before do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') end - describe '#current_application_settings' do + describe '#current_application_settings', :use_clean_rails_memory_store_caching do it 'allows keys to be called directly' do db_settings = create(:application_setting, - home_page_url: 'http://mydomain.com', - signup_enabled: false) + home_page_url: 'http://mydomain.com', + signup_enabled: false) expect(described_class.home_page_url).to eq(db_settings.home_page_url) expect(described_class.signup_enabled?).to be_falsey @@ -19,46 +17,54 @@ describe Gitlab::CurrentSettings do expect(described_class.metrics_sample_interval).to be(15) end - context 'with DB available' do + context 'when ENV["IN_MEMORY_APPLICATION_SETTINGS"] is true' do before do - # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(true)` causes issues - # during the initialization phase of the test suite, so instead let's mock the internals of it - allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true) - allow(ActiveRecord::Base.connection).to receive(:table_exists?).and_call_original - allow(ActiveRecord::Base.connection).to receive(:table_exists?).with('application_settings').and_return(true) + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'true') end - it 'attempts to use cached values first' do - expect(ApplicationSetting).to receive(:cached) + it 'returns an in-memory ApplicationSetting object' do + expect(ApplicationSetting).not_to receive(:current) expect(described_class.current_application_settings).to be_a(ApplicationSetting) + expect(described_class.current_application_settings).not_to be_persisted end + end - it 'falls back to DB if Redis returns an empty value' do - expect(ApplicationSetting).to receive(:cached).and_return(nil) - expect(ApplicationSetting).to receive(:last).and_call_original.twice - - expect(described_class.current_application_settings).to be_a(ApplicationSetting) + context 'with DB unavailable' do + before do + # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(false)` causes issues + # during the initialization phase of the test suite, so instead let's mock the internals of it + allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false) end - it 'falls back to DB if Redis fails' do - db_settings = ApplicationSetting.create!(ApplicationSetting.defaults) + it 'returns an in-memory ApplicationSetting object' do + expect(ApplicationSetting).not_to receive(:current) - expect(ApplicationSetting).to receive(:cached).and_raise(::Redis::BaseError) - expect(Rails.cache).to receive(:fetch).with(ApplicationSetting::CACHE_KEY).and_raise(Redis::BaseError) + expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings) + end + end - expect(described_class.current_application_settings).to eq(db_settings) + context 'with DB available' do + # This method returns the ::ApplicationSetting.defaults hash + # but with respect of custom attribute accessors of ApplicationSetting model + def settings_from_defaults + ar_wrapped_defaults = ::ApplicationSetting.build_from_defaults.attributes + ar_wrapped_defaults.slice(*::ApplicationSetting.defaults.keys) end - it 'creates default ApplicationSettings if none are present' do - expect(ApplicationSetting).to receive(:cached).and_raise(::Redis::BaseError) - expect(Rails.cache).to receive(:fetch).with(ApplicationSetting::CACHE_KEY).and_raise(Redis::BaseError) + before do + # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(true)` causes issues + # during the initialization phase of the test suite, so instead let's mock the internals of it + allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true) + allow(ActiveRecord::Base.connection).to receive(:cached_table_exists?).with('application_settings').and_return(true) + end + it 'creates default ApplicationSettings if none are present' do settings = described_class.current_application_settings expect(settings).to be_a(ApplicationSetting) expect(settings).to be_persisted - expect(settings).to have_attributes(ApplicationSetting.defaults) + expect(settings).to have_attributes(settings_from_defaults) end context 'with migrations pending' do @@ -69,7 +75,7 @@ describe Gitlab::CurrentSettings do it 'returns an in-memory ApplicationSetting object' do settings = described_class.current_application_settings - expect(settings).to be_a(OpenStruct) + expect(settings).to be_a(Gitlab::FakeApplicationSettings) expect(settings.sign_in_enabled?).to eq(settings.sign_in_enabled) expect(settings.sign_up_enabled?).to eq(settings.sign_up_enabled) end @@ -81,7 +87,7 @@ describe Gitlab::CurrentSettings do settings = described_class.current_application_settings app_defaults = ApplicationSetting.last - expect(settings).to be_a(OpenStruct) + expect(settings).to be_a(Gitlab::FakeApplicationSettings) expect(settings.home_page_url).to eq(db_settings.home_page_url) expect(settings.signup_enabled?).to be_falsey expect(settings.signup_enabled).to be_falsey @@ -91,34 +97,29 @@ describe Gitlab::CurrentSettings do settings.each { |key, _| expect(settings[key]).to eq(app_defaults[key]) } end end - end - - context 'with DB unavailable' do - before do - # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(false)` causes issues - # during the initialization phase of the test suite, so instead let's mock the internals of it - allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false) - end - it 'returns an in-memory ApplicationSetting object' do - expect(ApplicationSetting).not_to receive(:current) - expect(ApplicationSetting).not_to receive(:last) + context 'when ApplicationSettings.current is present' do + it 'returns the existing application settings' do + expect(ApplicationSetting).to receive(:current).and_return(:current_settings) - expect(described_class.current_application_settings).to be_a(OpenStruct) + expect(described_class.current_application_settings).to eq(:current_settings) + end end - end - context 'when ENV["IN_MEMORY_APPLICATION_SETTINGS"] is true' do - before do - stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'true') + context 'when the application_settings table does not exists' do + it 'returns an in-memory ApplicationSetting object' do + expect(ApplicationSetting).to receive(:create_from_defaults).and_raise(ActiveRecord::StatementInvalid) + + expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings) + end end - it 'returns an in-memory ApplicationSetting object' do - expect(ApplicationSetting).not_to receive(:current) - expect(ApplicationSetting).not_to receive(:last) + context 'when the application_settings table is not fully migrated' do + it 'returns an in-memory ApplicationSetting object' do + expect(ApplicationSetting).to receive(:create_from_defaults).and_raise(ActiveRecord::UnknownAttributeError) - expect(described_class.current_application_settings).to be_a(ApplicationSetting) - expect(described_class.current_application_settings).not_to be_persisted + expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings) + end end end end diff --git a/spec/lib/gitlab/database/count_spec.rb b/spec/lib/gitlab/database/count_spec.rb new file mode 100644 index 00000000000..9d9caaabe16 --- /dev/null +++ b/spec/lib/gitlab/database/count_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe Gitlab::Database::Count do + before do + create_list(:project, 3) + end + + describe '.execute_estimate_if_updated_recently', :postgresql do + context 'when reltuples have not been updated' do + before do + expect(described_class).to receive(:reltuples_updated_recently?).and_return(false) + end + + it 'returns nil' do + expect(described_class.execute_estimate_if_updated_recently(Project)).to be nil + end + end + + context 'when reltuples have been updated' do + before do + ActiveRecord::Base.connection.execute('ANALYZE projects') + end + + it 'calls postgresql_estimate_query' do + expect(described_class).to receive(:postgresql_estimate_query).with(Project).and_call_original + expect(described_class.execute_estimate_if_updated_recently(Project)).to eq(3) + end + end + end + + describe '.approximate_count' do + context 'when reltuples have not been updated' do + it 'counts all projects the normal way' do + allow(described_class).to receive(:reltuples_updated_recently?).and_return(false) + + expect(Project).to receive(:count).and_call_original + expect(described_class.approximate_count(Project)).to eq(3) + end + end + + context 'no permission' do + it 'falls back to standard query' do + allow(described_class).to receive(:reltuples_updated_recently?).and_raise(PG::InsufficientPrivilege) + + expect(Project).to receive(:count).and_call_original + expect(described_class.approximate_count(Project)).to eq(3) + end + end + + describe 'when reltuples have been updated', :postgresql do + before do + ActiveRecord::Base.connection.execute('ANALYZE projects') + end + + it 'counts all projects in the fast way' do + expect(described_class).to receive(:postgresql_estimate_query).with(Project).and_call_original + + expect(described_class.approximate_count(Project)).to eq(3) + end + end + end +end diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 1fe1d3926ad..8ac36ae8bab 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -32,6 +32,12 @@ describe Gitlab::Database do end describe '.version' do + around do |example| + described_class.instance_variable_set(:@version, nil) + example.run + described_class.instance_variable_set(:@version, nil) + end + context "on mysql" do it "extracts the version number" do allow(described_class).to receive(:database_version) @@ -49,6 +55,14 @@ describe Gitlab::Database do expect(described_class.version).to eq '9.4.4' end end + + it 'memoizes the result' do + count = ActiveRecord::QueryRecorder + .new { 2.times { described_class.version } } + .count + + expect(count).to eq(1) + end end describe '.join_lateral_supported?' do diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb index 452249210b0..154ab4b3856 100644 --- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb @@ -46,6 +46,20 @@ describe Gitlab::Email::Handler::CreateIssueHandler do expect(issue.description).to eq('') end end + + context "when there are quotes in email" do + let(:email_raw) { fixture_file("emails/valid_new_issue_with_quote.eml") } + + it "creates a new issue" do + expect { receiver.execute }.to change { project.issues.count }.by(1) + issue = project.issues.last + + expect(issue.author).to eq(user) + expect(issue.title).to eq('New Issue by email') + expect(issue.description).to include('reply by email') + expect(issue.description).to include('> this is a quote') + end + end end context "something is wrong" do diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb index e21a998adfe..0989188f7ee 100644 --- a/spec/lib/gitlab/email/reply_parser_spec.rb +++ b/spec/lib/gitlab/email/reply_parser_spec.rb @@ -3,8 +3,8 @@ require "spec_helper" # Inspired in great part by Discourse's Email::Receiver describe Gitlab::Email::ReplyParser do describe '#execute' do - def test_parse_body(mail_string) - described_class.new(Mail::Message.new(mail_string)).execute + def test_parse_body(mail_string, params = {}) + described_class.new(Mail::Message.new(mail_string), params).execute end it "returns an empty string if the message is blank" do @@ -212,5 +212,19 @@ describe Gitlab::Email::ReplyParser do it "does not wrap links with no href in unnecessary brackets" do expect(test_parse_body(fixture_file("emails/html_empty_link.eml"))).to eq("no brackets!") end + + it "does not trim reply if trim_reply option is false" do + expect(test_parse_body(fixture_file("emails/valid_new_issue_with_quote.eml"), { trim_reply: false })) + .to eq( + <<-BODY.strip_heredoc.chomp + The reply by email functionality should be extended to allow creating a new issue by email. + even when the email is forwarded to the project which may include lines that begin with ">" + + there should be a quote below this line: + + > this is a quote + BODY + ) + end end end diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index a05feaac1ca..08c6d1e55e9 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -66,7 +66,8 @@ describe Gitlab::Git::Commit, seed_helper: true do describe "Commit info from gitaly commit" do let(:subject) { "My commit".force_encoding('ASCII-8BIT') } let(:body) { subject + "My body".force_encoding('ASCII-8BIT') } - let(:gitaly_commit) { build(:gitaly_commit, subject: subject, body: body) } + let(:body_size) { body.length } + let(:gitaly_commit) { build(:gitaly_commit, subject: subject, body: body, body_size: body_size) } let(:id) { gitaly_commit.id } let(:committer) { gitaly_commit.committer } let(:author) { gitaly_commit.author } @@ -83,10 +84,30 @@ describe Gitlab::Git::Commit, seed_helper: true do it { expect(commit.committer_email).to eq(committer.email) } it { expect(commit.parent_ids).to eq(gitaly_commit.parent_ids) } - context 'no body' do + context 'body_size != body.size' do let(:body) { "".force_encoding('ASCII-8BIT') } - it { expect(commit.safe_message).to eq(subject) } + context 'zero body_size' do + it { expect(commit.safe_message).to eq(subject) } + end + + context 'body_size less than threshold' do + let(:body_size) { 123 } + + it 'fetches commit message seperately' do + expect(described_class).to receive(:get_message).with(repository, id) + + commit.safe_message + end + end + + context 'body_size greater than threshold' do + let(:body_size) { described_class::MAX_COMMIT_MESSAGE_DISPLAY_SIZE + 1 } + + it 'returns the suject plus a notice about message size' do + expect(commit.safe_message).to eq("My commit\n\n--commit message is too big") + end + end end end @@ -554,24 +575,10 @@ describe Gitlab::Git::Commit, seed_helper: true do it_should_behave_like '#stats' end - describe '#to_diff' do - subject { commit.to_diff } - - it { is_expected.not_to include "From #{SeedRepo::Commit::ID}" } - it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'} - end - describe '#has_zero_stats?' do it { expect(commit.has_zero_stats?).to eq(false) } end - describe '#to_patch' do - subject { commit.to_patch } - - it { is_expected.to include "From #{SeedRepo::Commit::ID}" } - it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'} - end - describe '#to_hash' do let(:hash) { commit.to_hash } subject { hash } @@ -603,6 +610,35 @@ describe Gitlab::Git::Commit, seed_helper: true do it { is_expected.not_to include("feature") } end + describe '.get_message' do + let(:commit_ids) { %w[6d394385cf567f80a8fd85055db1ab4c5295806f cfe32cf61b73a0d5e9f13e774abde7ff789b1660] } + + subject do + commit_ids.map { |id| described_class.get_message(repository, id) } + end + + shared_examples 'getting commit messages' do + it 'gets commit messages' do + expect(subject).to contain_exactly( + "Added contributing guide\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", + "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n" + ) + end + end + + context 'when Gitaly commit_messages feature is enabled' do + it_behaves_like 'getting commit messages' + + it 'gets messages in one batch', :request_store do + expect { subject.map(&:itself) }.to change { Gitlab::GitalyClient.get_request_count }.by(1) + end + end + + context 'when Gitaly commit_messages feature is disabled', :disable_gitaly do + it_behaves_like 'getting commit messages' + end + end + def sample_commit_hash { author_email: "dmitriy.zaporozhets@gmail.com", diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index cce84276fe3..af6a486ab20 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -249,10 +249,6 @@ describe Gitlab::Git::Repository, seed_helper: true do subject(:metadata) { repository.archive_metadata(ref, storage_path, format, append_sha: append_sha) } - it 'sets RepoPath to the repository path' do - expect(metadata['RepoPath']).to eq(repository.path) - end - it 'sets CommitId to the commit SHA' do expect(metadata['CommitId']).to eq(SeedRepo::LastCommit::ID) end @@ -600,6 +596,10 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + it 'does not fail when deleting an empty list of refs' do + expect { repo.delete_refs(*[]) }.not_to raise_error + end + it 'raises an error if it failed' do expect { repo.delete_refs('refs\heads\fix') }.to raise_error(Gitlab::Git::Repository::GitError) end @@ -615,32 +615,22 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#branch_names_contains_sha' do - shared_examples 'returning the right branches' do - let(:head_id) { repository.rugged.head.target.oid } - let(:new_branch) { head_id } - let(:utf8_branch) { 'branch-é' } + let(:head_id) { repository.rugged.head.target.oid } + let(:new_branch) { head_id } + let(:utf8_branch) { 'branch-é' } - before do - repository.create_branch(new_branch, 'master') - repository.create_branch(utf8_branch, 'master') - end - - after do - repository.delete_branch(new_branch) - repository.delete_branch(utf8_branch) - end - - it 'displays that branch' do - expect(repository.branch_names_contains_sha(head_id)).to include('master', new_branch, utf8_branch) - end + before do + repository.create_branch(new_branch, 'master') + repository.create_branch(utf8_branch, 'master') end - context 'when Gitaly is enabled' do - it_behaves_like 'returning the right branches' + after do + repository.delete_branch(new_branch) + repository.delete_branch(utf8_branch) end - context 'when Gitaly is disabled', :disable_gitaly do - it_behaves_like 'returning the right branches' + it 'displays that branch' do + expect(repository.branch_names_contains_sha(head_id)).to include('master', new_branch, utf8_branch) end end @@ -2257,66 +2247,42 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#checksum' do - shared_examples 'calculating checksum' do - it 'calculates the checksum for non-empty repo' do - expect(repository.checksum).to eq '54f21be4c32c02f6788d72207fa03ad3bce725e4' - end - - it 'returns 0000000000000000000000000000000000000000 for an empty repo' do - FileUtils.rm_rf(File.join(storage_path, 'empty-repo.git')) - - system(git_env, *%W(#{Gitlab.config.git.bin_path} init --bare empty-repo.git), - chdir: storage_path, - out: '/dev/null', - err: '/dev/null') - - empty_repo = described_class.new('default', 'empty-repo.git', '') + it 'calculates the checksum for non-empty repo' do + expect(repository.checksum).to eq '54f21be4c32c02f6788d72207fa03ad3bce725e4' + end - expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000' - end + it 'returns 0000000000000000000000000000000000000000 for an empty repo' do + FileUtils.rm_rf(File.join(storage_path, 'empty-repo.git')) - it 'raises Gitlab::Git::Repository::InvalidRepository error for non-valid git repo' do - FileUtils.rm_rf(File.join(storage_path, 'non-valid.git')) + system(git_env, *%W(#{Gitlab.config.git.bin_path} init --bare empty-repo.git), + chdir: storage_path, + out: '/dev/null', + err: '/dev/null') - system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} non-valid.git), - chdir: SEED_STORAGE_PATH, - out: '/dev/null', - err: '/dev/null') + empty_repo = described_class.new('default', 'empty-repo.git', '') - File.truncate(File.join(storage_path, 'non-valid.git/HEAD'), 0) + expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000' + end - non_valid = described_class.new('default', 'non-valid.git', '') + it 'raises Gitlab::Git::Repository::InvalidRepository error for non-valid git repo' do + FileUtils.rm_rf(File.join(storage_path, 'non-valid.git')) - expect { non_valid.checksum }.to raise_error(Gitlab::Git::Repository::InvalidRepository) - end + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} non-valid.git), + chdir: SEED_STORAGE_PATH, + out: '/dev/null', + err: '/dev/null') - it 'raises Gitlab::Git::Repository::NoRepository error when there is no repo' do - broken_repo = described_class.new('default', 'a/path.git', '') + File.truncate(File.join(storage_path, 'non-valid.git/HEAD'), 0) - expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository) - end - end + non_valid = described_class.new('default', 'non-valid.git', '') - context 'when calculate_checksum Gitaly feature is enabled' do - it_behaves_like 'calculating checksum' + expect { non_valid.checksum }.to raise_error(Gitlab::Git::Repository::InvalidRepository) end - context 'when calculate_checksum Gitaly feature is disabled', :disable_gitaly do - it_behaves_like 'calculating checksum' - - describe 'when storage is broken', :broken_storage do - it 'raises a storage exception when storage is not available' do - broken_repo = described_class.new('broken', 'a/path.git', '') - - expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Storage::Inaccessible) - end - end - - it "raises a Gitlab::Git::Repository::Failure error if the `popen` call to git returns a non-zero exit code" do - allow(repository).to receive(:popen).and_return(['output', nil]) + it 'raises Gitlab::Git::Repository::NoRepository error when there is no repo' do + broken_repo = described_class.new('default', 'a/path.git', '') - expect { repository.checksum }.to raise_error Gitlab::Git::Repository::ChecksumError - end + expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository) end end diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb index 6c4f538bf01..be2f5bfb819 100644 --- a/spec/lib/gitlab/git/tag_spec.rb +++ b/spec/lib/gitlab/git/tag_spec.rb @@ -32,4 +32,56 @@ describe Gitlab::Git::Tag, seed_helper: true do context 'when Gitaly tags feature is disabled', :skip_gitaly_mock do it_behaves_like 'Gitlab::Git::Repository#tags' end + + describe '.get_message' do + let(:tag_ids) { %w[f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8 8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b] } + + subject do + tag_ids.map { |id| described_class.get_message(repository, id) } + end + + shared_examples 'getting tag messages' do + it 'gets tag messages' do + expect(subject[0]).to eq("Release\n") + expect(subject[1]).to eq("Version 1.1.0\n") + end + end + + context 'when Gitaly tag_messages feature is enabled' do + it_behaves_like 'getting tag messages' + + it 'gets messages in one batch', :request_store do + expect { subject.map(&:itself) }.to change { Gitlab::GitalyClient.get_request_count }.by(1) + end + end + + context 'when Gitaly tag_messages feature is disabled', :disable_gitaly do + it_behaves_like 'getting tag messages' + end + end + + describe 'tag into from Gitaly tag' do + context 'message_size != message.size' do + let(:gitaly_tag) { build(:gitaly_tag, message: ''.b, message_size: message_size) } + let(:tag) { described_class.new(repository, gitaly_tag) } + + context 'message_size less than threshold' do + let(:message_size) { 123 } + + it 'fetches tag message seperately' do + expect(described_class).to receive(:get_message).with(repository, gitaly_tag.id) + + tag.message + end + end + + context 'message_size greater than threshold' do + let(:message_size) { described_class::MAX_TAG_MESSAGE_DISPLAY_SIZE + 1 } + + it 'returns a notice about message size' do + expect(tag.message).to eq("--tag message is too big") + 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 ad76adcc2e5..8b46b04b8b5 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -185,6 +185,7 @@ project: - cluster - clusters - cluster_project +- cluster_ingresses - creator - group - namespace diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 6d63749296e..4f64f2bd6b4 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -6760,26 +6760,6 @@ "wiki_page_events": true }, { - "id": 92, - "title": "Gemnasium", - "project_id": 5, - "created_at": "2016-06-14T15:01:51.202Z", - "updated_at": "2016-06-14T15:01:51.202Z", - "active": false, - "properties": {}, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "job_events": true, - "type": "GemnasiumService", - "category": "common", - "default": false, - "wiki_page_events": true - }, - { "id": 91, "title": "Flowdock", "project_id": 5, diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb index ad087f42e06..4c0c3fcbcc7 100644 --- a/spec/lib/gitlab/incoming_email_spec.rb +++ b/spec/lib/gitlab/incoming_email_spec.rb @@ -83,6 +83,10 @@ describe Gitlab::IncomingEmail do it "returns reply key" do expect(described_class.key_from_address("replies+key@example.com")).to eq("key") end + + it 'does not match emails with extra bits' do + expect(described_class.key_from_address('somereplies+somekey@example.com.someotherdomain.com')).to be nil + end end context 'self.key_from_fallback_message_id' do diff --git a/spec/lib/gitlab/metrics/web_transaction_spec.rb b/spec/lib/gitlab/metrics/web_transaction_spec.rb index 1d162f53a13..6eb0600f49e 100644 --- a/spec/lib/gitlab/metrics/web_transaction_spec.rb +++ b/spec/lib/gitlab/metrics/web_transaction_spec.rb @@ -180,11 +180,11 @@ describe Gitlab::Metrics::WebTransaction do end context 'when request goes to ActionController' do - let(:content_type) { 'text/html' } + let(:request) { double(:request, format: double(:format, ref: :html)) } before do klass = double(:klass, name: 'TestController') - controller = double(:controller, class: klass, action_name: 'show', content_type: content_type) + controller = double(:controller, class: klass, action_name: 'show', request: request) env['action_controller.instance'] = controller end @@ -195,7 +195,7 @@ describe Gitlab::Metrics::WebTransaction do end context 'when the response content type is not :html' do - let(:content_type) { 'application/json' } + let(:request) { double(:request, format: double(:format, ref: :json)) } it 'appends the mime type to the transaction action' do expect(transaction.labels).to eq({ controller: 'TestController', action: 'show.json' }) diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index a34b7d9905a..e3f705d2299 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -106,6 +106,18 @@ describe Gitlab::ProjectSearchResults do end end + context 'when the matching content contains multiple null bytes' do + let(:search_result) { "master:testdata/foo.txt\x001\x00blah\x001\x00foo" } + + it 'returns a valid FoundBlob' do + expect(subject.filename).to eq('testdata/foo.txt') + expect(subject.basename).to eq('testdata/foo') + expect(subject.ref).to eq('master') + expect(subject.startline).to eq(1) + expect(subject.data).to eq("blah\x001\x00foo") + end + end + context 'when the search result ends with an empty line' do let(:results) { project.repository.search_files_by_content('Role models', 'master') } diff --git a/spec/lib/gitlab/untrusted_regexp_spec.rb b/spec/lib/gitlab/untrusted_regexp_spec.rb index bed58d407ef..0a6ac0aa294 100644 --- a/spec/lib/gitlab/untrusted_regexp_spec.rb +++ b/spec/lib/gitlab/untrusted_regexp_spec.rb @@ -1,6 +1,49 @@ -require 'spec_helper' +require 'fast_spec_helper' +require 'support/shared_examples/malicious_regexp_shared_examples' describe Gitlab::UntrustedRegexp do + describe '.valid?' do + it 'returns true if regexp is valid' do + expect(described_class.valid?('/some ( thing/')) + .to be false + end + + it 'returns true if regexp is invalid' do + expect(described_class.valid?('/some .* thing/')) + .to be true + end + end + + describe '.fabricate' do + context 'when regexp is using /regexp/ scheme with flags' do + it 'fabricates regexp with a single flag' do + regexp = described_class.fabricate('/something/i') + + expect(regexp).to eq described_class.new('(?i)something') + expect(regexp.scan('SOMETHING')).to be_one + end + + it 'fabricates regexp with multiple flags' do + regexp = described_class.fabricate('/something/im') + + expect(regexp).to eq described_class.new('(?im)something') + end + + it 'fabricates regexp without flags' do + regexp = described_class.fabricate('/something/') + + expect(regexp).to eq described_class.new('something') + end + end + + context 'when regexp is a raw pattern' do + it 'raises an error' do + expect { described_class.fabricate('some .* thing') } + .to raise_error(RegexpError) + end + end + end + describe '#initialize' do subject { described_class.new(pattern) } @@ -39,6 +82,14 @@ describe Gitlab::UntrustedRegexp do expect(result).to be_falsy end + + it 'can handle regular expressions in multiline mode' do + regexp = described_class.new('^\d', multiline: true) + + result = regexp === "Header\n\n1. Content" + + expect(result).to be_truthy + end end describe '#scan' do diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index e732b089d44..660671cefaf 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Workhorse do - let(:project) { create(:project, :repository) } + set(:project) { create(:project, :repository) } let(:repository) { project.repository } def decode_workhorse_header(array) @@ -55,16 +55,6 @@ describe Gitlab::Workhorse do end end - context 'when Gitaly workhorse_archive feature is disabled', :disable_gitaly do - it 'sets the header correctly' do - key, command, params = decode_workhorse_header(subject) - - expect(key).to eq('Gitlab-Workhorse-Send-Data') - expect(command).to eq('git-archive') - expect(params).to eq(base_params) - end - end - context "when the repository doesn't have an archive file path" do before do allow(project.repository).to receive(:archive_metadata).and_return(Hash.new) diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb index da146e24893..d63f448883b 100644 --- a/spec/lib/gitlab_spec.rb +++ b/spec/lib/gitlab_spec.rb @@ -8,6 +8,66 @@ describe Gitlab do expect(described_class.root).to eq(Pathname.new(File.expand_path('../..', __dir__))) end end + describe '.revision' do + let(:cmd) { %W[#{described_class.config.git.bin_path} log --pretty=format:%h -n 1] } + + around do |example| + described_class.instance_variable_set(:@_revision, nil) + example.run + described_class.instance_variable_set(:@_revision, nil) + end + + context 'when a REVISION file exists' do + before do + expect(File).to receive(:exist?) + .with(described_class.root.join('REVISION')) + .and_return(true) + end + + it 'returns the actual Git revision' do + expect(File).to receive(:read) + .with(described_class.root.join('REVISION')) + .and_return("abc123\n") + + expect(described_class.revision).to eq('abc123') + end + + it 'memoizes the revision' do + expect(File).to receive(:read) + .once + .with(described_class.root.join('REVISION')) + .and_return("abc123\n") + + 2.times { described_class.revision } + end + end + + context 'when no REVISION file exist' do + context 'when the Git command succeeds' do + before do + expect(Gitlab::Popen).to receive(:popen_with_detail) + .with(cmd) + .and_return(Gitlab::Popen::Result.new(cmd, 'abc123', '', double(success?: true))) + end + + it 'returns the actual Git revision' do + expect(described_class.revision).to eq('abc123') + end + end + + context 'when the Git command fails' do + before do + expect(Gitlab::Popen).to receive(:popen_with_detail) + .with(cmd) + .and_return(Gitlab::Popen::Result.new(cmd, '', 'fatal: Not a git repository', double('Process::Status', success?: false))) + end + + it 'returns "Unknown"' do + expect(described_class.revision).to eq('Unknown') + end + end + end + end describe '.com?' do it 'is true when on GitLab.com' do diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 84ddbbbf2ee..f310a6854d5 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -390,6 +390,46 @@ describe Notify do end end + describe 'that are unmergeable' do + set(:merge_request) do + create(:merge_request, :conflict, + source_project: project, + target_project: project, + author: current_user, + assignee: assignee, + description: 'Awesome description') + end + + subject { described_class.merge_request_unmergeable_email(recipient.id, merge_request.id) } + + it_behaves_like 'a multiple recipients email' + it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do + let(:model) { merge_request } + end + it_behaves_like 'it should show Gmail Actions View Merge request link' + it_behaves_like 'an unsubscribeable thread' + + it 'is sent as the merge request author' do + sender = subject.header[:from].addrs[0] + expect(sender.display_name).to eq(merge_request.author.name) + expect(sender.address).to eq(gitlab_sender) + end + + it 'has the correct subject and body' do + reasons = %w[foo bar] + + allow_any_instance_of(MergeRequestPresenter).to receive(:unmergeable_reasons).and_return(reasons) + aggregate_failures do + is_expected.to have_referable_subject(merge_request, reply: true) + is_expected.to have_body_text(project_merge_request_path(project, merge_request)) + is_expected.to have_body_text('reasons:') + reasons.each do |reason| + is_expected.to have_body_text(reason) + end + end + end + end + shared_examples 'a push to an existing merge request' do let(:push_user) { create(:user) } @@ -594,7 +634,7 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Invitation to join the #{project.full_name} project" is_expected.to have_html_escaped_body_text project.full_name - is_expected.to have_body_text project.web_url + is_expected.to have_body_text project.full_name is_expected.to have_body_text project_member.human_access is_expected.to have_body_text project_member.invite_token end diff --git a/spec/migrations/add_unique_constraint_to_project_features_project_id_spec.rb b/spec/migrations/add_unique_constraint_to_project_features_project_id_spec.rb new file mode 100644 index 00000000000..bf299b70a29 --- /dev/null +++ b/spec/migrations/add_unique_constraint_to_project_features_project_id_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180511174224_add_unique_constraint_to_project_features_project_id.rb') + +describe AddUniqueConstraintToProjectFeaturesProjectId, :migration do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:features) { table(:project_features) } + let(:migration) { described_class.new } + + describe '#up' do + before do + (1..3).each do |i| + namespaces.create(id: i, name: "ns-test-#{i}", path: "ns-test-i#{i}") + projects.create!(id: i, name: "test-#{i}", path: "test-#{i}", namespace_id: i) + end + + features.create!(id: 1, project_id: 1) + features.create!(id: 2, project_id: 1) + features.create!(id: 3, project_id: 2) + features.create!(id: 4, project_id: 2) + features.create!(id: 5, project_id: 2) + features.create!(id: 6, project_id: 3) + end + + it 'creates a unique index and removes duplicates' do + expect(migration.index_exists?(:project_features, :project_id, unique: false, name: 'index_project_features_on_project_id')).to be true + + expect { migration.up }.to change { features.count }.from(6).to(3) + + expect(migration.index_exists?(:project_features, :project_id, unique: true, name: 'index_project_features_on_project_id')).to be true + expect(migration.index_exists?(:project_features, :project_id, name: 'index_project_features_on_project_id_unique')).to be false + + project_1_features = features.where(project_id: 1) + expect(project_1_features.count).to eq(1) + expect(project_1_features.first.id).to eq(2) + + project_2_features = features.where(project_id: 2) + expect(project_2_features.count).to eq(1) + expect(project_2_features.first.id).to eq(5) + + project_3_features = features.where(project_id: 3) + expect(project_3_features.count).to eq(1) + expect(project_3_features.first.id).to eq(6) + end + end + + describe '#down' do + it 'restores the original index' do + migration.up + + expect(migration.index_exists?(:project_features, :project_id, unique: true, name: 'index_project_features_on_project_id')).to be true + + migration.down + + expect(migration.index_exists?(:project_features, :project_id, unique: false, name: 'index_project_features_on_project_id')).to be true + expect(migration.index_exists?(:project_features, :project_id, name: 'index_project_features_on_project_id_old')).to be false + end + end +end diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb index 56b5d616284..77b07cf1ac9 100644 --- a/spec/models/appearance_spec.rb +++ b/spec/models/appearance_spec.rb @@ -3,33 +3,10 @@ require 'rails_helper' describe Appearance do subject { build(:appearance) } - it { is_expected.to be_valid } + it { include(CacheableAttributes) } + it { expect(described_class.current_without_cache).to eq(described_class.first) } - it { is_expected.to have_many(:uploads).dependent(:destroy) } - - describe '.current', :use_clean_rails_memory_store_caching do - let!(:appearance) { create(:appearance) } - - it 'returns the current appearance row' do - expect(described_class.current).to eq(appearance) - end - - it 'caches the result' do - expect(described_class).to receive(:first).once - - 2.times { described_class.current } - end - end - - describe '#flush_redis_cache' do - it 'flushes the cache in Redis' do - appearance = create(:appearance) - - expect(Rails.cache).to receive(:delete).with(described_class::CACHE_KEY) - - appearance.flush_redis_cache - end - end + it { is_expected.to have_many(:uploads) } describe '#single_appearance_row' do it 'adds an error when more than 1 row exists' do @@ -41,4 +18,12 @@ describe Appearance do expect(new_row.valid?).to eq(false) end end + + context 'with uploads' do + it_behaves_like 'model with mounted uploader', false do + let(:model_object) { create(:appearance, :with_logo) } + let(:upload_attribute) { :logo } + let(:uploader_class) { AttachmentUploader } + end + end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 10d6109cae7..7e47043a1cb 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -3,6 +3,9 @@ require 'spec_helper' describe ApplicationSetting do let(:setting) { described_class.create_from_defaults } + it { include(CacheableAttributes) } + it { expect(described_class.current_without_cache).to eq(described_class.last) } + it { expect(setting).to be_valid } it { expect(setting.uuid).to be_present } it { expect(setting).to have_db_column(:auto_devops_enabled) } @@ -318,33 +321,6 @@ describe ApplicationSetting do end end - describe '.current' do - context 'redis unavailable' do - it 'returns an ApplicationSetting' do - allow(Rails.cache).to receive(:fetch).and_call_original - allow(described_class).to receive(:last).and_return(:last) - expect(Rails.cache).to receive(:fetch).with(ApplicationSetting::CACHE_KEY).and_raise(ArgumentError) - - expect(described_class.current).to eq(:last) - end - end - - context 'when an ApplicationSetting is not yet present' do - it 'does not cache nil object' do - # when missing settings a nil object is returned, but not cached - allow(described_class).to receive(:last).and_return(nil).twice - expect(described_class.current).to be_nil - - # when the settings are set the method returns a valid object - allow(described_class).to receive(:last).and_return(:last) - expect(described_class.current).to eq(:last) - - # subsequent calls get everything from cache - expect(described_class.current).to eq(:last) - end - end - end - context 'restrict creating duplicates' do before do described_class.create_from_defaults diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index dc810489011..77179028ede 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -629,6 +629,14 @@ describe Ci::Build do it { is_expected.to eq('review/host') } end + + context 'when using persisted variables' do + let(:build) do + create(:ci_build, environment: 'review/x$CI_BUILD_ID') + end + + it { is_expected.to eq('review/x') } + end end describe '#starts_environment?' do @@ -1270,6 +1278,46 @@ describe Ci::Build do end end + describe '#playable?' do + context 'when build is a manual action' do + context 'when build has been skipped' do + subject { build_stubbed(:ci_build, :manual, status: :skipped) } + + it { is_expected.not_to be_playable } + end + + context 'when build has been canceled' do + subject { build_stubbed(:ci_build, :manual, status: :canceled) } + + it { is_expected.to be_playable } + end + + context 'when build is successful' do + subject { build_stubbed(:ci_build, :manual, status: :success) } + + it { is_expected.to be_playable } + end + + context 'when build has failed' do + subject { build_stubbed(:ci_build, :manual, status: :failed) } + + it { is_expected.to be_playable } + end + + context 'when build is a manual untriggered action' do + subject { build_stubbed(:ci_build, :manual, status: :manual) } + + it { is_expected.to be_playable } + end + end + + context 'when build is not a manual action' do + subject { build_stubbed(:ci_build, :success) } + + it { is_expected.not_to be_playable } + end + end + describe 'project settings' do describe '#allow_git_fetch' do it 'return project allow_git_fetch configuration' do @@ -1485,6 +1533,7 @@ describe Ci::Build do let(:container_registry_enabled) { false } let(:predefined_variables) do [ + { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }, { key: 'CI_JOB_ID', value: build.id.to_s, public: true }, { key: 'CI_JOB_TOKEN', value: build.token, public: false }, { key: 'CI_BUILD_ID', value: build.id.to_s, public: true }, @@ -1497,7 +1546,7 @@ describe Ci::Build do { key: 'GITLAB_FEATURES', value: project.licensed_features.join(','), public: true }, { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, - { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, + { key: 'CI_SERVER_REVISION', value: Gitlab.revision, public: true }, { key: 'CI_JOB_NAME', value: 'test', public: true }, { key: 'CI_JOB_STAGE', value: 'test', public: true }, { key: 'CI_COMMIT_SHA', value: build.sha, public: true }, @@ -1516,7 +1565,6 @@ describe Ci::Build do { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true }, { key: 'CI_PROJECT_URL', value: project.web_url, public: true }, { key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true }, - { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }, { key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, public: true }, { key: 'CI_PIPELINE_SOURCE', value: pipeline.source, public: true }, { key: 'CI_COMMIT_MESSAGE', value: pipeline.git_commit_message, public: true }, @@ -2044,7 +2092,7 @@ describe Ci::Build do let(:deploy_token_variables) do [ - { key: 'CI_DEPLOY_USER', value: deploy_token.name, public: true }, + { key: 'CI_DEPLOY_USER', value: deploy_token.username, public: true }, { key: 'CI_DEPLOY_PASSWORD', value: deploy_token.token, public: false } ] end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index ddd66a6be87..e4f4c62bd22 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -167,13 +167,39 @@ describe Ci::Pipeline, :mailer do end end + describe '#persisted_variables' do + context 'when pipeline is not persisted yet' do + subject { build(:ci_pipeline).persisted_variables } + + it 'does not contain some variables' do + keys = subject.map { |variable| variable[:key] } + + expect(keys).not_to include 'CI_PIPELINE_ID' + end + end + + context 'when pipeline is persisted' do + subject { build_stubbed(:ci_pipeline).persisted_variables } + + it 'does contains persisted variables' do + keys = subject.map { |variable| variable[:key] } + + expect(keys).to eq %w[CI_PIPELINE_ID] + end + end + end + describe '#predefined_variables' do subject { pipeline.predefined_variables } it 'includes all predefined variables in a valid order' do keys = subject.map { |variable| variable[:key] } - expect(keys).to eq %w[CI_PIPELINE_ID CI_CONFIG_PATH CI_PIPELINE_SOURCE CI_COMMIT_MESSAGE CI_COMMIT_TITLE CI_COMMIT_DESCRIPTION] + expect(keys).to eq %w[CI_CONFIG_PATH + CI_PIPELINE_SOURCE + CI_COMMIT_MESSAGE + CI_COMMIT_TITLE + CI_COMMIT_DESCRIPTION] end end @@ -774,6 +800,33 @@ describe Ci::Pipeline, :mailer do end end + describe '#number_of_warnings' do + it 'returns the number of warnings' do + create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop') + + expect(pipeline.number_of_warnings).to eq(1) + end + + it 'supports eager loading of the number of warnings' do + pipeline2 = create(:ci_empty_pipeline, status: :created, project: project) + + create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop') + create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline2, name: 'rubocop') + + pipelines = project.pipelines.to_a + + pipelines.each(&:number_of_warnings) + + # To run the queries we need to actually use the lazy objects, which we do + # by just sending "to_i" to them. + amount = ActiveRecord::QueryRecorder + .new { pipelines.each { |p| p.number_of_warnings.to_i } } + .count + + expect(amount).to eq(1) + end + end + shared_context 'with some outdated pipelines' do before do create_pipeline(:canceled, 'ref', 'A', project) diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index e2b212f4f4c..0fbc934f669 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -626,62 +626,26 @@ describe Ci::Runner do end describe '.assignable_for' do - let(:runner) { create(:ci_runner) } + let!(:unlocked_project_runner) { create(:ci_runner, runner_type: :project_type, projects: [project]) } + let!(:locked_project_runner) { create(:ci_runner, runner_type: :project_type, locked: true, projects: [project]) } + let!(:group_runner) { create(:ci_runner, runner_type: :group_type) } + let!(:instance_runner) { create(:ci_runner, :shared) } let(:project) { create(:project) } let(:another_project) { create(:project) } - before do - project.runners << runner - end - - context 'with shared runners' do - before do - runner.update(is_shared: true) - end - - context 'does not give owned runner' do - subject { described_class.assignable_for(project) } - - it { is_expected.to be_empty } - end - - context 'does not give shared runner' do - subject { described_class.assignable_for(another_project) } - - it { is_expected.to be_empty } - end - end - - context 'with unlocked runner' do - context 'does not give owned runner' do - subject { described_class.assignable_for(project) } - - it { is_expected.to be_empty } - end + context 'with already assigned project' do + subject { described_class.assignable_for(project) } - context 'does give a specific runner' do - subject { described_class.assignable_for(another_project) } - - it { is_expected.to contain_exactly(runner) } - end + it { is_expected.to be_empty } end - context 'with locked runner' do - before do - runner.update(locked: true) - end - - context 'does not give owned runner' do - subject { described_class.assignable_for(project) } - - it { is_expected.to be_empty } - end - - context 'does not give a locked runner' do - subject { described_class.assignable_for(another_project) } + context 'with a different project' do + subject { described_class.assignable_for(another_project) } - it { is_expected.to be_empty } - end + it { is_expected.to include(unlocked_project_runner) } + it { is_expected.not_to include(group_runner) } + it { is_expected.not_to include(locked_project_runner) } + it { is_expected.not_to include(instance_runner) } end end diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index aeca6ee903a..d2302583ac8 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -79,12 +79,22 @@ describe Clusters::Applications::Prometheus do end it 'creates proper url' do - expect(subject.prometheus_client.url).to eq('http://example.com/api/v1/proxy/namespaces/gitlab-managed-apps/service/prometheus-prometheus-server:80') + expect(subject.prometheus_client.url).to eq('http://example.com/api/v1/namespaces/gitlab-managed-apps/service/prometheus-prometheus-server:80/proxy') end it 'copies options and headers from kube client to proxy client' do expect(subject.prometheus_client.options).to eq(kube_client.rest_client.options.merge(headers: kube_client.headers)) end + + context 'when cluster is not reachable' do + before do + allow(kube_client).to receive(:proxy_url).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil)) + end + + it 'returns nil' do + expect(subject.prometheus_client).to be_nil + end + end end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 4e6b037a720..090f91168ad 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -52,22 +52,98 @@ describe Commit do end end - describe '#author' do + describe '#author', :request_store do it 'looks up the author in a case-insensitive way' do user = create(:user, email: commit.author_email.upcase) expect(commit.author).to eq(user) end - it 'caches the author', :request_store do + it 'caches the author' do user = create(:user, email: commit.author_email) - expect(User).to receive(:find_by_any_email).and_call_original expect(commit.author).to eq(user) + key = "Commit:author:#{commit.author_email.downcase}" - expect(RequestStore.store[key]).to eq(user) + expect(RequestStore.store[key]).to eq(user) expect(commit.author).to eq(user) end + + context 'using eager loading' do + let!(:alice) { create(:user, email: 'alice@example.com') } + let!(:bob) { create(:user, email: 'hunter2@example.com') } + + let(:alice_commit) do + described_class.new(RepoHelpers.sample_commit, project).tap do |c| + c.author_email = 'alice@example.com' + end + end + + let(:bob_commit) do + # The commit for Bob uses one of his alternative Emails, instead of the + # primary one. + described_class.new(RepoHelpers.sample_commit, project).tap do |c| + c.author_email = 'bob@example.com' + end + end + + let(:eve_commit) do + described_class.new(RepoHelpers.sample_commit, project).tap do |c| + c.author_email = 'eve@example.com' + end + end + + let!(:commits) { [alice_commit, bob_commit, eve_commit] } + + before do + create(:email, user: bob, email: 'bob@example.com') + end + + it 'executes only two SQL queries' do + recorder = ActiveRecord::QueryRecorder.new do + # Running this first ensures we don't run one query for every + # commit. + commits.each(&:lazy_author) + + # This forces the execution of the SQL queries necessary to load the + # data. + commits.each { |c| c.author.try(:id) } + end + + expect(recorder.count).to eq(2) + end + + it "preloads the authors for Commits matching a user's primary Email" do + commits.each(&:lazy_author) + + expect(alice_commit.author).to eq(alice) + end + + it "preloads the authors for Commits using a User's alternative Email" do + commits.each(&:lazy_author) + + expect(bob_commit.author).to eq(bob) + end + + it 'sets the author to Nil if an author could not be found for a Commit' do + commits.each(&:lazy_author) + + expect(eve_commit.author).to be_nil + end + + it 'does not execute SQL queries once the authors are preloaded' do + commits.each(&:lazy_author) + commits.each { |c| c.author.try(:id) } + + recorder = ActiveRecord::QueryRecorder.new do + alice_commit.author + bob_commit.author + eve_commit.author + end + + expect(recorder.count).to be_zero + end + end end describe '#to_reference' do @@ -182,7 +258,6 @@ eos it { is_expected.to respond_to(:date) } it { is_expected.to respond_to(:diffs) } it { is_expected.to respond_to(:id) } - it { is_expected.to respond_to(:to_patch) } end describe '#closes_issues' do diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 2ed29052dc1..f3f2bc28d2c 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -565,4 +565,10 @@ describe CommitStatus do it_behaves_like 'commit status enqueued' end end + + describe '#present' do + subject { commit_status.present } + + it { is_expected.to be_a(CommitStatusPresenter) } + end end diff --git a/spec/models/concerns/cacheable_attributes_spec.rb b/spec/models/concerns/cacheable_attributes_spec.rb new file mode 100644 index 00000000000..49e4b23ebc7 --- /dev/null +++ b/spec/models/concerns/cacheable_attributes_spec.rb @@ -0,0 +1,153 @@ +require 'spec_helper' + +describe CacheableAttributes do + let(:minimal_test_class) do + Class.new do + include ActiveModel::Model + extend ActiveModel::Callbacks + define_model_callbacks :commit + include CacheableAttributes + + def self.name + 'TestClass' + end + + def self.first + @_first ||= new('foo' => 'a') + end + + def self.last + @_last ||= new('foo' => 'a', 'bar' => 'b') + end + + attr_accessor :attributes + + def initialize(attrs = {}) + @attributes = attrs + end + end + end + + shared_context 'with defaults' do + before do + minimal_test_class.define_singleton_method(:defaults) do + { foo: 'a', bar: 'b', baz: 'c' } + end + end + end + + describe '.current_without_cache' do + it 'defaults to last' do + expect(minimal_test_class.current_without_cache).to eq(minimal_test_class.last) + end + + it 'can be overriden' do + minimal_test_class.define_singleton_method(:current_without_cache) do + first + end + + expect(minimal_test_class.current_without_cache).to eq(minimal_test_class.first) + end + end + + describe '.cache_key' do + it 'excludes cache attributes' do + expect(minimal_test_class.cache_key).to eq("TestClass:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:json") + end + end + + describe '.defaults' do + it 'defaults to {}' do + expect(minimal_test_class.defaults).to eq({}) + end + + context 'with defaults defined' do + include_context 'with defaults' + + it 'can be overriden' do + expect(minimal_test_class.defaults).to eq({ foo: 'a', bar: 'b', baz: 'c' }) + end + end + end + + describe '.build_from_defaults' do + include_context 'with defaults' + + context 'without any attributes given' do + it 'intializes a new object with the defaults' do + expect(minimal_test_class.build_from_defaults).not_to be_persisted + end + end + + context 'without attributes given' do + it 'intializes a new object with the given attributes merged into the defaults' do + expect(minimal_test_class.build_from_defaults(foo: 'd').attributes[:foo]).to eq('d') + end + end + end + + describe '.current', :use_clean_rails_memory_store_caching do + context 'redis unavailable' do + it 'returns an uncached record' do + allow(minimal_test_class).to receive(:last).and_return(:last) + expect(Rails.cache).to receive(:read).and_raise(Redis::BaseError) + + expect(minimal_test_class.current).to eq(:last) + end + end + + context 'when a record is not yet present' do + it 'does not cache nil object' do + # when missing settings a nil object is returned, but not cached + allow(minimal_test_class).to receive(:last).twice.and_return(nil) + + expect(minimal_test_class.current).to be_nil + expect(Rails.cache.exist?(minimal_test_class.cache_key)).to be(false) + end + + it 'cache non-nil object' do + # when the settings are set the method returns a valid object + allow(minimal_test_class).to receive(:last).and_call_original + + expect(minimal_test_class.current).to eq(minimal_test_class.last) + expect(Rails.cache.exist?(minimal_test_class.cache_key)).to be(true) + + # subsequent calls retrieve the record from the cache + last_record = minimal_test_class.last + expect(minimal_test_class).not_to receive(:last) + expect(minimal_test_class.current.attributes).to eq(last_record.attributes) + end + end + end + + describe '.cached', :use_clean_rails_memory_store_caching do + context 'when cache is cold' do + it 'returns nil' do + expect(minimal_test_class.cached).to be_nil + end + end + + context 'when cached settings do not include the latest defaults' do + before do + Rails.cache.write(minimal_test_class.cache_key, { bar: 'b', baz: 'c' }.to_json) + minimal_test_class.define_singleton_method(:defaults) do + { foo: 'a', bar: 'b', baz: 'c' } + end + end + + it 'includes attributes from defaults' do + expect(minimal_test_class.cached.attributes[:foo]).to eq(minimal_test_class.defaults[:foo]) + end + end + end + + describe '#cache!', :use_clean_rails_memory_store_caching do + let(:appearance_record) { create(:appearance) } + + it 'caches the attributes' do + appearance_record.cache! + + expect(Rails.cache.read(Appearance.cache_key)).to eq(appearance_record.attributes.to_json) + end + end +end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 3d3092b8ac9..bd6bf5b0712 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -266,6 +266,19 @@ describe Issuable do end end + describe '#time_estimate=' do + it 'coerces the value below Gitlab::Database::MAX_INT_VALUE' do + expect { issue.time_estimate = 100 }.to change { issue.time_estimate }.to(100) + expect { issue.time_estimate = Gitlab::Database::MAX_INT_VALUE + 100 }.to change { issue.time_estimate }.to(Gitlab::Database::MAX_INT_VALUE) + end + + it 'skips coercion for not Integer values' do + expect { issue.time_estimate = nil }.to change { issue.time_estimate }.to(nil) + expect { issue.time_estimate = 'invalid time' }.not_to raise_error(StandardError) + expect { issue.time_estimate = 22.33 }.not_to raise_error(StandardError) + end + end + describe '#to_hook_data' do let(:builder) { double } diff --git a/spec/models/concerns/redis_cacheable_spec.rb b/spec/models/concerns/redis_cacheable_spec.rb index 3d7963120b6..23c6c6233e9 100644 --- a/spec/models/concerns/redis_cacheable_spec.rb +++ b/spec/models/concerns/redis_cacheable_spec.rb @@ -1,21 +1,36 @@ require 'spec_helper' describe RedisCacheable do - let(:model) { double } + let(:model) do + Struct.new(:id, :attributes) do + def read_attribute(attribute) + attributes[attribute] + end + + def cast_value_from_cache(attribute, cached_value) + cached_value + end + + def has_attribute?(attribute) + attributes.has_key?(attribute) + end + end + end + + let(:payload) { { name: 'value', time: Time.zone.now } } + let(:instance) { model.new(1, payload) } + let(:cache_key) { instance.__send__(:cache_attribute_key) } before do - model.extend(described_class) - allow(model).to receive(:cache_attribute_key).and_return('key') + model.include(described_class) end describe '#cached_attribute' do - let(:payload) { { attribute: 'value' } } - - subject { model.cached_attribute(payload.keys.first) } + subject { instance.cached_attribute(payload.keys.first) } it 'gets the cache attribute' do Gitlab::Redis::SharedState.with do |redis| - expect(redis).to receive(:get).with('key') + expect(redis).to receive(:get).with(cache_key) .and_return(payload.to_json) end @@ -24,16 +39,62 @@ describe RedisCacheable do end describe '#cache_attributes' do - let(:values) { { name: 'new_name' } } - - subject { model.cache_attributes(values) } + subject { instance.cache_attributes(payload) } it 'sets the cache attributes' do Gitlab::Redis::SharedState.with do |redis| - expect(redis).to receive(:set).with('key', values.to_json, anything) + expect(redis).to receive(:set).with(cache_key, payload.to_json, anything) end subject end end + + describe '#cached_attr_reader', :clean_gitlab_redis_shared_state do + subject { instance.name } + + before do + model.cached_attr_reader(:name) + end + + context 'when there is no cached value' do + it 'reads the attribute' do + expect(instance).to receive(:read_attribute).and_call_original + + expect(subject).to eq(payload[:name]) + end + end + + context 'when there is a cached value' do + it 'reads the cached value' do + expect(instance).not_to receive(:read_attribute) + + instance.cache_attributes(payload) + + expect(subject).to eq(payload[:name]) + end + end + + it 'always returns the latest values' do + expect(instance.name).to eq(payload[:name]) + + instance.cache_attributes(name: 'new_value') + + expect(instance.name).to eq('new_value') + end + end + + describe '#cast_value_from_cache' do + subject { instance.__send__(:cast_value_from_cache, attribute, value) } + + context 'with runner contacted_at' do + let(:instance) { Ci::Runner.new } + let(:attribute) { :contacted_at } + let(:value) { '2018-05-07 13:53:08 UTC' } + + it 'converts cache string to appropriate type' do + expect(subject).to be_an_instance_of(ActiveSupport::TimeWithZone) + end + end + end end diff --git a/spec/models/concerns/resolvable_note_spec.rb b/spec/models/concerns/resolvable_note_spec.rb index 91591017587..fcb5250278e 100644 --- a/spec/models/concerns/resolvable_note_spec.rb +++ b/spec/models/concerns/resolvable_note_spec.rb @@ -326,4 +326,12 @@ describe Note, ResolvableNote do end end end + + describe "#potentially_resolvable?" do + it 'returns false if noteable could not be found' do + allow(subject).to receive(:noteable).and_return(nil) + + expect(subject.potentially_resolvable?).to be_falsey + end + end end diff --git a/spec/models/concerns/sortable_spec.rb b/spec/models/concerns/sortable_spec.rb new file mode 100644 index 00000000000..b821a84d5e0 --- /dev/null +++ b/spec/models/concerns/sortable_spec.rb @@ -0,0 +1,108 @@ +require 'spec_helper' + +describe Sortable do + describe '.order_by' do + let(:relation) { Group.all } + + describe 'ordering by id' do + it 'ascending' do + expect(relation).to receive(:reorder).with(id: :asc) + + relation.order_by('id_asc') + end + + it 'descending' do + expect(relation).to receive(:reorder).with(id: :desc) + + relation.order_by('id_desc') + end + end + + describe 'ordering by created day' do + it 'ascending' do + expect(relation).to receive(:reorder).with(created_at: :asc) + + relation.order_by('created_asc') + end + + it 'descending' do + expect(relation).to receive(:reorder).with(created_at: :desc) + + relation.order_by('created_desc') + end + + it 'order by "date"' do + expect(relation).to receive(:reorder).with(created_at: :desc) + + relation.order_by('created_date') + end + end + + describe 'ordering by name' do + it 'ascending' do + expect(relation).to receive(:reorder).with("lower(name) asc") + + relation.order_by('name_asc') + end + + it 'descending' do + expect(relation).to receive(:reorder).with("lower(name) desc") + + relation.order_by('name_desc') + end + end + + describe 'ordering by Updated Time' do + it 'ascending' do + expect(relation).to receive(:reorder).with(updated_at: :asc) + + relation.order_by('updated_asc') + end + + it 'descending' do + expect(relation).to receive(:reorder).with(updated_at: :desc) + + relation.order_by('updated_desc') + end + end + + it 'does not call reorder in case of unrecognized ordering' do + expect(relation).not_to receive(:reorder) + + relation.order_by('random_ordering') + end + end + + describe 'sorting groups' do + def ordered_group_names(order) + Group.all.order_by(order).map(&:name) + end + + let!(:ref_time) { Time.parse('2018-05-01 00:00:00') } + let!(:group1) { create(:group, name: 'aa', id: 1, created_at: ref_time - 15.seconds, updated_at: ref_time) } + let!(:group2) { create(:group, name: 'AAA', id: 2, created_at: ref_time - 10.seconds, updated_at: ref_time - 5.seconds) } + let!(:group3) { create(:group, name: 'BB', id: 3, created_at: ref_time - 5.seconds, updated_at: ref_time - 10.seconds) } + let!(:group4) { create(:group, name: 'bbb', id: 4, created_at: ref_time, updated_at: ref_time - 15.seconds) } + + it 'sorts groups by id' do + expect(ordered_group_names('id_asc')).to eq(%w(aa AAA BB bbb)) + expect(ordered_group_names('id_desc')).to eq(%w(bbb BB AAA aa)) + end + + it 'sorts groups by name via case-insentitive comparision' do + expect(ordered_group_names('name_asc')).to eq(%w(aa AAA BB bbb)) + expect(ordered_group_names('name_desc')).to eq(%w(bbb BB AAA aa)) + end + + it 'sorts groups by created_at' do + expect(ordered_group_names('created_asc')).to eq(%w(aa AAA BB bbb)) + expect(ordered_group_names('created_desc')).to eq(%w(bbb BB AAA aa)) + expect(ordered_group_names('created_date')).to eq(%w(bbb BB AAA aa)) + end + + it 'sorts groups by updated_at' do + expect(ordered_group_names('updated_asc')).to eq(%w(bbb BB AAA aa)) + expect(ordered_group_names('updated_desc')).to eq(%w(aa AAA BB bbb)) + end + end +end diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb index 673049d1cc4..a3e68d2e646 100644 --- a/spec/models/generic_commit_status_spec.rb +++ b/spec/models/generic_commit_status_spec.rb @@ -78,4 +78,10 @@ describe GenericCommitStatus do it { is_expected.not_to be_nil } end end + + describe '#present' do + subject { generic_commit_status.present } + + it { is_expected.to be_a(GenericCommitStatusPresenter) } + end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 0907d28d33b..f83b52e8975 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -15,7 +15,7 @@ describe Group do it { is_expected.to have_many(:notification_settings).dependent(:destroy) } it { is_expected.to have_many(:labels).class_name('GroupLabel') } it { is_expected.to have_many(:variables).class_name('Ci::GroupVariable') } - it { is_expected.to have_many(:uploads).dependent(:destroy) } + it { is_expected.to have_many(:uploads) } it { is_expected.to have_one(:chat_team) } it { is_expected.to have_many(:custom_attributes).class_name('GroupCustomAttribute') } it { is_expected.to have_many(:badges).class_name('GroupBadge') } @@ -691,4 +691,12 @@ describe Group do end end end + + context 'with uploads' do + it_behaves_like 'model with mounted uploader', true do + let(:model_object) { create(:group, :with_avatar) } + let(:upload_attribute) { :avatar } + let(:uploader_class) { AttachmentUploader } + end + end end diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb index 2afdd6751a4..fc30f3056e5 100644 --- a/spec/models/guest_spec.rb +++ b/spec/models/guest_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' describe Guest do - let(:public_project) { build_stubbed(:project, :public) } - let(:private_project) { build_stubbed(:project, :private) } - let(:internal_project) { build_stubbed(:project, :internal) } + set(:public_project) { create(:project, :public) } + set(:private_project) { create(:project, :private) } + set(:internal_project) { create(:project, :internal) } describe '.can_pull?' do context 'when project is private' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 04379e7d2c3..92e33a64d26 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1245,38 +1245,50 @@ describe MergeRequest do describe '#check_if_can_be_merged' do let(:project) { create(:project, only_allow_merge_if_pipeline_succeeds: true) } - subject { create(:merge_request, source_project: project, merge_status: :unchecked) } + shared_examples 'checking if can be merged' do + context 'when it is not broken and has no conflicts' do + before do + allow(subject).to receive(:broken?) { false } + allow(project.repository).to receive(:can_be_merged?).and_return(true) + end - context 'when it is not broken and has no conflicts' do - before do - allow(subject).to receive(:broken?) { false } - allow(project.repository).to receive(:can_be_merged?).and_return(true) + it 'is marked as mergeable' do + expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged') + end end - it 'is marked as mergeable' do - expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged') - end - end + context 'when broken' do + before do + allow(subject).to receive(:broken?) { true } + end - context 'when broken' do - before do - allow(subject).to receive(:broken?) { true } + it 'becomes unmergeable' do + expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged') + end end - it 'becomes unmergeable' do - expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged') + context 'when it has conflicts' do + before do + allow(subject).to receive(:broken?) { false } + allow(project.repository).to receive(:can_be_merged?).and_return(false) + end + + it 'becomes unmergeable' do + expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged') + end end end - context 'when it has conflicts' do - before do - allow(subject).to receive(:broken?) { false } - allow(project.repository).to receive(:can_be_merged?).and_return(false) - end + context 'when merge_status is unchecked' do + subject { create(:merge_request, source_project: project, merge_status: :unchecked) } - it 'becomes unmergeable' do - expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged') - end + it_behaves_like 'checking if can be merged' + end + + context 'when merge_status is unchecked' do + subject { create(:merge_request, source_project: project, merge_status: :cannot_be_merged_recheck) } + + it_behaves_like 'checking if can be merged' end end @@ -2064,6 +2076,53 @@ describe MergeRequest do expect(subject.merge_jid).to be_nil end end + + describe 'transition to cannot_be_merged' do + let(:notification_service) { double(:notification_service) } + let(:todo_service) { double(:todo_service) } + + subject { create(:merge_request, merge_status: :unchecked) } + + before do + allow(NotificationService).to receive(:new).and_return(notification_service) + allow(TodoService).to receive(:new).and_return(todo_service) + end + + it 'notifies, but does not notify again if rechecking still results in cannot_be_merged' do + expect(notification_service).to receive(:merge_request_unmergeable).with(subject).once + expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).once + + subject.mark_as_unmergeable + subject.mark_as_unchecked + subject.mark_as_unmergeable + end + + it 'notifies whenever merge request is newly unmergeable' do + expect(notification_service).to receive(:merge_request_unmergeable).with(subject).twice + expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).twice + + subject.mark_as_unmergeable + subject.mark_as_unchecked + subject.mark_as_mergeable + subject.mark_as_unchecked + subject.mark_as_unmergeable + end + end + + describe 'check_state?' do + it 'indicates whether MR is still checking for mergeability' do + state_machine = described_class.state_machines[:merge_status] + check_states = [:unchecked, :cannot_be_merged_recheck] + + check_states.each do |merge_status| + expect(state_machine.check_state?(merge_status)).to be true + end + + (state_machine.states.map(&:name) - check_states).each do |merge_status| + expect(state_machine.check_state?(merge_status)).to be false + end + end + end end describe '#should_be_rebased?' do @@ -2195,4 +2254,39 @@ describe MergeRequest do expect(merge_request.can_allow_maintainer_to_push?(user)).to be_truthy end end + + describe '#merge_participants' do + it 'contains author' do + expect(subject.merge_participants).to eq([subject.author]) + end + + describe 'when merge_when_pipeline_succeeds? is true' do + describe 'when merge user is author' do + let(:user) { create(:user) } + subject do + create(:merge_request, + merge_when_pipeline_succeeds: true, + merge_user: user, + author: user) + end + + it 'contains author only' do + expect(subject.merge_participants).to eq([subject.author]) + end + end + + describe 'when merge user and author are different users' do + let(:merge_user) { create(:user) } + subject do + create(:merge_request, + merge_when_pipeline_succeeds: true, + merge_user: merge_user) + end + + it 'contains author and merge user' do + expect(subject.merge_participants).to eq([subject.author, merge_user]) + end + end + end + end end diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb index 4c61bc0af95..6e417323e40 100644 --- a/spec/models/project_services/gemnasium_service_spec.rb +++ b/spec/models/project_services/gemnasium_service_spec.rb @@ -26,24 +26,49 @@ describe GemnasiumService do end end + describe "deprecated?" do + let(:project) { create(:project, :repository) } + let(:gemnasium_service) { described_class.new } + + before do + allow(gemnasium_service).to receive_messages( + project_id: project.id, + project: project, + service_hook: true, + token: 'verySecret', + api_key: 'GemnasiumUserApiKey' + ) + end + + it "is true" do + expect(gemnasium_service.deprecated?).to be true + end + + it "can't create a new service" do + expect(gemnasium_service.save).to be false + expect(gemnasium_service.errors[:base].first) + .to eq('Gemnasium has been acquired by GitLab in January 2018. Since May 15, 2018, the service provided by Gemnasium is no longer available.') + end + end + describe "Execute" do let(:user) { create(:user) } let(:project) { create(:project, :repository) } + let(:gemnasium_service) { described_class.new } + let(:sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) } before do - @gemnasium_service = described_class.new - allow(@gemnasium_service).to receive_messages( + allow(gemnasium_service).to receive_messages( project_id: project.id, project: project, service_hook: true, token: 'verySecret', api_key: 'GemnasiumUserApiKey' ) - @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user) end it "calls Gemnasium service" do expect(Gemnasium::GitlabService).to receive(:execute).with(an_instance_of(Hash)).once - @gemnasium_service.execute(@sample_data) + gemnasium_service.execute(sample_data) end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 5b452f17979..af2240f4f89 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -76,7 +76,7 @@ describe Project do it { is_expected.to have_many(:project_group_links) } it { is_expected.to have_many(:notification_settings).dependent(:delete_all) } it { is_expected.to have_many(:forks).through(:forked_project_links) } - it { is_expected.to have_many(:uploads).dependent(:destroy) } + it { is_expected.to have_many(:uploads) } it { is_expected.to have_many(:pipeline_schedules) } it { is_expected.to have_many(:members_and_requesters) } it { is_expected.to have_many(:clusters) } @@ -469,6 +469,34 @@ describe Project do end end + describe "#readme_url" do + let(:project) { create(:project, :repository, path: "somewhere") } + + context 'with a non-existing repository' do + it 'returns nil' do + allow(project.repository).to receive(:tree).with(:head).and_return(nil) + + expect(project.readme_url).to be_nil + end + end + + context 'with an existing repository' do + context 'when no README exists' do + it 'returns nil' do + allow_any_instance_of(Tree).to receive(:readme).and_return(nil) + + expect(project.readme_url).to be_nil + end + end + + context 'when a README exists' do + it 'returns the README' do + expect(project.readme_url).to eql("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere/blob/master/README.md") + end + end + end + end + describe "#new_issuable_address" do let(:project) { create(:project, path: "somewhere") } let(:user) { create(:user) } @@ -3739,4 +3767,12 @@ describe Project do it { is_expected.to be_nil } end end + + context 'with uploads' do + it_behaves_like 'model with mounted uploader', true do + let(:model_object) { create(:project, :with_avatar) } + let(:upload_attribute) { :avatar } + let(:uploader_class) { AttachmentUploader } + end + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index a7755a505d8..0ccf55bd895 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -671,7 +671,7 @@ describe Repository do end end - describe "search_files_by_content" do + shared_examples "search_files_by_content" do let(:results) { repository.search_files_by_content('feature', 'master') } subject { results } @@ -718,7 +718,7 @@ describe Repository do end end - describe "search_files_by_name" do + shared_examples "search_files_by_name" do let(:results) { repository.search_files_by_name('files', 'master') } it 'returns result' do @@ -758,6 +758,16 @@ describe Repository do end end + describe 'with gitaly enabled' do + it_behaves_like 'search_files_by_content' + it_behaves_like 'search_files_by_name' + end + + describe 'with gitaly disabled', :disable_gitaly do + it_behaves_like 'search_files_by_content' + it_behaves_like 'search_files_by_name' + end + describe '#async_remove_remote' do before do masterrev = repository.find_branch('master').dereferenced_target @@ -990,65 +1000,25 @@ describe Repository do subject { repository.add_branch(user, branch_name, target) } - context 'with Gitaly enabled' do - it "calls Gitaly's OperationService" do - expect_any_instance_of(Gitlab::GitalyClient::OperationService) - .to receive(:user_create_branch).with(branch_name, user, target) - .and_return(nil) - - subject - end - - it 'creates_the_branch' do - expect(subject.name).to eq(branch_name) - expect(repository.find_branch(branch_name)).not_to be_nil - end + it "calls Gitaly's OperationService" do + expect_any_instance_of(Gitlab::GitalyClient::OperationService) + .to receive(:user_create_branch).with(branch_name, user, target) + .and_return(nil) - context 'with a non-existing target' do - let(:target) { 'fake-target' } - - it "returns false and doesn't create the branch" do - expect(subject).to be(false) - expect(repository.find_branch(branch_name)).to be_nil - end - end + subject end - context 'with Gitaly disabled', :disable_gitaly do - context 'when pre hooks were successful' do - it 'runs without errors' do - hook = double(trigger: [true, nil]) - expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) - - expect { subject }.not_to raise_error - end - - it 'creates the branch' do - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil]) - - expect(subject.name).to eq(branch_name) - end - - it 'calls the after_create_branch hook' do - expect(repository).to receive(:after_create_branch) - - subject - end - end - - context 'when pre hooks failed' do - it 'gets an error' do - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) - - expect { subject }.to raise_error(Gitlab::Git::HooksService::PreReceiveError) - end + it 'creates_the_branch' do + expect(subject.name).to eq(branch_name) + expect(repository.find_branch(branch_name)).not_to be_nil + end - it 'does not create the branch' do - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) + context 'with a non-existing target' do + let(:target) { 'fake-target' } - expect { subject }.to raise_error(Gitlab::Git::HooksService::PreReceiveError) - expect(repository.find_branch(branch_name)).to be_nil - end + it "returns false and doesn't create the branch" do + expect(subject).to be(false) + expect(repository.find_branch(branch_name)).to be_nil end end end @@ -2061,27 +2031,27 @@ describe Repository do describe '#xcode_project?' do before do - allow(repository).to receive(:tree).with(:head).and_return(double(:tree, blobs: [blob])) + allow(repository).to receive(:tree).with(:head).and_return(double(:tree, trees: [tree])) end - context 'when the root contains a *.xcodeproj file' do - let(:blob) { double(:blob, path: 'Foo.xcodeproj') } + context 'when the root contains a *.xcodeproj directory' do + let(:tree) { double(:tree, path: 'Foo.xcodeproj') } it 'returns true' do expect(repository.xcode_project?).to be_truthy end end - context 'when the root contains a *.xcworkspace file' do - let(:blob) { double(:blob, path: 'Foo.xcworkspace') } + context 'when the root contains a *.xcworkspace directory' do + let(:tree) { double(:tree, path: 'Foo.xcworkspace') } it 'returns true' do expect(repository.xcode_project?).to be_truthy end end - context 'when the root contains no XCode config file' do - let(:blob) { double(:blob, path: 'subdir/Foo.xcworkspace') } + context 'when the root contains no Xcode config directory' do + let(:tree) { double(:tree, path: 'Foo') } it 'returns false' do expect(repository.xcode_project?).to be_falsey diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ad094b3ed48..6a2f4a39f09 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -39,7 +39,7 @@ describe User do it { is_expected.to have_many(:builds).dependent(:nullify) } it { is_expected.to have_many(:pipelines).dependent(:nullify) } it { is_expected.to have_many(:chat_names).dependent(:destroy) } - it { is_expected.to have_many(:uploads).dependent(:destroy) } + it { is_expected.to have_many(:uploads) } it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') } it { is_expected.to have_many(:custom_attributes).class_name('UserCustomAttribute') } @@ -393,24 +393,6 @@ describe User do end describe 'after commit hook' do - describe '.update_invalid_gpg_signatures' do - let(:user) do - create(:user, email: 'tula.torphy@abshire.ca').tap do |user| - user.skip_reconfirmation! - end - end - - it 'does nothing when the name is updated' do - expect(user).not_to receive(:update_invalid_gpg_signatures) - user.update_attributes!(name: 'Bette') - end - - it 'synchronizes the gpg keys when the email is updated' do - expect(user).to receive(:update_invalid_gpg_signatures).at_most(:twice) - user.update_attributes!(email: 'shawnee.ritchie@denesik.com') - end - end - describe '#update_emails_with_primary_email' do before do @user = create(:user, email: 'primary@example.com').tap do |user| @@ -450,6 +432,76 @@ describe User do expect(@user.emails.first.confirmed_at).not_to eq nil end end + + describe '#update_notification_email' do + # Regression: https://gitlab.com/gitlab-org/gitlab-ce/issues/22846 + context 'when changing :email' do + let(:user) { create(:user) } + let(:new_email) { 'new-email@example.com' } + + it 'sets :unconfirmed_email' do + expect do + user.tap { |u| u.update!(email: new_email) }.reload + end.to change(user, :unconfirmed_email).to(new_email) + end + + it 'does not change :notification_email' do + expect do + user.tap { |u| u.update!(email: new_email) }.reload + end.not_to change(user, :notification_email) + end + + it 'updates :notification_email to the new email once confirmed' do + user.update!(email: new_email) + + expect do + user.tap(&:confirm).reload + end.to change(user, :notification_email).to eq(new_email) + end + + context 'and :notification_email is set to a secondary email' do + let!(:email_attrs) { attributes_for(:email, :confirmed, user: user) } + let(:secondary) { create(:email, :confirmed, email: 'secondary@example.com', user: user) } + + before do + user.emails.create(email_attrs) + user.tap { |u| u.update!(notification_email: email_attrs[:email]) }.reload + end + + it 'does not change :notification_email to :email' do + expect do + user.tap { |u| u.update!(email: new_email) }.reload + end.not_to change(user, :notification_email) + end + + it 'does not change :notification_email to :email once confirmed' do + user.update!(email: new_email) + + expect do + user.tap(&:confirm).reload + end.not_to change(user, :notification_email) + end + end + end + end + + describe '#update_invalid_gpg_signatures' do + let(:user) do + create(:user, email: 'tula.torphy@abshire.ca').tap do |user| + user.skip_reconfirmation! + end + end + + it 'does nothing when the name is updated' do + expect(user).not_to receive(:update_invalid_gpg_signatures) + user.update_attributes!(name: 'Bette') + end + + it 'synchronizes the gpg keys when the email is updated' do + expect(user).to receive(:update_invalid_gpg_signatures).at_most(:twice) + user.update_attributes!(email: 'shawnee.ritchie@denesik.com') + end + end end describe '#update_tracked_fields!', :clean_gitlab_redis_shared_state do @@ -1223,6 +1275,24 @@ describe User do end end + describe '#accept_pending_invitations!' do + let(:user) { create(:user, email: 'user@email.com') } + let!(:project_member_invite) { create(:project_member, :invited, invite_email: user.email) } + let!(:group_member_invite) { create(:group_member, :invited, invite_email: user.email) } + let!(:external_project_member_invite) { create(:project_member, :invited, invite_email: 'external@email.com') } + let!(:external_group_member_invite) { create(:group_member, :invited, invite_email: 'external@email.com') } + + it 'accepts all the user members pending invitations and returns the accepted_members' do + accepted_members = user.accept_pending_invitations! + + expect(accepted_members).to match_array([project_member_invite, group_member_invite]) + expect(group_member_invite.reload).not_to be_invite + expect(project_member_invite.reload).not_to be_invite + expect(external_project_member_invite.reload).to be_invite + expect(external_group_member_invite.reload).to be_invite + end + end + describe '#all_emails' do let(:user) { create(:user) } @@ -1786,28 +1856,54 @@ describe User do end end - describe '#ci_authorized_runners' do + describe '#ci_owned_runners' do let(:user) { create(:user) } - let(:runner) { create(:ci_runner) } + let(:runner_1) { create(:ci_runner) } + let(:runner_2) { create(:ci_runner) } - before do - project.runners << runner - end - - context 'without any projects' do - let(:project) { create(:project) } + context 'without any projects nor groups' do + let!(:project) { create(:project, runners: [runner_1]) } + let!(:group) { create(:group) } it 'does not load' do - expect(user.ci_authorized_runners).to be_empty + expect(user.ci_owned_runners).to be_empty end end context 'with personal projects runners' do let(:namespace) { create(:namespace, owner: user) } - let(:project) { create(:project, namespace: namespace) } + let!(:project) { create(:project, namespace: namespace, runners: [runner_1]) } + + it 'loads' do + expect(user.ci_owned_runners).to contain_exactly(runner_1) + end + end + + context 'with personal group runner' do + let!(:project) { create(:project, runners: [runner_1]) } + let!(:group) do + create(:group, runners: [runner_2]).tap do |group| + group.add_owner(user) + end + end + + it 'loads' do + expect(user.ci_owned_runners).to contain_exactly(runner_2) + end + end + + context 'with personal project and group runner' do + let(:namespace) { create(:namespace, owner: user) } + let!(:project) { create(:project, namespace: namespace, runners: [runner_1]) } + + let!(:group) do + create(:group, runners: [runner_2]).tap do |group| + group.add_owner(user) + end + end it 'loads' do - expect(user.ci_authorized_runners).to contain_exactly(runner) + expect(user.ci_owned_runners).to contain_exactly(runner_1, runner_2) end end @@ -1818,7 +1914,7 @@ describe User do end it 'loads' do - expect(user.ci_authorized_runners).to contain_exactly(runner) + expect(user.ci_owned_runners).to contain_exactly(runner_1) end end @@ -1828,14 +1924,28 @@ describe User do end it 'does not load' do - expect(user.ci_authorized_runners).to be_empty + expect(user.ci_owned_runners).to be_empty end end end context 'with groups projects runners' do let(:group) { create(:group) } - let(:project) { create(:project, group: group) } + let!(:project) { create(:project, group: group, runners: [runner_1]) } + + def add_user(access) + group.add_user(user, access) + end + + it_behaves_like :member + end + + context 'with groups runners' do + let!(:group) do + create(:group, runners: [runner_1]).tap do |group| + group.add_owner(user) + end + end def add_user(access) group.add_user(user, access) @@ -1845,7 +1955,7 @@ describe User do end context 'with other projects runners' do - let(:project) { create(:project) } + let!(:project) { create(:project, runners: [runner_1]) } def add_user(access) project.add_role(user, access) @@ -1858,7 +1968,7 @@ describe User do let(:group) { create(:group) } let(:another_user) { create(:user) } let(:subgroup) { create(:group, parent: group) } - let(:project) { create(:project, group: subgroup) } + let!(:project) { create(:project, group: subgroup, runners: [runner_1]) } def add_user(access) group.add_user(user, access) @@ -2755,4 +2865,26 @@ describe User do it { is_expected.to be_truthy } end end + + describe '#increment_failed_attempts!' do + subject(:user) { create(:user, failed_attempts: 0) } + + it 'logs failed sign-in attempts' do + expect { user.increment_failed_attempts! }.to change(user, :failed_attempts).from(0).to(1) + end + + it 'does not log failed sign-in attempts when in a GitLab read-only instance' do + allow(Gitlab::Database).to receive(:read_only?) { true } + + expect { user.increment_failed_attempts! }.not_to change(user, :failed_attempts) + end + end + + context 'with uploads' do + it_behaves_like 'model with mounted uploader', false do + let(:model_object) { create(:user, :with_avatar) } + let(:upload_attribute) { :avatar } + let(:uploader_class) { AttachmentUploader } + end + end end diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb index 41cf2ef7225..9ca156deaa0 100644 --- a/spec/policies/ci/build_policy_spec.rb +++ b/spec/policies/ci/build_policy_spec.rb @@ -94,6 +94,19 @@ describe Ci::BuildPolicy do end end end + + context 'when maintainer is allowed to push to pipeline branch' do + let(:project) { create(:project, :public) } + let(:owner) { user } + + it 'enables update_build if user is maintainer' do + allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false) + allow_any_instance_of(Project).to receive(:branch_allows_maintainer_push?).and_return(true) + + expect(policy).to be_allowed :update_build + expect(policy).to be_allowed :update_commit_status + end + end end describe 'rules for protected ref' do diff --git a/spec/policies/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb index 48a8064c5fc..a5e509cfa0f 100644 --- a/spec/policies/ci/pipeline_policy_spec.rb +++ b/spec/policies/ci/pipeline_policy_spec.rb @@ -62,5 +62,17 @@ describe Ci::PipelinePolicy, :models do end end end + + context 'when maintainer is allowed to push to pipeline branch' do + let(:project) { create(:project, :public) } + let(:owner) { user } + + it 'enables update_pipeline if user is maintainer' do + allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false) + allow_any_instance_of(Project).to receive(:branch_allows_maintainer_push?).and_return(true) + + expect(policy).to be_allowed :update_pipeline + end + end end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 8b9c4ac0b4b..6609f5f7afd 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -404,7 +404,7 @@ describe ProjectPolicy do ) end let(:maintainer_abilities) do - %w(create_build update_build create_pipeline update_pipeline) + %w(create_build create_pipeline) end subject { described_class.new(user, project) } diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb index 4bc005df2fc..efd175247b5 100644 --- a/spec/presenters/ci/build_presenter_spec.rb +++ b/spec/presenters/ci/build_presenter_spec.rb @@ -10,7 +10,7 @@ describe Ci::BuildPresenter do end it 'inherits from Gitlab::View::Presenter::Delegated' do - expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated) + expect(described_class.ancestors).to include(Gitlab::View::Presenter::Delegated) end describe '#initialize' do diff --git a/spec/presenters/commit_status_presenter_spec.rb b/spec/presenters/commit_status_presenter_spec.rb new file mode 100644 index 00000000000..f81ee44e371 --- /dev/null +++ b/spec/presenters/commit_status_presenter_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe CommitStatusPresenter do + let(:project) { create(:project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } + + subject(:presenter) do + described_class.new(build) + end + + it 'inherits from Gitlab::View::Presenter::Delegated' do + expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated) + end +end diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb index e3b37739e8e..d5fb4a7742c 100644 --- a/spec/presenters/merge_request_presenter_spec.rb +++ b/spec/presenters/merge_request_presenter_spec.rb @@ -70,6 +70,41 @@ describe MergeRequestPresenter do end end + describe "#unmergeable_reasons" do + let(:presenter) { described_class.new(resource, current_user: user) } + + before do + # Mergeable base state + allow(resource).to receive(:has_no_commits?).and_return(false) + allow(resource).to receive(:source_branch_exists?).and_return(true) + allow(resource).to receive(:target_branch_exists?).and_return(true) + allow(resource.project.repository).to receive(:can_be_merged?).and_return(true) + end + + it "handles mergeable request" do + expect(presenter.unmergeable_reasons).to eq([]) + end + + it "handles no commit" do + allow(resource).to receive(:has_no_commits?).and_return(true) + + expect(presenter.unmergeable_reasons).to eq(["no commits"]) + end + + it "handles branches missing" do + allow(resource).to receive(:source_branch_exists?).and_return(false) + allow(resource).to receive(:target_branch_exists?).and_return(false) + + expect(presenter.unmergeable_reasons).to eq(["source branch is missing", "target branch is missing"]) + end + + it "handles merge conflict" do + allow(resource.project.repository).to receive(:can_be_merged?).and_return(false) + + expect(presenter.unmergeable_reasons).to eq(["has merge conflicts"]) + end + end + describe '#conflict_resolution_path' do let(:project) { create :project } let(:user) { create :user } diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 53d48a91007..fdddca5d0ef 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -15,7 +15,7 @@ describe API::Environments do it 'returns project environments' do project_data_keys = %w( id description default_branch tag_list - ssh_url_to_repo http_url_to_repo web_url + ssh_url_to_repo http_url_to_repo web_url readme_url name name_with_namespace path path_with_namespace star_count forks_count diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index bb0034e3237..7d923932309 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -738,13 +738,16 @@ describe API::Groups do describe "DELETE /groups/:id" do context "when authenticated as user" do it "removes group" do - delete api("/groups/#{group1.id}", user1) + Sidekiq::Testing.fake! do + expect { delete api("/groups/#{group1.id}", user1) }.to change(GroupDestroyWorker.jobs, :size).by(1) + end - expect(response).to have_gitlab_http_status(204) + expect(response).to have_gitlab_http_status(202) end it_behaves_like '412 response' do let(:request) { api("/groups/#{group1.id}", user1) } + let(:success_status) { 202 } end it "does not remove a group if not an owner" do @@ -773,7 +776,7 @@ describe API::Groups do it "removes any existing group" do delete api("/groups/#{group2.id}", admin) - expect(response).to have_gitlab_http_status(204) + expect(response).to have_gitlab_http_status(202) end it "does not remove a non existing group" do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 90f9c4ad214..3106083293f 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -64,12 +64,32 @@ describe API::Issues do describe "GET /issues" do context "when unauthenticated" do - it "returns authentication error" do + it "returns an array of all issues" do + get api("/issues"), scope: 'all' + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + end + + it "returns authentication error without any scope" do get api("/issues") - expect(response).to have_gitlab_http_status(401) + expect(response).to have_http_status(401) + end + + it "returns authentication error when scope is assigned-to-me" do + get api("/issues"), scope: 'assigned-to-me' + + expect(response).to have_http_status(401) + end + + it "returns authentication error when scope is created-by-me" do + get api("/issues"), scope: 'created-by-me' + + expect(response).to have_http_status(401) end end + context "when authenticated" do let(:first_issue) { json_response.first } @@ -106,6 +126,15 @@ describe API::Issues do it 'returns issues assigned to me' do issue2 = create(:issue, assignees: [user2], project: project) + get api('/issues', user2), scope: 'assigned_to_me' + + expect_paginated_array_response(size: 1) + expect(first_issue['id']).to eq(issue2.id) + end + + it 'returns issues assigned to me (kebab-case)' do + issue2 = create(:issue, assignees: [user2], project: project) + get api('/issues', user2), scope: 'assigned-to-me' expect_paginated_array_response(size: 1) @@ -379,9 +408,6 @@ describe API::Issues do end let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) } - before do - group_project.add_reporter(user) - end let(:base_url) { "/groups/#{group.id}/issues" } context 'when group has subgroups', :nested_groups do @@ -408,178 +434,201 @@ describe API::Issues do end end - it 'returns all group issues (including opened and closed)' do - get api(base_url, admin) + context 'when user is unauthenticated' do + it 'lists all issues in public projects' do + get api(base_url) - expect_paginated_array_response(size: 3) + expect_paginated_array_response(size: 2) + end end - it 'returns group issues without confidential issues for non project members' do - get api("#{base_url}?state=opened", non_member) + context 'when user is a group member' do + before do + group_project.add_reporter(user) + end - expect_paginated_array_response(size: 1) - expect(json_response.first['title']).to eq(group_issue.title) - end + it 'returns all group issues (including opened and closed)' do + get api(base_url, admin) - it 'returns group confidential issues for author' do - get api("#{base_url}?state=opened", author) + expect_paginated_array_response(size: 3) + end - expect_paginated_array_response(size: 2) - end + it 'returns group issues without confidential issues for non project members' do + get api("#{base_url}?state=opened", non_member) - it 'returns group confidential issues for assignee' do - get api("#{base_url}?state=opened", assignee) + expect_paginated_array_response(size: 1) + expect(json_response.first['title']).to eq(group_issue.title) + end - expect_paginated_array_response(size: 2) - end + it 'returns group confidential issues for author' do + get api("#{base_url}?state=opened", author) - it 'returns group issues with confidential issues for project members' do - get api("#{base_url}?state=opened", user) + expect_paginated_array_response(size: 2) + end - expect_paginated_array_response(size: 2) - end + it 'returns group confidential issues for assignee' do + get api("#{base_url}?state=opened", assignee) - it 'returns group confidential issues for admin' do - get api("#{base_url}?state=opened", admin) + expect_paginated_array_response(size: 2) + end - expect_paginated_array_response(size: 2) - end + it 'returns group issues with confidential issues for project members' do + get api("#{base_url}?state=opened", user) - it 'returns an array of labeled group issues' do - get api("#{base_url}?labels=#{group_label.title}", user) + expect_paginated_array_response(size: 2) + end - expect_paginated_array_response(size: 1) - expect(json_response.first['labels']).to eq([group_label.title]) - end + it 'returns group confidential issues for admin' do + get api("#{base_url}?state=opened", admin) - it 'returns an array of labeled group issues where all labels match' do - get api("#{base_url}?labels=#{group_label.title},foo,bar", user) + expect_paginated_array_response(size: 2) + end - expect_paginated_array_response(size: 0) - end + it 'returns an array of labeled group issues' do + get api("#{base_url}?labels=#{group_label.title}", user) - it 'returns issues matching given search string for title' do - get api("#{base_url}?search=#{group_issue.title}", user) + expect_paginated_array_response(size: 1) + expect(json_response.first['labels']).to eq([group_label.title]) + end - expect_paginated_array_response(size: 1) - expect(json_response.first['id']).to eq(group_issue.id) - end + it 'returns an array of labeled group issues where all labels match' do + get api("#{base_url}?labels=#{group_label.title},foo,bar", user) - it 'returns issues matching given search string for description' do - get api("#{base_url}?search=#{group_issue.description}", user) + expect_paginated_array_response(size: 0) + end - expect_paginated_array_response(size: 1) - expect(json_response.first['id']).to eq(group_issue.id) - end + it 'returns issues matching given search string for title' do + get api("#{base_url}?search=#{group_issue.title}", user) - it 'returns an array of labeled issues when all labels matches' do - label_b = create(:label, title: 'foo', project: group_project) - label_c = create(:label, title: 'bar', project: group_project) + expect_paginated_array_response(size: 1) + expect(json_response.first['id']).to eq(group_issue.id) + end - create(:label_link, label: label_b, target: group_issue) - create(:label_link, label: label_c, target: group_issue) + it 'returns issues matching given search string for description' do + get api("#{base_url}?search=#{group_issue.description}", user) - get api("#{base_url}", user), labels: "#{group_label.title},#{label_b.title},#{label_c.title}" + expect_paginated_array_response(size: 1) + expect(json_response.first['id']).to eq(group_issue.id) + end - expect_paginated_array_response(size: 1) - expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title]) - end + it 'returns an array of labeled issues when all labels matches' do + label_b = create(:label, title: 'foo', project: group_project) + label_c = create(:label, title: 'bar', project: group_project) - it 'returns an array of issues found by iids' do - get api(base_url, user), iids: [group_issue.iid] + create(:label_link, label: label_b, target: group_issue) + create(:label_link, label: label_c, target: group_issue) - expect_paginated_array_response(size: 1) - expect(json_response.first['id']).to eq(group_issue.id) - end + get api("#{base_url}", user), labels: "#{group_label.title},#{label_b.title},#{label_c.title}" - it 'returns an empty array if iid does not exist' do - get api(base_url, user), iids: [99999] + expect_paginated_array_response(size: 1) + expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title]) + end - expect_paginated_array_response(size: 0) - end + it 'returns an array of issues found by iids' do + get api(base_url, user), iids: [group_issue.iid] - it 'returns an empty array if no group issue matches labels' do - get api("#{base_url}?labels=foo,bar", user) + expect_paginated_array_response(size: 1) + expect(json_response.first['id']).to eq(group_issue.id) + end - expect_paginated_array_response(size: 0) - end + it 'returns an empty array if iid does not exist' do + get api(base_url, user), iids: [99999] - it 'returns an empty array if no issue matches milestone' do - get api("#{base_url}?milestone=#{group_empty_milestone.title}", user) + expect_paginated_array_response(size: 0) + end - expect_paginated_array_response(size: 0) - end + it 'returns an empty array if no group issue matches labels' do + get api("#{base_url}?labels=foo,bar", user) - it 'returns an empty array if milestone does not exist' do - get api("#{base_url}?milestone=foo", user) + expect_paginated_array_response(size: 0) + end - expect_paginated_array_response(size: 0) - end + it 'returns an empty array if no issue matches milestone' do + get api("#{base_url}?milestone=#{group_empty_milestone.title}", user) - it 'returns an array of issues in given milestone' do - get api("#{base_url}?state=opened&milestone=#{group_milestone.title}", user) + expect_paginated_array_response(size: 0) + end - expect_paginated_array_response(size: 1) - expect(json_response.first['id']).to eq(group_issue.id) - end + it 'returns an empty array if milestone does not exist' do + get api("#{base_url}?milestone=foo", user) - it 'returns an array of issues matching state in milestone' do - get api("#{base_url}?milestone=#{group_milestone.title}"\ - '&state=closed', user) + expect_paginated_array_response(size: 0) + end - expect_paginated_array_response(size: 1) - expect(json_response.first['id']).to eq(group_closed_issue.id) - end + it 'returns an array of issues in given milestone' do + get api("#{base_url}?state=opened&milestone=#{group_milestone.title}", user) - it 'returns an array of issues with no milestone' do - get api("#{base_url}?milestone=#{no_milestone_title}", user) + expect_paginated_array_response(size: 1) + expect(json_response.first['id']).to eq(group_issue.id) + end - expect(response).to have_gitlab_http_status(200) + it 'returns an array of issues matching state in milestone' do + get api("#{base_url}?milestone=#{group_milestone.title}"\ + '&state=closed', user) - expect_paginated_array_response(size: 1) - expect(json_response.first['id']).to eq(group_confidential_issue.id) - end + expect_paginated_array_response(size: 1) + expect(json_response.first['id']).to eq(group_closed_issue.id) + end - it 'sorts by created_at descending by default' do - get api(base_url, user) + it 'returns an array of issues with no milestone' do + get api("#{base_url}?milestone=#{no_milestone_title}", user) - response_dates = json_response.map { |issue| issue['created_at'] } + expect(response).to have_gitlab_http_status(200) - expect_paginated_array_response(size: 3) - expect(response_dates).to eq(response_dates.sort.reverse) - end + expect_paginated_array_response(size: 1) + expect(json_response.first['id']).to eq(group_confidential_issue.id) + end - it 'sorts ascending when requested' do - get api("#{base_url}?sort=asc", user) + it 'sorts by created_at descending by default' do + get api(base_url, user) - response_dates = json_response.map { |issue| issue['created_at'] } + response_dates = json_response.map { |issue| issue['created_at'] } - expect_paginated_array_response(size: 3) - expect(response_dates).to eq(response_dates.sort) - end + expect_paginated_array_response(size: 3) + expect(response_dates).to eq(response_dates.sort.reverse) + end - it 'sorts by updated_at descending when requested' do - get api("#{base_url}?order_by=updated_at", user) + it 'sorts ascending when requested' do + get api("#{base_url}?sort=asc", user) - response_dates = json_response.map { |issue| issue['updated_at'] } + response_dates = json_response.map { |issue| issue['created_at'] } - expect_paginated_array_response(size: 3) - expect(response_dates).to eq(response_dates.sort.reverse) - end + expect_paginated_array_response(size: 3) + expect(response_dates).to eq(response_dates.sort) + end - it 'sorts by updated_at ascending when requested' do - get api("#{base_url}?order_by=updated_at&sort=asc", user) + it 'sorts by updated_at descending when requested' do + get api("#{base_url}?order_by=updated_at", user) - response_dates = json_response.map { |issue| issue['updated_at'] } + response_dates = json_response.map { |issue| issue['updated_at'] } - expect_paginated_array_response(size: 3) - expect(response_dates).to eq(response_dates.sort) + expect_paginated_array_response(size: 3) + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts by updated_at ascending when requested' do + get api("#{base_url}?order_by=updated_at&sort=asc", user) + + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect_paginated_array_response(size: 3) + expect(response_dates).to eq(response_dates.sort) + end end end describe "GET /projects/:id/issues" do let(:base_url) { "/projects/#{project.id}" } + context 'when unauthenticated' do + it 'returns public project issues' do + get api("/projects/#{project.id}/issues") + + expect_paginated_array_response(size: 2) + expect(json_response.first['title']).to eq(issue.title) + end + end + it 'avoids N+1 queries' do control_count = ActiveRecord::QueryRecorder.new do get api("/projects/#{project.id}/issues", user) @@ -789,6 +838,14 @@ describe API::Issues do end describe "GET /projects/:id/issues/:issue_iid" do + context 'when unauthenticated' do + it 'returns public issues' do + get api("/projects/#{project.id}/issues/#{issue.iid}") + + expect(response).to have_gitlab_http_status(200) + end + end + it 'exposes known attributes' do get api("/projects/#{project.id}/issues/#{issue.iid}", user) @@ -1581,6 +1638,14 @@ describe API::Issues do create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request) end + context 'when unauthenticated' do + it 'return public project issues' do + get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by") + + expect_paginated_array_response(size: 1) + end + end + it 'returns merge requests that will close issue on merge' do get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by", user) @@ -1605,6 +1670,14 @@ describe API::Issues do describe "GET /projects/:id/issues/:issue_iid/user_agent_detail" do let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) } + context 'when unauthenticated' do + it "returns unautorized" do + get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail") + + expect(response).to have_gitlab_http_status(401) + end + end + it 'exposes known attributes' do get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", admin) diff --git a/spec/requests/api/markdown_spec.rb b/spec/requests/api/markdown_spec.rb new file mode 100644 index 00000000000..a55796cf343 --- /dev/null +++ b/spec/requests/api/markdown_spec.rb @@ -0,0 +1,112 @@ +require "spec_helper" + +describe API::Markdown do + RSpec::Matchers.define_negated_matcher :exclude, :include + + describe "POST /markdown" do + let(:user) {} # No-op. It gets overwritten in the contexts below. + + before do + post api("/markdown", user), params + end + + shared_examples "rendered markdown text without GFM" do + it "renders markdown text" do + expect(response).to have_http_status(201) + expect(response.headers["Content-Type"]).to eq("application/json") + expect(json_response).to be_a(Hash) + expect(json_response["html"]).to eq("<p>#{text}</p>") + end + end + + shared_examples "404 Project Not Found" do + it "responses with 404 Not Found" do + expect(response).to have_http_status(404) + expect(response.headers["Content-Type"]).to eq("application/json") + expect(json_response).to be_a(Hash) + expect(json_response["message"]).to eq("404 Project Not Found") + end + end + + context "when arguments are invalid" do + context "when text is missing" do + let(:params) { {} } + + it "responses with 400 Bad Request" do + expect(response).to have_http_status(400) + expect(response.headers["Content-Type"]).to eq("application/json") + expect(json_response).to be_a(Hash) + expect(json_response["error"]).to eq("text is missing") + end + end + + context "when project is not found" do + let(:params) { { text: "Hello world!", gfm: true, project: "Dummy project" } } + + it_behaves_like "404 Project Not Found" + end + end + + context "when arguments are valid" do + set(:project) { create(:project) } + set(:issue) { create(:issue, project: project) } + let(:text) { ":tada: Hello world! :100: #{issue.to_reference}" } + + context "when not using gfm" do + context "without project" do + let(:params) { { text: text } } + + it_behaves_like "rendered markdown text without GFM" + end + + context "with project" do + let(:params) { { text: text, project: project.full_path } } + + context "when not authorized" do + it_behaves_like "404 Project Not Found" + end + + context "when authorized" do + let(:user) { project.owner } + + it_behaves_like "rendered markdown text without GFM" + end + end + end + + context "when using gfm" do + context "without project" do + let(:params) { { text: text, gfm: true } } + + it "renders markdown text" do + expect(response).to have_http_status(201) + expect(response.headers["Content-Type"]).to eq("application/json") + expect(json_response).to be_a(Hash) + expect(json_response["html"]).to include("Hello world!") + .and include('data-name="tada"') + .and include('data-name="100"') + .and include("#1") + .and exclude("<a href=\"#{IssuesHelper.url_for_issue(issue.iid, project)}\"") + .and exclude("#1</a>") + end + end + + context "with project" do + let(:params) { { text: text, gfm: true, project: project.full_path } } + let(:user) { project.owner } + + it "renders markdown text" do + expect(response).to have_http_status(201) + expect(response.headers["Content-Type"]).to eq("application/json") + expect(json_response).to be_a(Hash) + expect(json_response["html"]).to include("Hello world!") + .and include('data-name="tada"') + .and include('data-name="100"') + .and include("<a href=\"#{IssuesHelper.url_for_issue(issue.iid, project)}\"") + .and include("#1</a>") + end + end + end + end + end +end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index f64623d7018..1eeeb4f1045 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -34,8 +34,7 @@ describe API::MergeRequests do it 'returns an array of all merge requests' do get api('/merge_requests', user), scope: 'all' - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array + expect_paginated_array_response end it "returns authentication error without any scope" do @@ -50,11 +49,23 @@ describe API::MergeRequests do expect(response).to have_gitlab_http_status(401) end + it "returns authentication error when scope is assigned_to_me" do + get api("/merge_requests"), scope: 'assigned_to_me' + + expect(response).to have_gitlab_http_status(401) + end + it "returns authentication error when scope is created-by-me" do get api("/merge_requests"), scope: 'created-by-me' expect(response).to have_gitlab_http_status(401) end + + it "returns authentication error when scope is created_by_me" do + get api("/merge_requests"), scope: 'created_by_me' + + expect(response).to have_gitlab_http_status(401) + end end context 'when authenticated' do @@ -62,27 +73,14 @@ describe API::MergeRequests do let!(:merge_request2) { create(:merge_request, :simple, author: user, assignee: user, source_project: project2, target_project: project2) } let(:user2) { create(:user) } - it 'returns an array of all merge requests' do - get api('/merge_requests', user), scope: :all - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.map { |mr| mr['id'] }) - .to contain_exactly(merge_request.id, merge_request_closed.id, merge_request_merged.id, merge_request2.id) - end - - it 'does not return unauthorized merge requests' do + it 'returns an array of all merge requests except unauthorized ones' do private_project = create(:project, :private) merge_request3 = create(:merge_request, :simple, source_project: private_project, target_project: private_project, source_branch: 'other-branch') get api('/merge_requests', user), scope: :all - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.map { |mr| mr['id'] }) - .not_to include(merge_request3.id) + expect_response_contain_exactly(merge_request2, merge_request_merged, merge_request_closed, merge_request) + expect(json_response.map { |mr| mr['id'] }).not_to include(merge_request3.id) end it 'returns an array of merge requests created by current user if no scope is given' do @@ -90,10 +88,7 @@ describe API::MergeRequests do get api('/merge_requests', user2) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(merge_request3.id) + expect_response_ordered_exactly(merge_request3) end it 'returns an array of merge requests authored by the given user' do @@ -101,10 +96,7 @@ describe API::MergeRequests do get api('/merge_requests', user), author_id: user2.id, scope: :all - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(merge_request3.id) + expect_response_ordered_exactly(merge_request3) end it 'returns an array of merge requests assigned to the given user' do @@ -112,32 +104,39 @@ describe API::MergeRequests do get api('/merge_requests', user), assignee_id: user2.id, scope: :all - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(merge_request3.id) + expect_response_ordered_exactly(merge_request3) end it 'returns an array of merge requests assigned to me' do merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch') + get api('/merge_requests', user2), scope: 'assigned_to_me' + + expect_response_ordered_exactly(merge_request3) + end + + it 'returns an array of merge requests assigned to me (kebab-case)' do + merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch') + get api('/merge_requests', user2), scope: 'assigned-to-me' - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(merge_request3.id) + expect_response_ordered_exactly(merge_request3) end it 'returns an array of merge requests created by me' do merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch') + get api('/merge_requests', user2), scope: 'created_by_me' + + expect_response_ordered_exactly(merge_request3) + end + + it 'returns an array of merge requests created by me (kebab-case)' do + merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch') + get api('/merge_requests', user2), scope: 'created-by-me' - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(merge_request3.id) + expect_response_ordered_exactly(merge_request3) end it 'returns merge requests reacted by the authenticated user by the given emoji' do @@ -146,19 +145,14 @@ describe API::MergeRequests do get api('/merge_requests', user2), my_reaction_emoji: award_emoji.name, scope: 'all' - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(merge_request3.id) + expect_response_ordered_exactly(merge_request3) end context 'source_branch param' do it 'returns merge requests with the given source branch' do get api('/merge_requests', user), source_branch: merge_request_closed.source_branch, state: 'all' - expect(json_response.length).to eq(2) - expect(json_response.map { |mr| mr['id'] }) - .to contain_exactly(merge_request_closed.id, merge_request_merged.id) + expect_response_contain_exactly(merge_request_closed, merge_request_merged) end end @@ -166,9 +160,7 @@ describe API::MergeRequests do it 'returns merge requests with the given target branch' do get api('/merge_requests', user), target_branch: merge_request_closed.target_branch, state: 'all' - expect(json_response.length).to eq(2) - expect(json_response.map { |mr| mr['id'] }) - .to contain_exactly(merge_request_closed.id, merge_request_merged.id) + expect_response_contain_exactly(merge_request_closed, merge_request_merged) end end @@ -177,8 +169,7 @@ describe API::MergeRequests do get api('/merge_requests?created_before=2000-01-02T00:00:00.060Z', user) - expect(json_response.size).to eq(1) - expect(json_response.first['id']).to eq(merge_request2.id) + expect_response_ordered_exactly(merge_request2) end it 'returns merge requests created after a specific date' do @@ -186,8 +177,7 @@ describe API::MergeRequests do get api("/merge_requests?created_after=#{merge_request2.created_at}", user) - expect(json_response.size).to eq(1) - expect(json_response.first['id']).to eq(merge_request2.id) + expect_response_ordered_exactly(merge_request2) end it 'returns merge requests updated before a specific date' do @@ -195,8 +185,7 @@ describe API::MergeRequests do get api('/merge_requests?updated_before=2000-01-02T00:00:00.060Z', user) - expect(json_response.size).to eq(1) - expect(json_response.first['id']).to eq(merge_request2.id) + expect_response_ordered_exactly(merge_request2) end it 'returns merge requests updated after a specific date' do @@ -204,8 +193,7 @@ describe API::MergeRequests do get api("/merge_requests?updated_after=#{merge_request2.updated_at}", user) - expect(json_response.size).to eq(1) - expect(json_response.first['id']).to eq(merge_request2.id) + expect_response_ordered_exactly(merge_request2) end context 'search params' do @@ -216,15 +204,13 @@ describe API::MergeRequests do it 'returns merge requests matching given search string for title' do get api("/merge_requests", user), search: merge_request.title - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(merge_request.id) + expect_response_ordered_exactly(merge_request) end it 'returns merge requests for project matching given search string for description' do get api("/merge_requests", user), project_id: project.id, search: merge_request.description - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(merge_request.id) + expect_response_ordered_exactly(merge_request) end end end @@ -235,8 +221,7 @@ describe API::MergeRequests do it 'returns merge requests for public projects' do get api("/projects/#{project.id}/merge_requests") - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array + expect_paginated_array_response end it "returns 404 for non public projects" do @@ -265,10 +250,7 @@ describe API::MergeRequests do it "returns an array of all merge_requests" do get api("/projects/#{project.id}/merge_requests", user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) + expect_response_ordered_exactly(merge_request_merged, merge_request_closed, merge_request) expect(json_response.last['title']).to eq(merge_request.title) expect(json_response.last).to have_key('web_url') expect(json_response.last['sha']).to eq(merge_request.diff_head_sha) @@ -286,11 +268,8 @@ describe API::MergeRequests do it "returns an array of all merge_requests using simple mode" do get api("/projects/#{project.id}/merge_requests?view=simple", user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers + expect_response_ordered_exactly(merge_request_merged, merge_request_closed, merge_request) expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at)) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) expect(json_response.last['iid']).to eq(merge_request.iid) expect(json_response.last['title']).to eq(merge_request.title) expect(json_response.last).to have_key('web_url') @@ -302,51 +281,36 @@ describe API::MergeRequests do it "returns an array of all merge_requests" do get api("/projects/#{project.id}/merge_requests?state", user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) + expect_response_ordered_exactly(merge_request_merged, merge_request_closed, merge_request) expect(json_response.last['title']).to eq(merge_request.title) end it "returns an array of open merge_requests" do get api("/projects/#{project.id}/merge_requests?state=opened", user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) + expect_response_ordered_exactly(merge_request) expect(json_response.last['title']).to eq(merge_request.title) end it "returns an array of closed merge_requests" do get api("/projects/#{project.id}/merge_requests?state=closed", user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) + expect_response_ordered_exactly(merge_request_closed) expect(json_response.first['title']).to eq(merge_request_closed.title) end it "returns an array of merged merge_requests" do get api("/projects/#{project.id}/merge_requests?state=merged", user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) + expect_response_ordered_exactly(merge_request_merged) expect(json_response.first['title']).to eq(merge_request_merged.title) end it 'returns merge_request by "iids" array' do get api("/projects/#{project.id}/merge_requests", user), iids: [merge_request.iid, merge_request_closed.iid] - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) + expect_response_ordered_exactly(merge_request_closed, merge_request) expect(json_response.first['title']).to eq merge_request_closed.title - expect(json_response.first['id']).to eq merge_request_closed.id end it 'matches V4 response schema' do @@ -359,16 +323,14 @@ describe API::MergeRequests do it 'returns an empty array if no issue matches milestone' do get api("/projects/#{project.id}/merge_requests", user), milestone: '1.0.0' - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array + expect_paginated_array_response expect(json_response.length).to eq(0) end it 'returns an empty array if milestone does not exist' do get api("/projects/#{project.id}/merge_requests", user), milestone: 'foo' - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array + expect_paginated_array_response expect(json_response.length).to eq(0) end @@ -382,17 +344,13 @@ describe API::MergeRequests do it 'returns an array of merge requests matching state in milestone' do get api("/projects/#{project.id}/merge_requests", user), milestone: '0.9', state: 'closed' - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(merge_request_closed.id) + expect_response_ordered_exactly(merge_request_closed) end it 'returns an array of labeled merge requests' do get api("/projects/#{project.id}/merge_requests?labels=#{label.title}", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array + expect_paginated_array_response expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([label2.title, label.title]) end @@ -400,16 +358,14 @@ describe API::MergeRequests do it 'returns an array of labeled merge requests where all labels match' do get api("/projects/#{project.id}/merge_requests?labels=#{label.title},foo,bar", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array + expect_paginated_array_response expect(json_response.length).to eq(0) end it 'returns an empty array if no merge request matches labels' do get api("/projects/#{project.id}/merge_requests?labels=foo,bar", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array + expect_paginated_array_response expect(json_response.length).to eq(0) end @@ -427,13 +383,12 @@ describe API::MergeRequests do get api("/projects/#{project.id}/merge_requests?labels=#{bug_label.title}&milestone=#{milestone1.title}&state=merged", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(mr2.id) + expect_response_ordered_exactly(mr2) end context "with ordering" do + let(:merge_requests) { [merge_request_merged, merge_request_closed, merge_request] } + before do @mr_later = mr_with_later_created_and_updated_at_time @mr_earlier = mr_with_earlier_created_and_updated_at_time @@ -442,45 +397,25 @@ describe API::MergeRequests do it "returns an array of merge_requests in ascending order" do get api("/projects/#{project.id}/merge_requests?sort=asc", user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - response_dates = json_response.map { |merge_request| merge_request['created_at'] } - expect(response_dates).to eq(response_dates.sort) + expect_response_ordered_exactly(*merge_requests.sort_by { |mr| mr['created_at'] }) end it "returns an array of merge_requests in descending order" do get api("/projects/#{project.id}/merge_requests?sort=desc", user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - response_dates = json_response.map { |merge_request| merge_request['created_at'] } - expect(response_dates).to eq(response_dates.sort.reverse) + expect_response_ordered_exactly(*merge_requests.sort_by { |mr| mr['created_at'] }.reverse) end it "returns an array of merge_requests ordered by updated_at" do get api("/projects/#{project.id}/merge_requests?order_by=updated_at", user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - response_dates = json_response.map { |merge_request| merge_request['updated_at'] } - expect(response_dates).to eq(response_dates.sort.reverse) + expect_response_ordered_exactly(*merge_requests.sort_by { |mr| mr['updated_at'] }.reverse) end it "returns an array of merge_requests ordered by created_at" do get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) - response_dates = json_response.map { |merge_request| merge_request['created_at'] } - expect(response_dates).to eq(response_dates.sort) + expect_response_ordered_exactly(*merge_requests.sort_by { |mr| mr['created_at'] }) end end @@ -488,9 +423,7 @@ describe API::MergeRequests do it 'returns merge requests with the given source branch' do get api('/merge_requests', user), source_branch: merge_request_closed.source_branch, state: 'all' - expect(json_response.length).to eq(2) - expect(json_response.map { |mr| mr['id'] }) - .to contain_exactly(merge_request_closed.id, merge_request_merged.id) + expect_response_contain_exactly(merge_request_closed, merge_request_merged) end end @@ -498,9 +431,7 @@ describe API::MergeRequests do it 'returns merge requests with the given target branch' do get api('/merge_requests', user), target_branch: merge_request_closed.target_branch, state: 'all' - expect(json_response.length).to eq(2) - expect(json_response.map { |mr| mr['id'] }) - .to contain_exactly(merge_request_closed.id, merge_request_merged.id) + expect_response_contain_exactly(merge_request_closed, merge_request_merged) end end end @@ -1341,4 +1272,22 @@ describe API::MergeRequests do merge_request_closed.save merge_request_closed end + + def expect_response_contain_exactly(*items) + expect_paginated_array_response + expect(json_response.length).to eq(items.size) + expect(json_response.map { |element| element['id'] }).to contain_exactly(*items.map(&:id)) + end + + def expect_response_ordered_exactly(*items) + expect_paginated_array_response + expect(json_response.length).to eq(items.size) + expect(json_response.map { |element| element['id'] }).to eq(items.map(&:id)) + end + + def expect_paginated_array_response + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 85a571b8f0e..9b7c3205c1f 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -18,7 +18,7 @@ describe API::Projects do let(:user2) { create(:user) } let(:user3) { create(:user) } let(:admin) { create(:admin) } - let(:project) { create(:project, namespace: user.namespace) } + let(:project) { create(:project, :repository, namespace: user.namespace) } let(:project2) { create(:project, namespace: user.namespace) } let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') } let(:project_member) { create(:project_member, :developer, user: user3, project: project) } @@ -220,7 +220,7 @@ describe API::Projects do it 'returns a simplified version of all the projects' do expected_keys = %w( id description default_branch tag_list - ssh_url_to_repo http_url_to_repo web_url + ssh_url_to_repo http_url_to_repo web_url readme_url name name_with_namespace path path_with_namespace star_count forks_count @@ -854,6 +854,7 @@ describe API::Projects do expect(json_response['only_allow_merge_if_pipeline_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds) expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved) expect(json_response['merge_method']).to eq(project.merge_method.to_s) + expect(json_response['readme_url']).to eq(project.readme_url) end it 'returns a project by path name' do diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index da392c5ab81..efb9bddde44 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -918,6 +918,22 @@ describe API::Runner, :clean_gitlab_redis_shared_state do expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended' end + context 'when job is cancelled' do + before do + job.cancel + end + + context 'when trace is patched' do + before do + patch_the_trace + end + + it 'returns Forbidden ' do + expect(response.status).to eq(403) + end + end + end + context 'when redis data are flushed' do before do redis_shared_state_cleanup! diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index 981ac768e3a..c7587c877fc 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -27,7 +27,7 @@ describe API::Runners do end end - let!(:group_runner) { create(:ci_runner, description: 'Group runner', groups: [group]) } + let!(:group_runner) { create(:ci_runner, description: 'Group runner', groups: [group], runner_type: :group_type) } before do # Set project access for users @@ -48,7 +48,7 @@ describe API::Runners do expect(json_response).to be_an Array expect(json_response[0]).to have_key('ip_address') expect(descriptions).to contain_exactly( - 'Project runner', 'Two projects runner' + 'Project runner', 'Two projects runner', 'Group runner' ) expect(shared).to be_falsey end @@ -592,6 +592,15 @@ describe API::Runners do end.to change { project.runners.count }.by(+1) expect(response).to have_gitlab_http_status(201) end + + it 'enables a shared runner' do + expect do + post api("/projects/#{project.id}/runners", admin), runner_id: shared_runner.id + end.to change { project.runners.count }.by(1) + + expect(shared_runner.reload).not_to be_shared + expect(response).to have_gitlab_http_status(201) + end end context 'user is not admin' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index e8196980a8c..05637eb0729 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -488,10 +488,6 @@ describe API::Users do describe "PUT /users/:id" do let!(:admin_user) { create(:admin) } - before do - admin - end - it "updates user with new bio" do put api("/users/#{user.id}", admin), { bio: 'new test bio' } @@ -525,27 +521,28 @@ describe API::Users do expect(json_response['avatar_url']).to include(user.avatar_path) end - it 'updates user with his own email' do - put api("/users/#{user.id}", admin), email: user.email - - expect(response).to have_gitlab_http_status(200) - expect(json_response['email']).to eq(user.email) - expect(user.reload.email).to eq(user.email) - end - it 'updates user with a new email' do + old_email = user.email + old_notification_email = user.notification_email put api("/users/#{user.id}", admin), email: 'new@email.com' + user.reload + expect(response).to have_gitlab_http_status(200) - expect(user.reload.notification_email).to eq('new@email.com') + expect(user).to be_confirmed + expect(user.email).to eq(old_email) + expect(user.notification_email).to eq(old_notification_email) + expect(user.unconfirmed_email).to eq('new@email.com') end it 'skips reconfirmation when requested' do - put api("/users/#{user.id}", admin), { skip_reconfirmation: true } + put api("/users/#{user.id}", admin), email: 'new@email.com', skip_reconfirmation: true user.reload - expect(user.confirmed_at).to be_present + expect(response).to have_gitlab_http_status(200) + expect(user).to be_confirmed + expect(user.email).to eq('new@email.com') end it 'updates user with his own username' do diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb index a1cdf583de3..34d4b8e9565 100644 --- a/spec/requests/api/v3/groups_spec.rb +++ b/spec/requests/api/v3/groups_spec.rb @@ -458,9 +458,11 @@ describe API::V3::Groups do describe "DELETE /groups/:id" do context "when authenticated as user" do it "removes group" do - delete v3_api("/groups/#{group1.id}", user1) + Sidekiq::Testing.fake! do + expect { delete v3_api("/groups/#{group1.id}", user1) }.to change(GroupDestroyWorker.jobs, :size).by(1) + end - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(202) end it "does not remove a group if not an owner" do @@ -489,7 +491,7 @@ describe API::V3::Groups do it "removes any existing group" do delete v3_api("/groups/#{group2.id}", admin) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(202) end it "does not remove a non existing group" do diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index 4c25bd935c6..158ddf171bc 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -82,7 +82,7 @@ describe API::V3::Projects do it 'returns a simplified version of all the projects' do expected_keys = %w( id description default_branch tag_list - ssh_url_to_repo http_url_to_repo web_url + ssh_url_to_repo http_url_to_repo web_url readme_url name name_with_namespace path path_with_namespace star_count forks_count diff --git a/spec/requests/api/version_spec.rb b/spec/requests/api/version_spec.rb index 7bbf34422b8..38b618191fb 100644 --- a/spec/requests/api/version_spec.rb +++ b/spec/requests/api/version_spec.rb @@ -18,7 +18,7 @@ describe API::Version do expect(response).to have_gitlab_http_status(200) expect(json_response['version']).to eq(Gitlab::VERSION) - expect(json_response['revision']).to eq(Gitlab::REVISION) + expect(json_response['revision']).to eq(Gitlab.revision) end end end diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb index cd1a6cfc427..be286c490fe 100644 --- a/spec/requests/openid_connect_spec.rb +++ b/spec/requests/openid_connect_spec.rb @@ -159,7 +159,9 @@ describe 'OpenID Connect requests' do get '/.well-known/openid-configuration' expect(response).to have_gitlab_http_status(200) - expect(json_response).to have_key('issuer') + expect(json_response['issuer']).to eq('http://localhost') + expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys') + expect(json_response['scopes_supported']).to eq(%w[api read_user sudo read_repository openid]) end end end diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb index 2473c561f4b..e67d12b7a89 100644 --- a/spec/serializers/pipeline_entity_spec.rb +++ b/spec/serializers/pipeline_entity_spec.rb @@ -26,6 +26,13 @@ describe PipelineEntity do expect(subject).to include :updated_at, :created_at end + it 'excludes coverage data when disabled' do + entity = described_class + .represent(pipeline, request: request, disable_coverage: true) + + expect(entity.as_json).not_to include(:coverage) + end + it 'contains details' do expect(subject).to include :details expect(subject[:details]) diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index e88e86c2998..b741308e2c5 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -114,7 +114,9 @@ describe PipelineSerializer do Gitlab::GitalyClient.reset_counts end - shared_examples 'no N+1 queries' do + context 'with the same ref' do + let(:ref) { 'feature' } + it 'verifies number of queries', :request_store do recorded = ActiveRecord::QueryRecorder.new { subject } @@ -123,12 +125,6 @@ describe PipelineSerializer do end end - context 'with the same ref' do - let(:ref) { 'feature' } - - it_behaves_like 'no N+1 queries' - end - context 'with different refs' do def ref @sequence ||= 0 @@ -136,7 +132,16 @@ describe PipelineSerializer do "feature-#{@sequence}" end - it_behaves_like 'no N+1 queries' + it 'verifies number of queries', :request_store do + recorded = ActiveRecord::QueryRecorder.new { subject } + + # For each ref there is a permission check if maintainer can update + # pipeline. With the same ref this check is cached but if refs are + # different then there is an extra query per ref + # https://gitlab.com/gitlab-org/gitlab-ce/issues/46368 + expect(recorded.count).to be_within(1).of(51) + expect(recorded.cached_count).to eq(0) + end end def create_pipeline(status) diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 9a0b6efd8a9..2b88fcc9a96 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -395,7 +395,27 @@ describe Ci::CreatePipelineService do result = execute_service expect(result).to be_persisted - expect(Environment.find_by(name: "review/master")).not_to be_nil + expect(Environment.find_by(name: "review/master")).to be_present + end + end + + context 'with environment name including persisted variables' do + before do + config = YAML.dump( + deploy: { + environment: { name: "review/id1$CI_PIPELINE_ID/id2$CI_BUILD_ID" }, + script: 'ls' + } + ) + + stub_ci_pipeline_yaml_file(config) + end + + it 'skipps persisted variables in environment name' do + result = execute_service + + expect(result).to be_persisted + expect(Environment.find_by(name: "review/id1/id2")).to be_present end end diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb index f1acfc48468..a73bd7a0268 100644 --- a/spec/services/ci/retry_pipeline_service_spec.rb +++ b/spec/services/ci/retry_pipeline_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Ci::RetryPipelineService, '#execute' do + include ProjectForksHelper + let(:user) { create(:user) } let(:project) { create(:project) } let(:pipeline) { create(:ci_pipeline, project: project) } @@ -266,6 +268,33 @@ describe Ci::RetryPipelineService, '#execute' do end end + context 'when maintainer is allowed to push to forked project' do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:forked_project) { fork_project(project) } + let(:pipeline) { create(:ci_pipeline, project: forked_project, ref: 'fixes') } + + before do + project.add_master(user) + create(:merge_request, + source_project: forked_project, + target_project: project, + source_branch: 'fixes', + allow_maintainer_to_push: true) + create_build('rspec 1', :failed, 1) + end + + it 'allows to retry failed pipeline' do + allow_any_instance_of(Project).to receive(:fetch_branch_allows_maintainer_push?).and_return(true) + allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false) + + service.execute(pipeline) + + expect(build('rspec 1')).to be_pending + expect(pipeline.reload).to be_running + end + end + def statuses pipeline.reload.statuses end diff --git a/spec/services/clusters/applications/schedule_installation_service_spec.rb b/spec/services/clusters/applications/schedule_installation_service_spec.rb index 473dbcbb692..bca1e71bef2 100644 --- a/spec/services/clusters/applications/schedule_installation_service_spec.rb +++ b/spec/services/clusters/applications/schedule_installation_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Clusters::Applications::ScheduleInstallationService do def count_scheduled - application_class&.with_status(:scheduled)&.count || 0 + application&.class&.with_status(:scheduled)&.count || 0 end shared_examples 'a failing service' do @@ -10,45 +10,42 @@ describe Clusters::Applications::ScheduleInstallationService do expect(ClusterInstallAppWorker).not_to receive(:perform_async) count_before = count_scheduled - expect { service.execute }.to raise_error(StandardError) + expect { service.execute(application) }.to raise_error(StandardError) expect(count_scheduled).to eq(count_before) end end describe '#execute' do - let(:application_class) { Clusters::Applications::Helm } - let(:cluster) { create(:cluster, :project, :provided_by_gcp) } - let(:project) { cluster.project } - let(:service) { described_class.new(project, nil, cluster: cluster, application_class: application_class) } + let(:project) { double(:project) } + let(:service) { described_class.new(project, nil) } - it 'creates a new application' do - allow(ClusterInstallAppWorker).to receive(:perform_async) + context 'when application is installable' do + let(:application) { create(:clusters_applications_helm, :installable) } - expect { service.execute }.to change { application_class.count }.by(1) - end - - it 'make the application scheduled' do - expect(ClusterInstallAppWorker).to receive(:perform_async).with(application_class.application_name, kind_of(Numeric)).once + it 'make the application scheduled' do + expect(ClusterInstallAppWorker).to receive(:perform_async).with(application.name, kind_of(Numeric)).once - expect { service.execute }.to change { application_class.with_status(:scheduled).count }.by(1) + expect { service.execute(application) }.to change { application.class.with_status(:scheduled).count }.by(1) + end end context 'when installation is already in progress' do let(:application) { create(:clusters_applications_helm, :installing) } - let(:cluster) { application.cluster } it_behaves_like 'a failing service' end - context 'when application_class is nil' do - let(:application_class) { nil } + context 'when application is nil' do + let(:application) { nil } it_behaves_like 'a failing service' end context 'when application cannot be persisted' do + let(:application) { create(:clusters_applications_helm) } + before do - expect_any_instance_of(application_class).to receive(:make_scheduled!).once.and_raise(ActiveRecord::RecordInvalid) + expect(application).to receive(:make_scheduled!).once.and_raise(ActiveRecord::RecordInvalid) end it_behaves_like 'a failing service' diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb index 1c2f9c5cf43..1685dc748bd 100644 --- a/spec/services/clusters/create_service_spec.rb +++ b/spec/services/clusters/create_service_spec.rb @@ -8,80 +8,22 @@ describe Clusters::CreateService do subject { described_class.new(project, user, params).execute(access_token) } context 'when provider is gcp' do - shared_context 'valid params' do - let(:params) do - { - name: 'test-cluster', - provider_type: :gcp, - provider_gcp_attributes: { - gcp_project_id: 'gcp-project', - zone: 'us-central1-a', - num_nodes: 1, - machine_type: 'machine_type-a' - } - } - end - end - - shared_context 'invalid params' do - let(:params) do - { - name: 'test-cluster', - provider_type: :gcp, - provider_gcp_attributes: { - gcp_project_id: '!!!!!!!', - zone: 'us-central1-a', - num_nodes: 1, - machine_type: 'machine_type-a' - } - } - end - end - - shared_examples 'create cluster' do - it 'creates a cluster object and performs a worker' do - expect(ClusterProvisionWorker).to receive(:perform_async) - - expect { subject } - .to change { Clusters::Cluster.count }.by(1) - .and change { Clusters::Providers::Gcp.count }.by(1) - - expect(subject.name).to eq('test-cluster') - expect(subject.user).to eq(user) - expect(subject.project).to eq(project) - expect(subject.provider.gcp_project_id).to eq('gcp-project') - expect(subject.provider.zone).to eq('us-central1-a') - expect(subject.provider.num_nodes).to eq(1) - expect(subject.provider.machine_type).to eq('machine_type-a') - expect(subject.provider.access_token).to eq(access_token) - expect(subject.platform).to be_nil - end - end - - shared_examples 'error' do - it 'returns an error' do - expect(ClusterProvisionWorker).not_to receive(:perform_async) - expect { subject }.to change { Clusters::Cluster.count }.by(0) - expect(subject.errors[:"provider_gcp.gcp_project_id"]).to be_present - end - end - context 'when project has no clusters' do context 'when correct params' do - include_context 'valid params' + include_context 'valid cluster create params' - include_examples 'create cluster' + include_examples 'create cluster service success' end context 'when invalid params' do - include_context 'invalid params' + include_context 'invalid cluster create params' - include_examples 'error' + include_examples 'create cluster service error' end end context 'when project has a cluster' do - include_context 'valid params' + include_context 'valid cluster create params' let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) } it 'does not create a cluster' do diff --git a/spec/services/keys/destroy_service_spec.rb b/spec/services/keys/destroy_service_spec.rb new file mode 100644 index 00000000000..28ac72ddd42 --- /dev/null +++ b/spec/services/keys/destroy_service_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe Keys::DestroyService do + let(:user) { create(:user) } + + subject { described_class.new(user) } + + it 'destroys a key' do + key = create(:key) + + expect { subject.execute(key) }.to change(Key, :count).by(-1) + end +end diff --git a/spec/services/merge_requests/conflicts/list_service_spec.rb b/spec/services/merge_requests/conflicts/list_service_spec.rb index 837b8a56d12..d57852615d9 100644 --- a/spec/services/merge_requests/conflicts/list_service_spec.rb +++ b/spec/services/merge_requests/conflicts/list_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe MergeRequests::Conflicts::ListService do describe '#can_be_resolved_in_ui?' do def create_merge_request(source_branch) - create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start') do |mr| + create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start', merge_status: :unchecked) do |mr| mr.mark_as_unmergeable end end diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb index 240aa638f79..8838742a637 100644 --- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb @@ -112,32 +112,6 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do service.trigger(unrelated_pipeline) end end - - context 'when the merge request is not mergeable' do - let(:mr_conflict) do - create(:merge_request, merge_when_pipeline_succeeds: true, merge_user: user, - source_branch: 'master', target_branch: 'feature-conflict', - source_project: project, target_project: project) - end - - let(:conflict_pipeline) do - create(:ci_pipeline, project: project, ref: mr_conflict.source_branch, - sha: mr_conflict.diff_head_sha, status: 'success', - head_pipeline_of: mr_conflict) - end - - it 'does not merge the merge request' do - expect(MergeWorker).not_to receive(:perform_async) - - service.trigger(conflict_pipeline) - end - - it 'creates todos for unmergeability' do - expect_any_instance_of(TodoService).to receive(:merge_request_became_unmergeable).with(mr_conflict) - - service.trigger(conflict_pipeline) - end - end end describe "#cancel" do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 5f28bc123f3..831678b949d 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -1224,6 +1224,32 @@ describe NotificationService, :mailer do end end + describe '#merge_request_unmergeable' do + it "sends email to merge request author" do + notification.merge_request_unmergeable(merge_request) + + should_email(merge_request.author) + expect(email_recipients.size).to eq(1) + end + + describe 'when merge_when_pipeline_succeeds is true' do + before do + merge_request.update_attributes( + merge_when_pipeline_succeeds: true, + merge_user: create(:user) + ) + end + + it "sends email to merge request author and merge_user" do + notification.merge_request_unmergeable(merge_request) + + should_email(merge_request.author) + should_email(merge_request.merge_user) + expect(email_recipients.size).to eq(2) + end + end + end + describe '#closed_merge_request' do before do update_custom_notification(:close_merge_request, @u_guest_custom, resource: project) diff --git a/spec/services/projects/open_issues_count_service_spec.rb b/spec/services/projects/open_issues_count_service_spec.rb index f964f9972cd..06b470849b3 100644 --- a/spec/services/projects/open_issues_count_service_spec.rb +++ b/spec/services/projects/open_issues_count_service_spec.rb @@ -2,20 +2,53 @@ require 'spec_helper' describe Projects::OpenIssuesCountService do describe '#count' do - it 'returns the number of open issues' do - project = create(:project) - create(:issue, :opened, project: project) + let(:project) { create(:project) } - expect(described_class.new(project).count).to eq(1) + context 'when user is nil' do + it 'does not include confidential issues in the issue count' do + create(:issue, :opened, project: project) + create(:issue, :opened, confidential: true, project: project) + + expect(described_class.new(project).count).to eq(1) + end end - it 'does not include confidential issues in the issue count' do - project = create(:project) + context 'when user is provided' do + let(:user) { create(:user) } + + context 'when user can read confidential issues' do + before do + project.add_reporter(user) + end + + it 'returns the right count with confidential issues' do + create(:issue, :opened, project: project) + create(:issue, :opened, confidential: true, project: project) + + expect(described_class.new(project, user).count).to eq(2) + end + + it 'uses total_open_issues_count cache key' do + expect(described_class.new(project, user).cache_key_name).to eq('total_open_issues_count') + end + end + + context 'when user cannot read confidential issues' do + before do + project.add_guest(user) + end + + it 'does not include confidential issues' do + create(:issue, :opened, project: project) + create(:issue, :opened, confidential: true, project: project) - create(:issue, :opened, project: project) - create(:issue, :opened, confidential: true, project: project) + expect(described_class.new(project, user).count).to eq(1) + end - expect(described_class.new(project).count).to eq(1) + it 'uses public_open_issues_count cache key' do + expect(described_class.new(project, user).cache_key_name).to eq('public_open_issues_count') + end + end end end end diff --git a/spec/services/projects/update_remote_mirror_service_spec.rb b/spec/services/projects/update_remote_mirror_service_spec.rb index be09afd9f36..723cb374c37 100644 --- a/spec/services/projects/update_remote_mirror_service_spec.rb +++ b/spec/services/projects/update_remote_mirror_service_spec.rb @@ -343,7 +343,11 @@ describe Projects::UpdateRemoteMirrorService do tag = repository.find_tag(name) target = tag.try(:target) target_commit = tag.try(:dereferenced_target) - tags << Gitlab::Git::Tag.new(repository.raw_repository, name, target, target_commit) + tags << Gitlab::Git::Tag.new(repository.raw_repository, { + name: name, + target: target, + target_commit: target_commit + }) end end diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 562b89e6767..9a51c873b30 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -721,17 +721,18 @@ describe TodoService do end describe '#merge_request_build_failed' do - it 'creates a pending todo for the merge request author' do - service.merge_request_build_failed(mr_unassigned) + let(:merge_participants) { [mr_unassigned.author, admin] } - should_create_todo(user: author, target: mr_unassigned, action: Todo::BUILD_FAILED) + before do + allow(mr_unassigned).to receive(:merge_participants).and_return(merge_participants) end - it 'creates a pending todo for merge_user' do - mr_unassigned.update(merge_when_pipeline_succeeds: true, merge_user: admin) + it 'creates a pending todo for each merge_participant' do service.merge_request_build_failed(mr_unassigned) - should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::BUILD_FAILED) + merge_participants.each do |participant| + should_create_todo(user: participant, author: participant, target: mr_unassigned, action: Todo::BUILD_FAILED) + end end end @@ -747,11 +748,19 @@ describe TodoService do end describe '#merge_request_became_unmergeable' do - it 'creates a pending todo for a merge_user' do + let(:merge_participants) { [admin, create(:user)] } + + before do + allow(mr_unassigned).to receive(:merge_participants).and_return(merge_participants) + end + + it 'creates a pending todo for each merge_participant' do mr_unassigned.update(merge_when_pipeline_succeeds: true, merge_user: admin) service.merge_request_became_unmergeable(mr_unassigned) - should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::UNMERGEABLE) + merge_participants.each do |participant| + should_create_todo(user: participant, author: participant, target: mr_unassigned, action: Todo::UNMERGEABLE) + end end end diff --git a/spec/support/api/milestones_shared_examples.rb b/spec/support/api/milestones_shared_examples.rb index 0388f110d71..a15189db35f 100644 --- a/spec/support/api/milestones_shared_examples.rb +++ b/spec/support/api/milestones_shared_examples.rb @@ -196,6 +196,12 @@ shared_examples_for 'group and project milestones' do |route_definition| expect(json_response['state']).to eq('closed') end + + it 'updates milestone with only start date' do + put api(resource_route, user), start_date: Date.tomorrow + + expect(response).to have_gitlab_http_status(200) + end end describe "GET #{route_definition}/:milestone_id/issues" do diff --git a/spec/support/helpers/board_helpers.rb b/spec/support/helpers/board_helpers.rb index 507d0432d7f..b85fde222ea 100644 --- a/spec/support/helpers/board_helpers.rb +++ b/spec/support/helpers/board_helpers.rb @@ -1,7 +1,7 @@ module BoardHelpers def click_card(card) within card do - first('.card-number').click + first('.board-card-number').click end wait_for_sidebar diff --git a/spec/support/helpers/features/sorting_helpers.rb b/spec/support/helpers/features/sorting_helpers.rb index 50457b64745..ad0053ec5cf 100644 --- a/spec/support/helpers/features/sorting_helpers.rb +++ b/spec/support/helpers/features/sorting_helpers.rb @@ -15,7 +15,7 @@ module Spec def sort_by(value) find('button.dropdown-toggle').click - page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do + page.within('.content ul.dropdown-menu.dropdown-menu-right li') do click_link(value) end end diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb index 72e5c2d66dd..f7b71bf42e3 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -132,6 +132,14 @@ module LoginHelpers env['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym] end + def stub_omniauth_failure(strategy, message_key, exception = nil) + env = @request.env + + env['omniauth.error'] = exception + env['omniauth.error.type'] = message_key.to_sym + env['omniauth.error.strategy'] = strategy + end + def stub_omniauth_saml_config(messages) set_devise_mapping(context: Rails.application) Rails.application.routes.disable_clear_and_finalize = true diff --git a/spec/support/helpers/mobile_helpers.rb b/spec/support/helpers/mobile_helpers.rb index 3b9eb84e824..9dc1f1de436 100644 --- a/spec/support/helpers/mobile_helpers.rb +++ b/spec/support/helpers/mobile_helpers.rb @@ -1,10 +1,10 @@ module MobileHelpers def resize_screen_xs - resize_window(767, 768) + resize_window(575, 768) end def resize_screen_sm - resize_window(900, 768) + resize_window(767, 768) end def restore_window_size diff --git a/spec/support/helpers/rake_helpers.rb b/spec/support/helpers/rake_helpers.rb index 86bfeed107c..acd9cce6a67 100644 --- a/spec/support/helpers/rake_helpers.rb +++ b/spec/support/helpers/rake_helpers.rb @@ -13,6 +13,10 @@ module RakeHelpers allow(main_object).to receive(:print) end + def silence_progress_bar + allow_any_instance_of(ProgressBar::Output).to receive(:stream).and_return(double().as_null_object) + end + def main_object @main_object ||= TOPLEVEL_BINDING.eval('self') end diff --git a/spec/support/helpers/routes_helpers.rb b/spec/support/helpers/routes_helpers.rb new file mode 100644 index 00000000000..c4129606418 --- /dev/null +++ b/spec/support/helpers/routes_helpers.rb @@ -0,0 +1,7 @@ +module RoutesHelpers + def fake_routes(&block) + @routes = @routes.dup + @routes.formatter.clear + ActionDispatch::Routing::Mapper.new(@routes).instance_exec(&block) + end +end diff --git a/spec/support/helpers/sorting_helper.rb b/spec/support/helpers/sorting_helper.rb index 577518d726c..9496a94d8f4 100644 --- a/spec/support/helpers/sorting_helper.rb +++ b/spec/support/helpers/sorting_helper.rb @@ -11,7 +11,7 @@ module SortingHelper def sorting_by(value) find('button.dropdown-toggle').click - page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do + page.within('.content ul.dropdown-menu.dropdown-menu-right li') do click_link value end end diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 1dad39fdab3..57aa07cf4fa 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -159,7 +159,11 @@ module TestEnv end spawn_script = Rails.root.join('scripts/gitaly-test-spawn').to_s - @gitaly_pid = Bundler.with_original_env { IO.popen([spawn_script], &:read).to_i } + Bundler.with_original_env do + raise "gitaly spawn failed" unless system(spawn_script) + end + @gitaly_pid = Integer(File.read('tmp/tests/gitaly.pid')) + Kernel.at_exit { stop_gitaly } wait_gitaly diff --git a/spec/support/services/clusters/create_service_shared.rb b/spec/support/services/clusters/create_service_shared.rb new file mode 100644 index 00000000000..43a2fd05498 --- /dev/null +++ b/spec/support/services/clusters/create_service_shared.rb @@ -0,0 +1,57 @@ +shared_context 'valid cluster create params' do + let(:params) do + { + name: 'test-cluster', + provider_type: :gcp, + provider_gcp_attributes: { + gcp_project_id: 'gcp-project', + zone: 'us-central1-a', + num_nodes: 1, + machine_type: 'machine_type-a' + } + } + end +end + +shared_context 'invalid cluster create params' do + let(:params) do + { + name: 'test-cluster', + provider_type: :gcp, + provider_gcp_attributes: { + gcp_project_id: '!!!!!!!', + zone: 'us-central1-a', + num_nodes: 1, + machine_type: 'machine_type-a' + } + } + end +end + +shared_examples 'create cluster service success' do + it 'creates a cluster object and performs a worker' do + expect(ClusterProvisionWorker).to receive(:perform_async) + + expect { subject } + .to change { Clusters::Cluster.count }.by(1) + .and change { Clusters::Providers::Gcp.count }.by(1) + + expect(subject.name).to eq('test-cluster') + expect(subject.user).to eq(user) + expect(subject.project).to eq(project) + expect(subject.provider.gcp_project_id).to eq('gcp-project') + expect(subject.provider.zone).to eq('us-central1-a') + expect(subject.provider.num_nodes).to eq(1) + expect(subject.provider.machine_type).to eq('machine_type-a') + expect(subject.provider.access_token).to eq(access_token) + expect(subject.platform).to be_nil + end +end + +shared_examples 'create cluster service error' do + it 'returns an error' do + expect(ClusterProvisionWorker).not_to receive(:perform_async) + expect { subject }.to change { Clusters::Cluster.count }.by(0) + expect(subject.errors[:"provider_gcp.gcp_project_id"]).to be_present + end +end diff --git a/spec/support/shared_examples/features/protected_branches_access_control_ce.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce.rb index 17f319f49e9..5241c0fa6f1 100644 --- a/spec/support/shared_examples/features/protected_branches_access_control_ce.rb +++ b/spec/support/shared_examples/features/protected_branches_access_control_ce.rb @@ -10,7 +10,7 @@ shared_examples "protected branches > access control > CE" do unless allowed_to_push_button.text == access_type_name allowed_to_push_button.click - within(".dropdown.open .dropdown-menu") { click_on access_type_name } + within(".dropdown.show .dropdown-menu") { click_on access_type_name } end end @@ -55,7 +55,7 @@ shared_examples "protected branches > access control > CE" do unless allowed_to_merge_button.text == access_type_name allowed_to_merge_button.click - within(".dropdown.open .dropdown-menu") { click_on access_type_name } + within(".dropdown.show .dropdown-menu") { click_on access_type_name } end end diff --git a/spec/support/shared_examples/malicious_regexp_shared_examples.rb b/spec/support/shared_examples/malicious_regexp_shared_examples.rb index ac5d22298bb..65026f1d7c0 100644 --- a/spec/support/shared_examples/malicious_regexp_shared_examples.rb +++ b/spec/support/shared_examples/malicious_regexp_shared_examples.rb @@ -1,3 +1,5 @@ +require 'timeout' + shared_examples 'malicious regexp' do let(:malicious_text) { 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!' } let(:malicious_regexp) { '(?i)^(([a-z])+.)+[A-Z]([a-z])+$' } diff --git a/spec/support/shared_examples/models/with_uploads_shared_examples.rb b/spec/support/shared_examples/models/with_uploads_shared_examples.rb new file mode 100644 index 00000000000..47ad0c6345d --- /dev/null +++ b/spec/support/shared_examples/models/with_uploads_shared_examples.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +shared_examples_for 'model with mounted uploader' do |supports_fileuploads| + describe '.destroy' do + before do + stub_uploads_object_storage(uploader_class) + + model_object.public_send(upload_attribute).migrate!(ObjectStorage::Store::REMOTE) + end + + it 'deletes remote uploads' do + expect_any_instance_of(CarrierWave::Storage::Fog::File).to receive(:delete).and_call_original + + expect { model_object.destroy }.to change { Upload.count }.by(-1) + end + + it 'deletes any FileUploader uploads which are not mounted', skip: !supports_fileuploads do + create(:upload, uploader: FileUploader, model: model_object) + + expect { model_object.destroy }.to change { Upload.count }.by(-2) + end + end +end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index a2e5642a72c..c9252bebb2e 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -125,6 +125,16 @@ describe 'gitlab:app namespace rake task' do expect(Dir.entries(File.join(project.repository.path, 'custom_hooks'))).to include("dummy.txt") end end + + context 'specific backup tasks' do + let(:task_list) { %w(db repo uploads builds artifacts pages lfs registry) } + + it 'prints a progress message to stdout' do + task_list.each do |task| + expect { run_rake_task("gitlab:backup:#{task}:create") }.to output(/Dumping /).to_stdout + end + end + end end context 'tar creation' do diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb index 099baacf019..59c777ea338 100644 --- a/spec/views/admin/dashboard/index.html.haml_spec.rb +++ b/spec/views/admin/dashboard/index.html.haml_spec.rb @@ -22,6 +22,6 @@ describe 'admin/dashboard/index.html.haml' do it "includes revision of GitLab" do render - expect(rendered).to have_content "#{Gitlab::VERSION} (#{Gitlab::REVISION})" + expect(rendered).to have_content "#{Gitlab::VERSION} (#{Gitlab.revision})" end end diff --git a/spec/views/help/index.html.haml_spec.rb b/spec/views/help/index.html.haml_spec.rb index 0a78606171d..836d452304c 100644 --- a/spec/views/help/index.html.haml_spec.rb +++ b/spec/views/help/index.html.haml_spec.rb @@ -39,7 +39,7 @@ describe 'help/index' do def stub_version(version, revision) stub_const('Gitlab::VERSION', version) - stub_const('Gitlab::REVISION', revision) + allow(Gitlab).to receive(:revision).and_return(revision) end def stub_helpers diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb index f28bf430f02..98d4456b277 100644 --- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb @@ -36,16 +36,17 @@ describe 'layouts/nav/sidebar/_project' do expect(rendered).to have_text 'Registry' end - it 'highlights only one tab' do + it 'highlights sidebar item and flyout' do render - expect(rendered).to have_css('.active', count: 1) + expect(rendered).to have_css('.sidebar-top-level-items > li.active', count: 1) + expect(rendered).to have_css('.is-fly-out-only > li.active', count: 1) end - it 'highlights container registry tab only' do + it 'highlights container registry tab' do render - expect(rendered).to have_css('.active', text: 'Registry') + expect(rendered).to have_css('.sidebar-top-level-items > li.active', text: 'Registry') end end end diff --git a/spec/workers/update_merge_requests_worker_spec.rb b/spec/workers/update_merge_requests_worker_spec.rb index 0fa19ac84bb..80137815d2b 100644 --- a/spec/workers/update_merge_requests_worker_spec.rb +++ b/spec/workers/update_merge_requests_worker_spec.rb @@ -18,8 +18,13 @@ describe UpdateMergeRequestsWorker do end it 'executes MergeRequests::RefreshService with expected values' do - expect(MergeRequests::RefreshService).to receive(:new).with(project, user).and_call_original - expect_any_instance_of(MergeRequests::RefreshService).to receive(:execute).with(oldrev, newrev, ref) + expect(MergeRequests::RefreshService).to receive(:new) + .with(project, user).and_wrap_original do |method, *args| + method.call(*args).tap do |refresh_service| + expect(refresh_service) + .to receive(:execute).with(oldrev, newrev, ref) + end + end perform end diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml index a00c6e89a1d..589ebcf1414 100644 --- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml @@ -77,7 +77,7 @@ test: only: - branches -codequality: +code_quality: image: docker:stable variables: DOCKER_DRIVER: overlay2 @@ -86,9 +86,9 @@ codequality: - docker:stable-dind script: - setup_docker - - codeclimate + - code_quality artifacts: - paths: [codeclimate.json] + paths: [gl-code-quality-report.json] performance: stage: performance @@ -136,7 +136,7 @@ dependency_scanning: artifacts: paths: [gl-dependency-scanning-report.json] -sast:container: +container_scanning: image: docker:stable variables: DOCKER_DRIVER: overlay2 @@ -145,9 +145,9 @@ sast:container: - docker:stable-dind script: - setup_docker - - sast_container + - container_scanning artifacts: - paths: [gl-sast-container-report.json] + paths: [gl-container-scanning-report.json] dast: stage: dast @@ -388,7 +388,7 @@ rollout 100%: # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') - function sast_container() { + function container_scanning() { if [[ -n "$CI_REGISTRY_USER" ]]; then echo "Logging to GitLab Container Registry with CI credentials..." docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" @@ -406,10 +406,10 @@ rollout 100%: retries=0 echo "Waiting for clair daemon to start" while( ! wget -T 10 -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; echo -n "." ; if [ $retries -eq 10 ] ; then echo " Timeout, aborting." ; exit 1 ; fi ; retries=$(($retries+1)) ; done - ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-sast-container-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true + ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-container-scanning-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true } - function codeclimate() { + function code_quality() { docker run --env SOURCE_CODE="$PWD" \ --volume "$PWD":/code \ --volume /var/run/docker.sock:/var/run/docker.sock \ diff --git a/vendor/ingress/values.yaml b/vendor/ingress/values.yaml index a6b499953bf..d0c1673cefc 100644 --- a/vendor/ingress/values.yaml +++ b/vendor/ingress/values.yaml @@ -7,3 +7,8 @@ controller: podAnnotations: prometheus.io/scrape: "true" prometheus.io/port: "10254" + +rbac: + create: false + createRole: false + createClusterRole: false diff --git a/yarn.lock b/yarn.lock index b48a16c6ff1..af8785bbc66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -54,9 +54,9 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" -"@gitlab-org/gitlab-svgs@^1.20.0": - version "1.20.0" - resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.20.0.tgz#4c3fa3a91e0693114654b0066fb1ef04c0602047" +"@gitlab-org/gitlab-svgs@^1.23.0": + version "1.23.0" + resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.23.0.tgz#42047aeedcc06bc12d417ed1efadad1749af9670" "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" @@ -73,6 +73,20 @@ version "2.0.48" resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-2.0.48.tgz#3e90d8cde2d29015e5583017f7830cb3975b2eef" +"@vue/component-compiler-utils@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-1.2.1.tgz#3d543baa75cfe5dab96e29415b78366450156ef6" + dependencies: + consolidate "^0.15.1" + hash-sum "^1.0.2" + lru-cache "^4.1.2" + merge-source-map "^1.1.0" + postcss "^6.0.20" + postcss-selector-parser "^3.1.1" + prettier "^1.11.1" + source-map "^0.5.6" + vue-template-es2015-compiler "^1.6.0" + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -1270,9 +1284,9 @@ boom@5.x.x: dependencies: hoek "4.x.x" -bootstrap-sass@^3.3.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/bootstrap-sass/-/bootstrap-sass-3.3.6.tgz#363b0d300e868d3e70134c1a742bb17288444fd1" +bootstrap@4.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.0.tgz#110b05c31a236d56dbc9adcda6dd16f53738a28a" boxen@^1.2.1: version "1.3.0" @@ -1467,6 +1481,15 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" +cache-loader@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cache-loader/-/cache-loader-1.2.2.tgz#6d5c38ded959a09cc5d58190ab5af6f73bd353f5" + dependencies: + loader-utils "^1.1.0" + mkdirp "^0.5.1" + neo-async "^2.5.0" + schema-utils "^0.4.2" + cacheable-request@^2.1.1: version "2.1.4" resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d" @@ -1947,9 +1970,9 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" -consolidate@^0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.14.5.tgz#5a25047bc76f73072667c8cb52c989888f494c63" +consolidate@^0.15.1: + version "0.15.1" + resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.15.1.tgz#21ab043235c71a07d45d9aad98593b0dba56bab7" dependencies: bluebird "^3.1.1" @@ -2021,18 +2044,6 @@ core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" -cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892" - dependencies: - is-directory "^0.3.1" - js-yaml "^3.4.3" - minimist "^1.2.0" - object-assign "^4.1.0" - os-homedir "^1.0.1" - parse-json "^2.2.0" - require-from-string "^1.1.0" - create-ecdh@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" @@ -2660,7 +2671,7 @@ domutils@^1.5.1: dom-serializer "0" domelementtype "1" -dot-prop@^4.1.0: +dot-prop@^4.1.0, dot-prop@^4.1.1: version "4.2.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" dependencies: @@ -4552,10 +4563,6 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: is-data-descriptor "^1.0.0" kind-of "^6.0.2" -is-directory@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" - is-dotfile@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" @@ -4989,13 +4996,6 @@ js-yaml@3.x, js-yaml@^3.5.1, js-yaml@^3.7.0: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^3.4.3: - version "3.11.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef" - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - js-yaml@~3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80" @@ -5555,6 +5555,13 @@ lru-cache@^4.0.1, lru-cache@^4.1.1: pseudomap "^1.0.2" yallist "^2.1.2" +lru-cache@^4.1.2: + version "4.1.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + lru-cache@~2.6.5: version "2.6.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.6.5.tgz#e56d6354148ede8d7707b58d143220fd08df0fd5" @@ -5697,6 +5704,12 @@ merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" +merge-source-map@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" + dependencies: + source-map "^0.6.1" + merge2@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.2.tgz#03212e3da8d86c4d8523cebd6318193414f94e34" @@ -6333,7 +6346,7 @@ os-browserify@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f" -os-homedir@^1.0.0, os-homedir@^1.0.1: +os-homedir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" @@ -6651,6 +6664,10 @@ pluralize@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" +popper.js@^1.14.3: + version "1.14.3" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095" + portfinder@^1.0.9: version "1.0.13" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9" @@ -6724,29 +6741,6 @@ postcss-filter-plugins@^2.0.0: postcss "^5.0.4" uniqid "^4.0.0" -postcss-load-config@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a" - dependencies: - cosmiconfig "^2.1.0" - object-assign "^4.1.0" - postcss-load-options "^1.2.0" - postcss-load-plugins "^2.3.0" - -postcss-load-options@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postcss-load-options/-/postcss-load-options-1.2.0.tgz#b098b1559ddac2df04bc0bb375f99a5cfe2b6d8c" - dependencies: - cosmiconfig "^2.1.0" - object-assign "^4.1.0" - -postcss-load-plugins@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz#745768116599aca2f009fad426b00175049d8d92" - dependencies: - cosmiconfig "^2.1.1" - object-assign "^4.1.0" - postcss-merge-idents@^2.1.5: version "2.1.7" resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270" @@ -6886,6 +6880,14 @@ postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2: indexes-of "^1.0.1" uniq "^1.0.1" +postcss-selector-parser@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865" + dependencies: + dot-prop "^4.1.1" + indexes-of "^1.0.1" + uniq "^1.0.1" + postcss-svgo@^2.1.1: version "2.1.6" resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d" @@ -6932,13 +6934,13 @@ postcss@^6.0.1, postcss@^6.0.14: source-map "^0.6.1" supports-color "^5.2.0" -postcss@^6.0.8: - version "6.0.21" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.21.tgz#8265662694eddf9e9a5960db6da33c39e4cd069d" +postcss@^6.0.20: + version "6.0.22" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.22.tgz#e23b78314905c3b90cbd61702121e7a78848f2a3" dependencies: - chalk "^2.3.2" + chalk "^2.4.1" source-map "^0.6.1" - supports-color "^5.3.0" + supports-color "^5.4.0" prelude-ls@~1.1.2: version "1.1.2" @@ -6960,14 +6962,14 @@ prettier@1.11.1: version "1.11.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.11.1.tgz#61e43fc4cd44e68f2b0dfc2c38cd4bb0fccdcc75" +prettier@^1.11.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.12.1.tgz#c1ad20e803e7749faf905a409d2367e06bbe7325" + prettier@^1.5.3: version "1.10.2" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.10.2.tgz#1af8356d1842276a99a5b5529c82dd9e9ad3cc93" -prettier@^1.7.0: - version "1.8.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.8.2.tgz#bff83e7fd573933c607875e5ba3abbdffb96aeb8" - pretty-bytes@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9" @@ -7533,10 +7535,6 @@ require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" -require-from-string@^1.1.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" - require-main-filename@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" @@ -7587,12 +7585,6 @@ resolve@^1.1.6, resolve@^1.2.0: dependencies: path-parse "^1.0.5" -resolve@^1.4.0: - version "1.7.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3" - dependencies: - path-parse "^1.0.5" - responselike@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" @@ -7710,7 +7702,7 @@ sax@~1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828" -schema-utils@^0.4.0, schema-utils@^0.4.3, schema-utils@^0.4.4, schema-utils@^0.4.5: +schema-utils@^0.4.0, schema-utils@^0.4.2, schema-utils@^0.4.3, schema-utils@^0.4.4, schema-utils@^0.4.5: version "0.4.5" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.5.tgz#21836f0608aac17b78f9e3e24daff14a5ca13a3e" dependencies: @@ -8200,6 +8192,10 @@ statuses@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" +stickyfilljs@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/stickyfilljs/-/stickyfilljs-2.0.5.tgz#d229e372d2199ddf5d283bbe34ac1f7d2529c2fc" + stream-browserify@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" @@ -8362,7 +8358,7 @@ supports-color@^5.2.0: dependencies: has-flag "^3.0.0" -supports-color@^5.3.0: +supports-color@^5.3.0, supports-color@^5.4.0: version "5.4.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" dependencies: @@ -8974,27 +8970,19 @@ vue-eslint-parser@^2.0.1: esquery "^1.0.0" lodash "^4.17.4" -vue-hot-reload-api@^2.2.0: +vue-hot-reload-api@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.0.tgz#97976142405d13d8efae154749e88c4e358cf926" -vue-loader@^14.1.1: - version "14.2.2" - resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-14.2.2.tgz#c8cf3c2e29b6fb2ee595248a2aa6005038a125b3" +vue-loader@^15.2.0: + version "15.2.0" + resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.2.0.tgz#5a8138e490a1040942d2f10ae68fa72b5a923364" dependencies: - consolidate "^0.14.0" + "@vue/component-compiler-utils" "^1.2.1" hash-sum "^1.0.2" loader-utils "^1.1.0" - lru-cache "^4.1.1" - postcss "^6.0.8" - postcss-load-config "^1.1.0" - postcss-selector-parser "^2.0.0" - prettier "^1.7.0" - resolve "^1.4.0" - source-map "^0.6.1" - vue-hot-reload-api "^2.2.0" - vue-style-loader "^4.0.1" - vue-template-es2015-compiler "^1.6.0" + vue-hot-reload-api "^2.3.0" + vue-style-loader "^4.1.0" vue-resource@^1.5.0: version "1.5.0" @@ -9006,7 +8994,7 @@ vue-router@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.1.tgz#d9b05ad9c7420ba0f626d6500d693e60092cc1e9" -vue-style-loader@^4.0.1: +vue-style-loader@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.0.tgz#7588bd778e2c9f8d87bfc3c5a4a039638da7a863" dependencies: |