From 07749a6761605c71bffbd2025b8bb4348cbb89c2 Mon Sep 17 00:00:00 2001 From: tiagonbotelho <tiagonbotelho@hotmail.com> Date: Tue, 15 Mar 2016 12:40:13 +0000 Subject: [PATCH 001/223] moves the stage and ref from the table in a ci commit build to a plain one line text --- app/views/projects/commit/_builds.html.haml | 15 +++++++++------ .../commit_statuses/_commit_status.html.haml | 10 ---------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml index befad27666c3..95c6353d0c58 100644 --- a/app/views/projects/commit/_builds.html.haml +++ b/app/views/projects/commit/_builds.html.haml @@ -12,9 +12,16 @@ - if defined?(link_to_commit) && link_to_commit for commit = link_to @ci_commit.short_sha, namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), class: "monospace" - - if @ci_commit.duration > 0 in - = time_interval_in_words @ci_commit.duration + - if @ci_commit.refs + = link_to @ci_commit.refs.first, namespace_project_commits_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.refs.first) + - else + .light none + - if defined?(@ci_commit.stage) + at stage + = @ci_commit.stage + - if @ci_commit.duration > 0 + = "(#{time_interval_in_words @ci_commit.duration})" - if @ci_commit.yaml_errors.present? .bs-callout.bs-callout-danger @@ -34,8 +41,6 @@ %tr %th Status %th Build ID - %th Ref - %th Stage %th Name %th Duration %th Finished at @@ -56,8 +61,6 @@ %tr %th Status %th Build ID - %th Ref - %th Stage %th Name %th Duration %th Finished at diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index a3449d1ae057..2e7645eccf06 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -21,12 +21,6 @@ %td = link_to commit_status.short_sha, namespace_project_commit_path(commit_status.project.namespace, commit_status.project, commit_status.sha), class: "monospace" - %td - - if commit_status.ref - = link_to commit_status.ref, namespace_project_commits_path(commit_status.project.namespace, commit_status.project, commit_status.ref) - - else - .light none - - if defined?(runner) && runner %td - if commit_status.try(:runner) @@ -34,10 +28,6 @@ - else .light none - - if defined?(stage) && stage - %td - = commit_status.stage - %td = commit_status.name -- GitLab From 21eeaa496a4167934109b2dae9d3f35439348f7b Mon Sep 17 00:00:00 2001 From: tiagonbotelho <tiagonbotelho@hotmail.com> Date: Thu, 17 Mar 2016 16:59:43 +0000 Subject: [PATCH 002/223] changes stage back to original position --- app/views/projects/commit/_builds.html.haml | 2 ++ app/views/projects/commit_statuses/_commit_status.html.haml | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml index 95c6353d0c58..f3580771e259 100644 --- a/app/views/projects/commit/_builds.html.haml +++ b/app/views/projects/commit/_builds.html.haml @@ -41,6 +41,7 @@ %tr %th Status %th Build ID + %th Stage %th Name %th Duration %th Finished at @@ -61,6 +62,7 @@ %tr %th Status %th Build ID + %th Stage %th Name %th Duration %th Finished at diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index 2e7645eccf06..107da156b396 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -28,6 +28,10 @@ - else .light none + - if defined?(stage) && runner + %td + = commit_status.stage + %td = commit_status.name -- GitLab From 6b5a127e879ab995e1a0c4ea344e89dedb773253 Mon Sep 17 00:00:00 2001 From: Christian Mehlmauer <firefart@gmail.com> Date: Sun, 14 Feb 2016 10:31:28 +0100 Subject: [PATCH 003/223] add bundle clean to upgrade docs --- doc/update/8.4-to-8.5.md | 3 +++ doc/update/patch_versions.md | 1 + 2 files changed, 4 insertions(+) diff --git a/doc/update/8.4-to-8.5.md b/doc/update/8.4-to-8.5.md index 408a17ac348a..57131ee9e719 100644 --- a/doc/update/8.4-to-8.5.md +++ b/doc/update/8.4-to-8.5.md @@ -64,6 +64,9 @@ sudo -u git -H bundle install --without postgres development test --deployment # PostgreSQL installations (note: the line below states '--without mysql') sudo -u git -H bundle install --without mysql development test --deployment +# Clean up old gems +sudo -u git -H bundle clean + # Run database migrations sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index a10e62877ba8..ce7ae0d285c0 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -62,6 +62,7 @@ sudo -u git -H bundle install --without development test mysql --deployment # MySQL sudo -u git -H bundle install --without development test postgres --deployment +sudo -u git -H bundle clean sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production ``` -- GitLab From b5d517cce7c53b8797c6abb2ff974132a271721f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 29 Feb 2016 10:20:45 +0100 Subject: [PATCH 004/223] Deprecated GitLab CI API clean up - README --- doc/ci/api/README.md | 87 ++++---------------------------------------- 1 file changed, 8 insertions(+), 79 deletions(-) diff --git a/doc/ci/api/README.md b/doc/ci/api/README.md index cf9710ede577..cb860bc43f0b 100644 --- a/doc/ci/api/README.md +++ b/doc/ci/api/README.md @@ -1,86 +1,15 @@ # GitLab CI API +## Purpose + +Main purpose of GitLab CI API is to provide necessary data and context for +GitLab CI Runners. + +For consumer API take a look at this [documentation](../../api/README.md) where +you will find all relevant information. + ## Resources -- [Projects](projects.md) - [Runners](runners.md) - [Commits](commits.md) - [Builds](builds.md) - - -## Authentication - -GitLab CI API uses different types of authentication depends on what API you use. -Each API document has section with information about authentication you need to use. - -GitLab CI API has 4 authentication methods: - -* GitLab user token & GitLab url -* GitLab CI project token -* GitLab CI runners registration token -* GitLab CI runner token - - -### Authentication #1: GitLab user token & GitLab url - -Authentication is done by -sending the `private-token` of a valid user and the `url` of an -authorized GitLab instance via a query string along with the API -request: - - GET http://gitlab.example.com/ci/api/v1/projects?private_token=QVy1PB7sTxfy4pqfZM1U&url=http://demo.gitlab.com/ - -If preferred, you may instead send the `private-token` as a header in -your request: - - curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" "http://gitlab.example.com/ci/api/v1/projects?url=http://demo.gitlab.com/" - - -### Authentication #2: GitLab CI project token - -Each project in GitLab CI has it own token. -It can be used to get project commits and builds information. -You can use project token only for certain project. - -### Authentication #3: GitLab CI runners registration token - -This token is not persisted and is generated on each application start. -It can be used only for registering new runners in system. You can find it on -GitLab CI Runners web page https://gitlab-ci.example.com/admin/runners - -### Authentication #4: GitLab CI runner token - -Every GitLab CI runner has it own token that allow it to receive and update -GitLab CI builds. This token exists of internal purposes and should be used only -by runners - -## JSON - -All API requests are serialized using JSON. You don't need to specify -`.json` at the end of API URL. - -## Status codes - -The API is designed to return different status codes according to context and action. In this way if a request results in an error the caller is able to get insight into what went wrong, e.g. status code `400 Bad Request` is returned if a required attribute is missing from the request. The following list gives an overview of how the API functions generally behave. - -API request types: - -- `GET` requests access one or more resources and return the result as JSON -- `POST` requests return `201 Created` if the resource is successfully created and return the newly created resource as JSON -- `GET`, `PUT` and `DELETE` return `200 OK` if the resource is accessed, modified or deleted successfully, the (modified) result is returned as JSON -- `DELETE` requests are designed to be idempotent, meaning a request a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind it is the user is not really interested if the resource existed before or not. - -The following list shows the possible return codes for API requests. - -Return values: - -- `200 OK` - The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON -- `201 Created` - The `POST` request was successful and the resource is returned as JSON -- `400 Bad Request` - A required attribute of the API request is missing, e.g. the title of an issue is not given -- `401 Unauthorized` - The user is not authenticated, a valid user token is necessary, see above -- `403 Forbidden` - The request is not allowed, e.g. the user is not allowed to delete a project -- `404 Not Found` - A resource could not be accessed, e.g. an ID for a resource could not be found -- `405 Method Not Allowed` - The request is not supported -- `409 Conflict` - A conflicting resource already exists, e.g. creating a project with a name that already exists -- `422 Unprocessable` - The entity could not be processed -- `500 Server Error` - While handling the request something went wrong on the server side -- GitLab From f072d05c68e7e4b57331ac32eee2ed314f9e1c07 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 29 Feb 2016 12:00:42 +0100 Subject: [PATCH 005/223] Update CI API docs for builds endpoint for runners [ci skip] --- doc/ci/api/README.md | 2 +- doc/ci/api/builds.md | 104 +++++++++++++++++-------------------------- 2 files changed, 42 insertions(+), 64 deletions(-) diff --git a/doc/ci/api/README.md b/doc/ci/api/README.md index cb860bc43f0b..d573bea0f80b 100644 --- a/doc/ci/api/README.md +++ b/doc/ci/api/README.md @@ -10,6 +10,6 @@ you will find all relevant information. ## Resources +- [Builds](builds.md) - [Runners](runners.md) - [Commits](commits.md) -- [Builds](builds.md) diff --git a/doc/ci/api/builds.md b/doc/ci/api/builds.md index 018ca22dbbd7..b82f2d19b19a 100644 --- a/doc/ci/api/builds.md +++ b/doc/ci/api/builds.md @@ -1,85 +1,63 @@ # Builds API -This API used by runners to receive and update builds. +API used by runners to receive and update builds. -__Authentication is done by runner token__ +_**Note:** This API is intended to be used only by Runners as their own +communication channel. For the consumer API see the +[Builds API](../../api/builds.md)._ + +## Authentication + +Unique runner token is required to authenticate. You can provide build token +using a `token` parameter, or by sending `BUILD-TOKEN` header that contains it. + +`token` parameter and `BUILD-TOKEN` header can be interchangeable. ## Builds ### Runs oldest pending build by runner - POST /ci/builds/register + POST /builds/register Parameters: * `token` (required) - The unique token of runner -Returns: - -```json -{ - "id": 48584, - "ref": "0.1.1", - "tag": true, - "sha": "d63117656af6ff57d99e50cc270f854691f335ad", - "status": "success", - "name": "pages", - "token": "9dd60b4f1a439d1765357446c1084c", - "stage": "test", - "project_id": 479, - "project_name": "test", - "commands": "echo commands", - "repo_url": "http://gitlab-ci-token:token@gitlab.example/group/test.git", - "before_sha": "0000000000000000000000000000000000000000", - "allow_git_fetch": false, - "options": { - "image": "docker:image", - "artifacts": { - "paths": [ - "public" - ] - }, - "cache": { - "paths": [ - "vendor" - ] - } - }, - "timeout": 3600, - "variables": [ - { - "key": "CI_BUILD_TAG", - "value": "0.1.1", - "public": true - } - ], - "depends_on_builds": [ - { - "id": 48584, - "ref": "0.1.1", - "tag": true, - "sha": "d63117656af6ff57d99e50cc270f854691f335ad", - "status": "success", - "name": "build", - "token": "9dd60b4f1a439d1765357446c1084c", - "stage": "build", - "project_id": 479, - "project_name": "test", - "artifacts_file": { - "filename": "artifacts.zip", - "size": 0 - } - } - ] -} -``` ### Update details of an existing build - PUT /ci/builds/:id + PUT /builds/:id Parameters: * `id` (required) - The ID of a project * `state` (optional) - The state of a build * `trace` (optional) - The trace of a build + +### Upload artifacts to build + + POST /builds/:id/artifacts + +Parameters: + + * `id` (required) - The ID of a build + * `token` (required) - The build authorization token + * `file` (required) - Artifacts file + +### Download the artifacts file from build + + GET /builds/:id/artifacts + +Parameters: + + * `id` (required) - The ID of a build + * `token` (required) - The build authorization token + +### Remove the artifacts file from build + + DELETE /builds/:id/artifacts + +Parameters: + + * ` id` (required) - The ID of a build + * `token` (required) - The build authorization token -- GitLab From ff310a5599fbe7b1564f8a552ed83d4ccd2b4a83 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 29 Feb 2016 12:12:51 +0100 Subject: [PATCH 006/223] Add CI API prefix to documentation [ci skip] --- doc/ci/api/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/ci/api/README.md b/doc/ci/api/README.md index d573bea0f80b..94ef4d2c38d5 100644 --- a/doc/ci/api/README.md +++ b/doc/ci/api/README.md @@ -8,6 +8,14 @@ GitLab CI Runners. For consumer API take a look at this [documentation](../../api/README.md) where you will find all relevant information. +## API Prefix + +Current CI API prefix is `/ci/api/v1`. + +You need to prepend this prefix to all examples in this documentation, like: + + GET /ci/api/v1/builds/:id/artifacts + ## Resources - [Builds](builds.md) -- GitLab From 5be4d41cf36378f27f6404ed78800b0df0c3d739 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 29 Feb 2016 12:14:46 +0100 Subject: [PATCH 007/223] Update CI API documentation for runners [ci skip] --- doc/ci/api/runners.md | 64 ++++++++----------------------------------- 1 file changed, 11 insertions(+), 53 deletions(-) diff --git a/doc/ci/api/runners.md b/doc/ci/api/runners.md index e9033aeacd55..c4716982357d 100644 --- a/doc/ci/api/runners.md +++ b/doc/ci/api/runners.md @@ -1,81 +1,39 @@ # Runners API +API used by runners to register and delete itselves. + _**Note:** This API is intended to be used only by Runners as their own communication channel. For the consumer API see the [new Runners API](../../api/runners.md)._ ## Runners -### Retrieve all runners - -__Authentication is done by GitLab user token & GitLab url__ - -Used to get information about all runners registered on the GitLab CI -instance. - - GET /ci/runners - -Returns: - -```json -[ - { - "id" : 85, - "token" : "12b68e90394084703135" - }, - { - "id" : 86, - "token" : "76bf894e969364709864" - }, -] -``` - ### Register a new runner - -__Authentication is done with a Shared runner registration token or a project Specific runner registration token__ +__Authentication is done with a shared runner registration token or a project +specific runner registration token.__ Used to make GitLab CI aware of available runners. - POST /ci/runners/register + POST /runners/register Parameters: - * `token` (required) - The registration token. It is 2 types of token you can pass here. + * `token` (required) - The registration token. + +It is 2 types of token you can pass here. 1. Shared runner registration token 2. Project specific registration token -Returns: - -```json -{ - "id" : 85, - "token" : "12b68e90394084703135" -} -``` - ### Delete a runner +__Authentication is done by using runner token.__ -__Authentication is done by runner token__ - -Used to removing runners. +Used to remove runner. - DELETE /ci/runners/delete + DELETE /runners/delete Parameters: * `token` (required) - The runner token. - -Returns: - -```json -{ - "id" : 1, - "token" : "d14963981a428f70121777e50643d1", - "created_at" : "2015-02-26T11:39:39.232Z", - "updated_at" : "2015-02-26T11:39:39.232Z", - "description" : "awesome runner" -} -``` -- GitLab From 5e92469ea64f1d5b67af9d12a800db913c0f45b8 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 29 Feb 2016 12:16:32 +0100 Subject: [PATCH 008/223] Remove deprecated CI API docs for commits and projects [ci skip] --- doc/api/commits.md | 3 +- doc/ci/api/README.md | 1 - doc/ci/api/commits.md | 108 ----------------------------- doc/ci/api/projects.md | 149 ----------------------------------------- 4 files changed, 1 insertion(+), 260 deletions(-) delete mode 100644 doc/ci/api/commits.md delete mode 100644 doc/ci/api/projects.md diff --git a/doc/api/commits.md b/doc/api/commits.md index e4d436b8e521..6341440c58b9 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -213,8 +213,7 @@ Example response: ## Commit status -Since GitLab 8.1, this is the new commit status API. The documentation in -[ci/api/commits](../ci/api/commits.md) is deprecated. +Since GitLab 8.1, this is the new commit status API. ### Get the status of a commit diff --git a/doc/ci/api/README.md b/doc/ci/api/README.md index 94ef4d2c38d5..aea808007fcb 100644 --- a/doc/ci/api/README.md +++ b/doc/ci/api/README.md @@ -20,4 +20,3 @@ You need to prepend this prefix to all examples in this documentation, like: - [Builds](builds.md) - [Runners](runners.md) -- [Commits](commits.md) diff --git a/doc/ci/api/commits.md b/doc/ci/api/commits.md deleted file mode 100644 index 871de7abcce1..000000000000 --- a/doc/ci/api/commits.md +++ /dev/null @@ -1,108 +0,0 @@ -# Commits API - -**DEPRECATED** - -Since GitLab 8.1, there is a new commit status API. Please see the [revised -documentation](../../api/commits.md#commit-status). - ---- - -__Authentication is done by GitLab CI project token__ - -## Commits - -### Retrieve all commits per project - -Get list of commits per project - - GET /ci/commits - -Parameters: - - * `project_id` (required) - The ID of a project - * `project_token` (requires) - Project token - * `page` (optional) - * `per_page` (optional) - items per request (default is 20) - -Returns: - -```json -[{ - "id": 3, - "ref": "master", - "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf", - "project_id": 2, - "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898", - "created_at": "2014-11-05T09:46:35.247Z", - "status": "success", - "finished_at": "2014-11-05T09:46:44.254Z", - "duration": 5.062692165374756, - "git_commit_message": "wow\n", - "git_author_name": "Administrator", - "git_author_email": "admin@example.com", - "builds": [{ - "id": 7, - "project_id": 2, - "ref": "master", - "status": "success", - "finished_at": "2014-11-05T09:46:44.254Z", - "created_at": "2014-11-05T09:46:35.259Z", - "updated_at": "2014-11-05T09:46:44.255Z", - "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf", - "started_at": "2014-11-05T09:46:39.192Z", - "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898", - "runner_id": 1, - "coverage": null, - "commit_id": 3 - }] -}] -``` - -### Create commit - -Inform GitLab CI about new commit you want it to build. - -__If commit already exists in GitLab CI it will not be created__ - - - POST /ci/commits - -Parameters: - - * `project_id` (required) - The ID of a project - * `project_token` (requires) - Project token - * `data` (required) - Push data. For example see comment in `lib/api/commits.rb` - -Returns: - -```json -{ - "id": 3, - "ref": "master", - "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf", - "project_id": 2, - "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898", - "created_at": "2014-11-05T09:46:35.247Z", - "status": "success", - "finished_at": "2014-11-05T09:46:44.254Z", - "duration": 5.062692165374756, - "git_commit_message": "wow\n", - "git_author_name": "Administrator", - "git_author_email": "admin@example.com", - "builds": [{ - "id": 7, - "project_id": 2, - "ref": "master", - "status": "success", - "finished_at": "2014-11-05T09:46:44.254Z", - "created_at": "2014-11-05T09:46:35.259Z", - "updated_at": "2014-11-05T09:46:44.255Z", - "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf", - "started_at": "2014-11-05T09:46:39.192Z", - "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898", - "runner_id": 1, - "coverage": null, - "commit_id": 3 - }] -} -``` diff --git a/doc/ci/api/projects.md b/doc/ci/api/projects.md deleted file mode 100644 index fe6b1c01352b..000000000000 --- a/doc/ci/api/projects.md +++ /dev/null @@ -1,149 +0,0 @@ -# Projects API - -This API is intended to aid in the setup and configuration of -projects on GitLab CI. - -__Authentication is done by GitLab user token & GitLab url__ - -## Projects - -### List Authorized Projects - -Lists all projects that the authenticated user has access to. - -``` -GET /ci/projects -``` - -Returns: - -```json -[ - { - "id" : 271, - "name" : "gitlabhq", - "timeout" : 1800, - "token" : "iPWx6WM4lhHNedGfBpPJNP", - "default_ref" : "master", - "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell", - "path" : "gitlab/gitlab-shell", - "always_build" : false, - "polling_interval" : null, - "public" : false, - "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git", - "gitlab_id" : 3 - }, - { - "id" : 272, - "name" : "gitlab-ci", - "timeout" : 1800, - "token" : "iPWx6WM4lhHNedGfBpPJNP", - "default_ref" : "master", - "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell", - "path" : "gitlab/gitlab-shell", - "always_build" : false, - "polling_interval" : null, - "public" : false, - "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git", - "gitlab_id" : 4 - } -] -``` - -### List Owned Projects - -Lists all projects that the authenticated user owns. - -``` -GET /ci/projects/owned -``` - -Returns: - -```json -[ - { - "id" : 272, - "name" : "gitlab-ci", - "timeout" : 1800, - "token" : "iPWx6WM4lhHNedGfBpPJNP", - "default_ref" : "master", - "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell", - "path" : "gitlab/gitlab-shell", - "always_build" : false, - "polling_interval" : null, - "public" : false, - "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git", - "gitlab_id" : 4 - } -] -``` - -### Single Project - -Returns information about a single project for which the user is -authorized. - - GET /ci/projects/:id - -Parameters: - - * `id` (required) - The ID of the GitLab CI project - -### Create Project - -Creates a GitLab CI project using GitLab project details. - - POST /ci/projects - -Parameters: - - * `name` (required) - The name of the project - * `gitlab_id` (required) - The ID of the project on the GitLab instance - * `default_ref` (optional) - The branch to run on (default to `master`) - -### Update Project - -Updates a GitLab CI project using GitLab project details that the -authenticated user has access to. - - PUT /ci/projects/:id - -Parameters: - - * `name` - The name of the project - * `default_ref` - The branch to run on (default to `master`) - -### Remove Project - -Removes a GitLab CI project that the authenticated user has access to. - - DELETE /ci/projects/:id - -Parameters: - - * `id` (required) - The ID of the GitLab CI project - -### Link Project to Runner - -Links a runner to a project so that it can make builds (only via -authorized user). - - POST /ci/projects/:id/runners/:runner_id - -Parameters: - - * `id` (required) - The ID of the GitLab CI project - * `runner_id` (required) - The ID of the GitLab CI runner - -### Remove Project from Runner - -Removes a runner from a project so that it can not make builds (only -via authorized user). - - DELETE /ci/projects/:id/runners/:runner_id - -Parameters: - - * `id` (required) - The ID of the GitLab CI project - * `runner_id` (required) - The ID of the GitLab CI runner \ No newline at end of file -- GitLab From cc71469071616ac60562dacd0bfca255c6f1cbb2 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Tue, 1 Mar 2016 10:57:12 +0100 Subject: [PATCH 009/223] Update format of documentation for CI API --- doc/ci/api/builds.md | 28 +++++++++++++++++++--------- doc/ci/api/runners.md | 35 +++++++++++++++++++++-------------- 2 files changed, 40 insertions(+), 23 deletions(-) diff --git a/doc/ci/api/builds.md b/doc/ci/api/builds.md index b82f2d19b19a..d100e2611789 100644 --- a/doc/ci/api/builds.md +++ b/doc/ci/api/builds.md @@ -8,35 +8,45 @@ communication channel. For the consumer API see the ## Authentication -Unique runner token is required to authenticate. You can provide build token -using a `token` parameter, or by sending `BUILD-TOKEN` header that contains it. +This API uses two types of authentication: -`token` parameter and `BUILD-TOKEN` header can be interchangeable. +1. Unique runner's token + + Token assigned to runner after it has been registered. + +2. Using build authorization token + + This is project's CI token that can be found in Continuous Integration + project settings. + + Build authorization token can be passed as a parameter or a value of + `BUILD-TOKEN` header. This method are interchangeable. ## Builds ### Runs oldest pending build by runner - POST /builds/register + POST /ci/api/v1/builds/register Parameters: - * `token` (required) - The unique token of runner + * `token` (required) - Unique runner token ### Update details of an existing build - PUT /builds/:id + PUT /ci/api/v1/builds/:id Parameters: * `id` (required) - The ID of a project + * `token` (required) - Unique runner token * `state` (optional) - The state of a build * `trace` (optional) - The trace of a build ### Upload artifacts to build - POST /builds/:id/artifacts + POST /ci/api/v1/builds/:id/artifacts Parameters: @@ -46,7 +56,7 @@ Parameters: ### Download the artifacts file from build - GET /builds/:id/artifacts + GET /ci/api/v1/builds/:id/artifacts Parameters: @@ -55,7 +65,7 @@ Parameters: ### Remove the artifacts file from build - DELETE /builds/:id/artifacts + DELETE /ci/api/v1/builds/:id/artifacts Parameters: diff --git a/doc/ci/api/runners.md b/doc/ci/api/runners.md index c4716982357d..2f01da4bd76c 100644 --- a/doc/ci/api/runners.md +++ b/doc/ci/api/runners.md @@ -1,39 +1,46 @@ # Runners API -API used by runners to register and delete itselves. +API used by runners to register and delete themselves. _**Note:** This API is intended to be used only by Runners as their own communication channel. For the consumer API see the [new Runners API](../../api/runners.md)._ +## Authentication + +This API uses two types of authentication: + +1. Unique runner's token + + Token assigned to runner after it has been registered. + +2. Using runners' registration token + + This is a token that can be found in project's settings. + It can be also found in Admin area » Runners settings. + + There are two types of tokens you can pass - shared runner registration + token or project specific registration token. + ## Runners ### Register a new runner -__Authentication is done with a shared runner registration token or a project -specific runner registration token.__ - Used to make GitLab CI aware of available runners. - POST /runners/register + POST /ci/api/v1/runners/register Parameters: - * `token` (required) - The registration token. + * `token` (required) - Registration token -It is 2 types of token you can pass here. - -1. Shared runner registration token -2. Project specific registration token ### Delete a runner -__Authentication is done by using runner token.__ - Used to remove runner. - DELETE /runners/delete + DELETE /ci/api/v1/runners/delete Parameters: - * `token` (required) - The runner token. + * `token` (required) - Unique runner token -- GitLab From 06dc50a3901875c852c6917f328d61d818bc1bcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= <glen@delfi.ee> Date: Tue, 1 Mar 2016 19:17:22 +0000 Subject: [PATCH 010/223] update slack instructions --- doc/integration/slack.md | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/doc/integration/slack.md b/doc/integration/slack.md index ecbe0d3e8873..f6ba80f46d5d 100644 --- a/doc/integration/slack.md +++ b/doc/integration/slack.md @@ -2,19 +2,11 @@ ## On Slack -To enable Slack integration you must create an Incoming WebHooks integration on Slack; +To enable Slack integration you must create an Incoming WebHooks integration on Slack: 1. [Sign in to Slack](https://slack.com/signin) -1. Select **Apps & Custom Integrations** from the dropdown next to your team name. - -1. Click the **Configure** link (right-upper corner). - -1. Select the **Custom integrations** tab. - -1. Click the **Incoming WebHooks** row. - -1. Click the **Add configuration** button. +1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/) 1. Choose the channel name you want to send notifications to. -- GitLab From 59874017047a0d62c6bb6543aeecb249a042a36c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Wed, 2 Mar 2016 10:33:38 +0000 Subject: [PATCH 011/223] Remove duplicate 'cache_store' configuration in test environment --- config/environments/test.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/config/environments/test.rb b/config/environments/test.rb index d6842affa6c1..f96ac6f97530 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -7,8 +7,6 @@ # and recreated between test runs. Don't rely on the data there! config.cache_classes = false - config.cache_store = :null_store - # Configure static asset server for tests with Cache-Control for performance config.serve_static_files = true config.static_cache_control = "public, max-age=3600" -- GitLab From 278e85bfa2a24626a48d252382aafd9869c4fd78 Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Wed, 2 Mar 2016 12:18:43 +0100 Subject: [PATCH 012/223] fix token issue - timing attack --- app/models/project.rb | 4 ++-- app/models/project_services/ci_service.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 3235a1cee505..c0f2ab91fa41 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -908,13 +908,13 @@ def any_runners?(&block) end def valid_runners_token? token - self.runners_token && self.runners_token == token + self.runners_token && ActiveSupport::SecurityUtils.secure_compare(token, self.runners_token) end # TODO (ayufan): For now we use runners_token (backward compatibility) # In 8.4 every build will have its own individual token valid for time of build def valid_build_token? token - self.builds_enabled? && self.runners_token && self.runners_token == token + self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.secure_compare(token, self.runners_token) end def build_coverage_enabled? diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index e10b5529b426..f328deda3542 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -26,7 +26,7 @@ class CiService < Service default_value_for :category, 'ci' def valid_token?(token) - self.respond_to?(:token) && self.token.present? && self.token == token + self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.secure_compare(token, self.token) end def supported_events -- GitLab From 87c5b12f491cb40a8020be72371bd3efd45bedaf Mon Sep 17 00:00:00 2001 From: nico de ceulaer <nico.deceulaer@tpvision.com> Date: Thu, 3 Mar 2016 14:50:21 +0100 Subject: [PATCH 013/223] ensure also alternative email addresses are used when looking up user --- app/helpers/application_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f0aa2b571217..40a8b512e4ce 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -72,7 +72,7 @@ def avatar_icon(user_or_email = nil, size = nil, scale = 2) if user_or_email.is_a?(User) user = user_or_email else - user = User.find_by(email: user_or_email.downcase) + user = User.find_by_any_email(user_or_email.downcase) end if user -- GitLab From 4dae3eca4b7d1cb2a1eaf5e118bf8632d21fafbb Mon Sep 17 00:00:00 2001 From: Valery Sizov <vsv2711@gmail.com> Date: Thu, 3 Mar 2016 15:19:27 +0200 Subject: [PATCH 014/223] Security: Fix issue auto closing --- app/services/git_push_service.rb | 4 +++- app/services/merge_requests/post_merge_service.rb | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 736b82e3571b..93a16e88967b 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -110,7 +110,9 @@ def process_commit_messages # a different branch. closed_issues = commit.closes_issues(current_user) closed_issues.each do |issue| - Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit) + if can?(current_user, :update_issue, issue) + Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit) + end end end diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb index 8f25c5e24967..ebb67c7db654 100644 --- a/app/services/merge_requests/post_merge_service.rb +++ b/app/services/merge_requests/post_merge_service.rb @@ -21,7 +21,9 @@ def close_issues(merge_request) closed_issues = merge_request.closes_issues(current_user) closed_issues.each do |issue| - Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request) + if can?(current_user, :update_issue, issue) + Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request) + end end end -- GitLab From 5d866785479f97a4cfada2cf39439822179235bb Mon Sep 17 00:00:00 2001 From: Patricio Cano <suprnova32@gmail.com> Date: Thu, 3 Mar 2016 11:21:32 -0500 Subject: [PATCH 015/223] Added Troubleshooting information for most used services. --- doc/integration/README.md | 31 +++++++ doc/integration/ldap.md | 27 ++++++ doc/integration/saml.md | 40 ++++++++- doc/project_services/jira.md | 13 +++ doc/workflow/lfs/lfs_administration.md | 8 +- .../lfs/manage_large_binaries_with_git_lfs.md | 85 +++++++++++++------ 6 files changed, 173 insertions(+), 31 deletions(-) diff --git a/doc/integration/README.md b/doc/integration/README.md index 281eea8363d4..7c8f785a61f1 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -39,3 +39,34 @@ please see the [project_services directory][projects-code]. [jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html [Project Service]: ../project_services/project_services.md [projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services + +## SSL certificate errors + +When trying to integrate GitLab with services that are using self-signed certificates, +it is very likely that SSL certificate errors will occur on different parts of the +application, most likely Sidekiq. There are 2 approaches you can take to solve this: + +1. Add the root certificate to the trusted chain of the OS. +1. If using Omnibus, you can add the certificate to GitLab's trusted certificates. + +**OS main trusted chain** + +This [resource](http://kb.kerio.com/product/kerio-connect/server-configuration/ssl-certificates/adding-trusted-root-certificates-to-the-server-1605.html) +has all the information you need to add a certificate to the main trusted chain. + +This [answer](http://superuser.com/questions/437330/how-do-you-add-a-certificate-authority-ca-to-ubuntu) +at SuperUser also has relevant information. + +**Omnibus Trusted Chain** + +It is enough to concatenate the certificate to the main trusted certificate: + +```bash +cat jira.pem >> /opt/gitlab/embedded/ssl/certs/cacert.pem +``` + +After that restart GitLab with: + +```bash +sudo gitlab-ctl restart +``` diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index f256477196b5..ac15a2cdffcf 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -204,3 +204,30 @@ When setting `method: ssl`, the underlying authentication method used by `omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with the LDAP server before any LDAP-protocol data is exchanged but no validation of the LDAP server's SSL certificate is performed. + +## Troubleshooting + +### Common problems + +**Invalid credentials when logging in** + +Make sure the user you are binding with has enough permissions to read the user's +tree and traverse it. + +Also make sure that the `user_filter` is not blocking otherwise valid users. + +To make sure that the LDAP settings are correct and GitLab can see your users, +execute the following command: + +For Omnibus installations: + +```bash +sudo gitlab-rake gitlab:ldap:check +``` + +For installations from source: + +```bash +sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production +``` + diff --git a/doc/integration/saml.md b/doc/integration/saml.md index c84113556cd7..16e47bb99b03 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -133,12 +133,18 @@ will be returned to GitLab and will be signed in. ## Troubleshooting +### Common problems + +**500 error after login** + If you see a "500 error" in GitLab when you are redirected back from the SAML sign in page, this likely indicates that GitLab could not get the email address for the SAML user. Make sure the IdP provides a claim containing the user's email address, using claim name `email` or `mail`. +**Redirect back to login screen with no evident error** + If after signing in into your SAML server you are redirected back to the sign in page and no error is displayed, check your `production.log` file. It will most likely contain the message `Can't verify CSRF token authenticity`. This means that there is an error during @@ -147,4 +153,36 @@ the SAML request, but this error never reaches GitLab due to the CSRF check. To bypass this you can add `skip_before_action :verify_authenticity_token` to the `omniauth_callbacks_controller.rb` file. This will allow the error to hit GitLab, where it can then be seen in the usual logs, or as a flash message in the login -screen. \ No newline at end of file +screen. + +**Invalid audience** + +This error means that the IdP doesn't recognize GitLab as a valid sender and +receiver of SAML requests. Make sure to add the GitLab callback URL to the approved +audiences of the IdP server. + +**Missing claims** + +The IdP server needs to pass certain information in order for GitLab to either +create an account, or match the login information to an existing account. `email` +is the minimum amount of information that needs to be passed. If the IdP server +is not providing this information, all SAML requests will fail. + +Make sure this information is provided. + +**Key validation error, Digest mismatch or Fingerprint mismatch** + +These errors all come from a similar place, the SAML certificate. SAML requests +need to be validated using a fingerprint, a certificate or a validator. + +For this you need take the following into account: + +- If no certificate is provided in the settings, a fingerprint or fingerprint + validator needs to be provided and the response from the server must contain + a certificate (`<ds:KeyInfo><ds:X509Data><ds:X509Certificate>`) +- If a certificate is provided in the settings, it is no longer necessary for + the request to contain one. In this case the fingerprint or fingerprint + validators are optional + +Make sure that one of the above described scenarios is valid, or the requests will +fail with one of the mentioned errors. \ No newline at end of file diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index 7c12557a3217..623ec857cbec 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -219,3 +219,16 @@ You can see from the above image that there are four references to GitLab: [JIRA Core]: https://www.atlassian.com/software/jira/core "The JIRA Core website" [jira-ce]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2146 "MR - Backport JIRA service" [8_3_post]: https://about.gitlab.com/2015/12/22/gitlab-8-3-released/ "GitLab 8.3 release post" + +## Troubleshooting + +**GitLab is unable to comment on a ticket** + +Make sure that the user you set up for GitLab to communicate with JIRA has the +correct access permission to post comments on a ticket and to also transition the +ticket, if you'd like GitLab to also take care of closing them. + +**GitLab is unable to close a ticket** + +Make sure the the `Transition ID` you set within the JIRA settings matches the +one your project needs to close a ticket. diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md index 5076b2697a35..36cb9da23804 100644 --- a/doc/workflow/lfs/lfs_administration.md +++ b/doc/workflow/lfs/lfs_administration.md @@ -9,7 +9,8 @@ Documentation on how to use Git LFS are under [Managing large binary files with ## Configuration -Git LFS objects can be large in size. By default, they are stored on the server GitLab is installed on. +Git LFS objects can be large in size. By default, they are stored on the server +GitLab is installed on. There are two configuration options to help GitLab server administrators: @@ -37,5 +38,8 @@ In `config/gitlab.yml`: ## Known limitations -* Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets) is not supported +* Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets) + is not supported * Currently, removing LFS objects from GitLab Git LFS storage is not supported +* LFS authentications via SSH is not supported for the time being +* Only compatible with the GitLFS client versions 1.1.0 or 1.0.2. diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md index b59e92cb3179..ba91685a20b8 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -1,17 +1,21 @@ # Git LFS -Managing large files such as audio, video and graphics files has always been one of the shortcomings of Git. -The general recommendation is to not have Git repositories larger than 1GB to preserve performance. +Managing large files such as audio, video and graphics files has always been one +of the shortcomings of Git. The general recommendation is to not have Git repositories +larger than 1GB to preserve performance. -GitLab already supports [managing large files with git annex](http://doc.gitlab.com/ee/workflow/git_annex.html) (EE only), however in certain -environments it is not always convenient to use different commands to differentiate between the large files and regular ones. +GitLab already supports [managing large files with git annex](http://doc.gitlab.com/ee/workflow/git_annex.html) +(EE only), however in certain environments it is not always convenient to use +different commands to differentiate between the large files and regular ones. -Git LFS makes this simpler for the end user by removing the requirement to learn new commands. +Git LFS makes this simpler for the end user by removing the requirement to +learn new commands. ## How it works -Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication to authorize client requests. -Once the request is authorized, Git LFS client receives instructions from where to fetch or where to push the large file. +Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication +to authorize client requests. Once the request is authorized, Git LFS client receives +instructions from where to fetch or where to push the large file. ## GitLab server configuration @@ -24,15 +28,19 @@ Documentation for GitLab instance administrators is under [LFS administration do ## Known limitations -* Git LFS v1 original API is not supported since it was deprecated early in LFS development +* Git LFS v1 original API is not supported since it was deprecated early in LFS + development * When SSH is set as a remote, Git LFS objects still go through HTTPS -* Any Git LFS request will ask for HTTPS credentials to be provided so good Git credentials store is recommended -* Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have to add the URL to Git config manually (see #troubleshooting) +* Any Git LFS request will ask for HTTPS credentials to be provided so good Git + credentials store is recommended +* Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have + to add the URL to Git config manually (see #troubleshooting) ## Using Git LFS -Lets take a look at the workflow when you need to check large files into your Git repository with Git LFS: -For example, if you want to upload a very large file and check it into your Git repository: +Lets take a look at the workflow when you need to check large files into your Git +repository with Git LFS. For example, if you want to upload a very large file and +check it into your Git repository: ```bash git clone git@gitlab.example.com:group/project.git @@ -40,7 +48,8 @@ git lfs init # initialize the Git LFS project project git lfs track "*.iso" # select the file extensions that you want to treat as large files ``` -Once a certain file extension is marked for tracking as a LFS object you can use Git as usual without having to redo the command to track a file with the same extension: +Once a certain file extension is marked for tracking as a LFS object you can use +Git as usual without having to redo the command to track a file with the same extension: ```bash cp ~/tmp/debian.iso ./ # copy a large file into the current directory @@ -49,13 +58,17 @@ git commit -am "Added Debian iso" # commit the file meta data git push origin master # sync the git repo and large file to the GitLab server ``` -Cloning the repository works the same as before. Git automatically detects the LFS-tracked files and clones them via HTTP. If you performed the git clone command with a SSH URL, you have to enter your GitLab credentials for HTTP authentication. +Cloning the repository works the same as before. Git automatically detects the +LFS-tracked files and clones them via HTTP. If you performed the git clone +command with a SSH URL, you have to enter your GitLab credentials for HTTP +authentication. ```bash git clone git@gitlab.example.com:group/project.git ``` -If you already cloned the repository and you want to get the latest LFS object that are on the remote repository, eg. from branch `master`: +If you already cloned the repository and you want to get the latest LFS object +that are on the remote repository, eg. from branch `master`: ```bash git lfs fetch master @@ -73,8 +86,8 @@ Check if you have permissions to push to the project or fetch from the project. * Project is not allowed to access the LFS object -LFS object you are trying to push to the project or fetch from the project is not available to the project anymore. -Probably the object was removed from the server. +LFS object you are trying to push to the project or fetch from the project is not +available to the project anymore. Probably the object was removed from the server. * Local git repository is using deprecated LFS API @@ -89,16 +102,26 @@ git lfs logs last If the status `error 501` is shown, it is because: -* Git LFS support is not enabled on the GitLab server. Check with your GitLab administrator why Git LFS is not enabled on the server. See [LFS administration documentation](lfs_administration.md) for instructions on how to enable LFS support. +* Git LFS support is not enabled on the GitLab server. Check with your GitLab + administrator why Git LFS is not enabled on the server. See + [LFS administration documentation](lfs_administration.md) for instructions + on how to enable LFS support. -* Git LFS client version is not supported by GitLab server. Check your Git LFS version with `git lfs version`. Check the Git config of the project for traces of deprecated API with `git lfs -l`. If `batch = false` is set in the config, remove the line and try to update your Git LFS client. Only version 1.0.1 and newer are supported. +* Git LFS client version is not supported by GitLab server. Check your Git LFS + version with `git lfs version`. Check the Git config of the project for traces + of deprecated API with `git lfs -l`. If `batch = false` is set in the config, + remove the line and try to update your Git LFS client. Only version 1.0.1 and + newer are supported. ### getsockopt: connection refused -If you push a LFS object to a project and you receive an error similar to: `Post <URL>/info/lfs/objects/batch: dial tcp IP: getsockopt: connection refused`, -the LFS client is trying to reach GitLab through HTTPS. However, your GitLab instance is being served on HTTP. +If you push a LFS object to a project and you receive an error similar to: +`Post <URL>/info/lfs/objects/batch: dial tcp IP: getsockopt: connection refused`, +the LFS client is trying to reach GitLab through HTTPS. However, your GitLab +instance is being served on HTTP. -This behaviour is caused by Git LFS using HTTPS connections by default when a `lfsurl` is not set in the Git config. +This behaviour is caused by Git LFS using HTTPS connections by default when a +`lfsurl` is not set in the Git config. To prevent this from happening, set the lfs url in project Git config: @@ -109,18 +132,24 @@ git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs/o ### Credentials are always required when pushing an object -Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing the LFS object on every push for every object, user HTTPS credentials are required. +Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing +the LFS object on every push for every object, user HTTPS credentials are required. -By default, Git has support for remembering the credentials for each repository you use. This is described in [Git credentials man pages](https://git-scm.com/docs/gitcredentials). +By default, Git has support for remembering the credentials for each repository +you use. This is described in [Git credentials man pages](https://git-scm.com/docs/gitcredentials). -For example, you can tell Git to remember the password for a period of time in which you expect to push the objects: +For example, you can tell Git to remember the password for a period of time in +which you expect to push the objects: ```bash git config --global credential.helper 'cache --timeout=3600' ``` -This will remember the credentials for an hour after which Git operations will require re-authentication. +This will remember the credentials for an hour after which Git operations will +require re-authentication. -If you are using OS X you can use `osxkeychain` to store and encrypt your credentials. For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases). +If you are using OS X you can use `osxkeychain` to store and encrypt your credentials. +For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases). -More details about various methods of storing the user credentials can be found on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). \ No newline at end of file +More details about various methods of storing the user credentials can be found +on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). \ No newline at end of file -- GitLab From ff093ad25d6a0f2f04b94e22f0f9113cfaafb9b5 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Thu, 3 Mar 2016 17:59:47 +0100 Subject: [PATCH 016/223] Tell clients/proxies to cache raw blob requests --- .../projects/avatars_controller.rb | 2 ++ app/controllers/projects/raw_controller.rb | 2 ++ app/helpers/blob_helper.rb | 25 +++++++++++++++++++ app/models/project.rb | 7 +++--- app/views/projects/blob/_image.html.haml | 2 +- 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb index b64dbbd89ce9..6de7888888fc 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -7,6 +7,8 @@ def show @blob = @repository.blob_at_branch('master', @project.avatar_in_git) if @blob headers['X-Content-Type-Options'] = 'nosniff' + set_cache_headers + check_etag! headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob)) headers['Content-Disposition'] = 'inline' headers['Content-Type'] = safe_content_type(@blob) diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index d9723acb1d96..b6ff08262d74 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -12,6 +12,8 @@ def show if @blob headers['X-Content-Type-Options'] = 'nosniff' + check_etag! + set_cache_headers if @blob.lfs_pointer? send_lfs_object diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 7f63a2e2cb4f..adb56e49c62e 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -152,4 +152,29 @@ def safe_content_type(blob) 'application/octet-stream' end end + + def set_cache_headers + if @project.visibility_level == Project::PUBLIC + cache_control = 'public, ' + else + cache_control = 'private, ' + end + + if @ref && @commit && @ref == @commit.id + # This is a link to a commit by its commit SHA. That means that the blob + # is immutable. + cache_control << 'max-age=600' # 10 minutes + else + # A branch or tag points at this blob. That means that the expected blob + # value may change over time. + cache_control << 'max-age=60' # 1 minute + end + + headers['Cache-Control'] = cache_control + headers['ETag'] = @blob.id + end + + def check_etag! + stale?(etag: @blob.id) + end end diff --git a/app/models/project.rb b/app/models/project.rb index c0f2ab91fa41..97894f8c1481 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -56,6 +56,7 @@ class Project < ActiveRecord::Base extend Gitlab::ConfigHelper UNKNOWN_IMPORT_URL = 'http://unknown.git' + AVATAR_BRANCH = 'master' default_value_for :archived, false default_value_for :visibility_level, gitlab_config_features.visibility_level @@ -545,9 +546,9 @@ def avatar_type end def avatar_in_git - @avatar_file ||= 'logo.png' if repository.blob_at_branch('master', 'logo.png') - @avatar_file ||= 'logo.jpg' if repository.blob_at_branch('master', 'logo.jpg') - @avatar_file ||= 'logo.gif' if repository.blob_at_branch('master', 'logo.gif') + @avatar_file ||= 'logo.png' if repository.blob_at_branch(AVATAR_BRANCH, 'logo.png') + @avatar_file ||= 'logo.jpg' if repository.blob_at_branch(AVATAR_BRANCH, 'logo.jpg') + @avatar_file ||= 'logo.gif' if repository.blob_at_branch(AVATAR_BRANCH, 'logo.gif') @avatar_file end diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml index 3c11b97921f5..18caddabd392 100644 --- a/app/views/projects/blob/_image.html.haml +++ b/app/views/projects/blob/_image.html.haml @@ -6,4 +6,4 @@ - blob = sanitize_svg(blob) %img{src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"} - else - %img{src: namespace_project_raw_path(@project.namespace, @project, @id)} + %img{src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path))} -- GitLab From eb031ed5abe8fad3243c2ad212b3793c62d95299 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller <bbodenmiller@hotmail.com> Date: Thu, 3 Mar 2016 19:16:38 +0000 Subject: [PATCH 017/223] add link to web hooks [skip ci] --- doc/hooks/custom_hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md index 0f2665a3bf71..5a4b2f43ba77 100644 --- a/doc/hooks/custom_hooks.md +++ b/doc/hooks/custom_hooks.md @@ -2,7 +2,7 @@ **Note: Custom git hooks must be configured on the filesystem of the GitLab server. Only GitLab server administrators will be able to complete these tasks. -Please explore webhooks as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).** +Please explore [web hooks](doc/web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).** Git natively supports hooks that are executed on different actions. Examples of server-side git hooks include pre-receive, post-receive, and update. -- GitLab From 87742abf363e3c0c60e24d1533bfaa0b0bda2b17 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 4 Mar 2016 08:42:22 +0000 Subject: [PATCH 018/223] Added discard button to comment form Also changed the labels on the buttons to better match the action they are completing. Closes #8057 --- app/assets/javascripts/notes.js.coffee | 61 +++++++++++++++---- .../projects/issues/_discussion.html.haml | 4 +- .../merge_requests/_discussion.html.haml | 4 +- app/views/projects/notes/_form.html.haml | 5 +- 4 files changed, 57 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index c95ead22e6ce..3142afccd6e1 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -51,6 +51,9 @@ class @Notes $(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton $(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm + # reset main target form when clicking discard + $(document).on "click", ".js-note-discard", @resetMainTargetForm + # update the file name when an attachment is selected $(document).on "change", ".js-note-attachment-input", @updateFormAttachment @@ -85,6 +88,7 @@ class @Notes $(document).off "keyup", ".js-note-text" $(document).off "click", ".js-note-target-reopen" $(document).off "click", ".js-note-target-close" + $(document).off "click", ".js-note-discard" $('.note .js-task-list-container').taskList('disable') $(document).off 'tasklist:changed', '.note .js-task-list-container' @@ -219,7 +223,7 @@ class @Notes Resets text and preview. Resets buttons. ### - resetMainTargetForm: -> + resetMainTargetForm: (e) => form = $(".js-main-target-form") # remove validation errors @@ -231,6 +235,8 @@ class @Notes form.find(".js-note-text").data("autosave").reset() + @updateTargetButtons(e) + reenableTargetFormSubmitButton: -> form = $(".js-main-target-form") @@ -274,8 +280,10 @@ class @Notes form.removeClass "js-new-note-form" form.find('.div-dropzone').remove() + # hide discard button + form.find('.js-note-discard').hide() + # setup preview buttons - form.find(".js-md-write-button, .js-md-preview-button").tooltip placement: "left" previewButton = form.find(".js-md-preview-button") textarea = form.find(".js-note-text") @@ -561,21 +569,52 @@ class @Notes updateCloseButton: (e) => textarea = $(e.target) form = textarea.parents('form') - form.find('.js-note-target-close').text('Close') + closebtn = form.find('.js-note-target-close') + closebtn.text(closebtn.data('original-text')) updateTargetButtons: (e) => textarea = $(e.target) form = textarea.parents('form') + reopenbtn = form.find('.js-note-target-reopen') + closebtn = form.find('.js-note-target-close') + discardbtn = form.find('.js-note-discard') + if textarea.val().trim().length > 0 - form.find('.js-note-target-reopen').text('Comment & reopen') - form.find('.js-note-target-close').text('Comment & close') - form.find('.js-note-target-reopen').addClass('btn-comment-and-reopen') - form.find('.js-note-target-close').addClass('btn-comment-and-close') + reopentext = reopenbtn.data('alternative-text') + closetext = closebtn.data('alternative-text') + + if reopenbtn.text() isnt reopentext + reopenbtn.text(reopentext) + + if closebtn.text() isnt closetext + closebtn.text(closetext) + + if reopenbtn.is(':not(.btn-comment-and-reopen)') + reopenbtn.addClass('btn-comment-and-reopen') + + if closebtn.is(':not(.btn-comment-and-close)') + closebtn.addClass('btn-comment-and-close') + + if discardbtn.is(':hidden') + discardbtn.show() else - form.find('.js-note-target-reopen').text('Reopen') - form.find('.js-note-target-close').text('Close') - form.find('.js-note-target-reopen').removeClass('btn-comment-and-reopen') - form.find('.js-note-target-close').removeClass('btn-comment-and-close') + reopentext = reopenbtn.data('original-text') + closetext = closebtn.data('original-text') + + if reopenbtn.text() isnt reopentext + reopenbtn.text(reopentext) + + if closebtn.text() isnt closetext + closebtn.text(closebtn.data('original-text')) + + if reopenbtn.is(':not(.btn-comment-and-reopen)') + reopenbtn.removeClass('btn-comment-and-reopen') + + if closebtn.is(':not(.btn-comment-and-close)') + closebtn.removeClass('btn-comment-and-close') + + if discardbtn.is(':visible') + discardbtn.hide() initTaskList: -> @enableTaskList() diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index eb9c225df2fb..b151393abab1 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -1,7 +1,7 @@ - content_for :note_actions do - if can?(current_user, :update_issue, @issue) - = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' - = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' + = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-grouped btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' + = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-grouped btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' #notes = render 'projects/notes/notes_with_form' diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index 1c7de94acfdf..393998f15b9b 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -1,8 +1,8 @@ - content_for :note_actions do - if can?(current_user, :update_merge_request, @merge_request) - if @merge_request.open? - = link_to 'Close', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request" + = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request", data: {original_text: "Close merge request", alternative_text: "Comment & close merge request"} - if @merge_request.closed? - = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request" + = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request", data: {original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"} #notes= render "projects/notes/notes_with_form" diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 09740d8ea128..2b293e4770d5 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -13,6 +13,7 @@ .error-alert .note-form-actions.clearfix - = f.submit 'Add Comment', class: "btn btn-nr btn-create comment-btn btn-grouped js-comment-button" + = f.submit 'Comment', class: "btn btn-nr btn-create comment-btn btn-grouped js-comment-button" = yield(:note_actions) - %a.btn.btn-nr.btn-cancel.js-close-discussion-note-form Cancel + %a.btn.btn-cancel.js-note-discard{role: "button"} + Discard draft -- GitLab From 8137b4922b46d666684bd368fc9a4329bfef9b30 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 4 Mar 2016 10:44:04 +0000 Subject: [PATCH 019/223] Fixed tests for comment forms --- app/assets/javascripts/notes.js.coffee | 5 +++++ app/views/projects/notes/_form.html.haml | 2 +- features/steps/project/snippets.rb | 2 +- spec/features/notes_on_merge_requests_spec.rb | 6 +++--- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 3142afccd6e1..eff8beb97e96 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -470,6 +470,11 @@ class @Notes form.find("#note_line_code").val dataHolder.data("lineCode") form.find("#note_noteable_type").val dataHolder.data("noteableType") form.find("#note_noteable_id").val dataHolder.data("noteableId") + form.find('.js-note-discard') + .show() + .removeClass('js-note-discard') + .addClass('js-close-discussion-note-form') + .text(form.find('.js-close-discussion-note-form').data('cancel-text')) @setupNoteForm form form.find(".js-note-text").focus() form.addClass "js-discussion-note-form" diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 2b293e4770d5..f675f092da18 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -15,5 +15,5 @@ .note-form-actions.clearfix = f.submit 'Comment', class: "btn btn-nr btn-create comment-btn btn-grouped js-comment-button" = yield(:note_actions) - %a.btn.btn-cancel.js-note-discard{role: "button"} + %a.btn.btn-cancel.js-note-discard{role: "button", data: {cancel_text: "Cancel"}} Discard draft diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb index 504654f90ddd..786a0cad9757 100644 --- a/features/steps/project/snippets.rb +++ b/features/steps/project/snippets.rb @@ -77,7 +77,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps step 'I leave a comment like "Good snippet!"' do page.within('.js-main-target-form') do fill_in "note_note", with: "Good snippet!" - click_button "Add Comment" + click_button "Comment" end end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 1a360cd1ebc8..d9a8058efd94 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -22,7 +22,7 @@ it 'should be valid' do is_expected.to have_css('.js-main-target-form', visible: true, count: 1) expect(find('.js-main-target-form input[type=submit]').value). - to eq('Add Comment') + to eq('Comment') page.within('.js-main-target-form') do expect(page).not_to have_link('Cancel') end @@ -49,7 +49,7 @@ page.within('.js-main-target-form') do fill_in 'note[note]', with: 'This is awsome!' find('.js-md-preview-button').click - click_button 'Add Comment' + click_button 'Comment' end end @@ -202,7 +202,7 @@ before do page.within("tr[id='#{line_code_2}'] + .js-temp-notes-holder") do fill_in 'note[note]', with: 'Another comment on line 10' - click_button('Add Comment') + click_button('Comment') end end -- GitLab From a2777f538304be0ed4b0b526ef06d3eb3d1ccac8 Mon Sep 17 00:00:00 2001 From: evuez <helloevuez@gmail.com> Date: Wed, 24 Feb 2016 13:14:37 +0100 Subject: [PATCH 020/223] Add fields to GET /users/* API endpoints for admins Added fields are last_sign_in_at and confirmed_at. They are available for GET /users/ and GET /users/:id for admins. Closes #840 --- CHANGELOG | 1 + doc/api/users.md | 2 ++ lib/api/entities.rb | 2 ++ spec/requests/api/users_spec.rb | 2 ++ 4 files changed, 7 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 5b3b18ef2dc9..1c840f102e1c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,7 @@ v 8.6.0 (unreleased) - Don't load all of GitLab in mail_room - Indicate how much an MR diverged from the target branch (Pierre de La Morinerie) - Strip leading and trailing spaces in URL validator (evuez) + - Add "last_sign_in_at" and "confirmed_at" to GET /users/* API endpoints for admins (evuez) - Return empty array instead of 404 when commit has no statuses in commit status API - Add support for cross-project label references - Update documentation to reflect Guest role not being enforced on internal projects diff --git a/doc/api/users.md b/doc/api/users.md index b7fc903825ea..82c57a2fd437 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -151,6 +151,8 @@ Parameters: "name": "John Smith", "state": "active", "created_at": "2012-05-23T08:00:58Z", + "confirmed_at": "2012-05-23T08:00:58Z", + "last_sign_in_at": "2015-03-23T08:00:58Z", "bio": null, "skype": "", "linkedin": "", diff --git a/lib/api/entities.rb b/lib/api/entities.rb index b021db8fa5b9..0779fb881a03 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -23,6 +23,8 @@ class Identity < Grape::Entity end class UserFull < User + expose :last_sign_in_at + expose :confirmed_at expose :email expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at expose :identities, using: Entities::Identity diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index b82c5c7685f4..96e8c8c51f86 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -47,6 +47,8 @@ expect(json_response.first.keys).to include 'identities' expect(json_response.first.keys).to include 'can_create_project' expect(json_response.first.keys).to include 'two_factor_enabled' + expect(json_response.first.keys).to include 'last_sign_in_at' + expect(json_response.first.keys).to include 'confirmed_at' end end end -- GitLab From 82e52bed319994f18d882f09808f1a207222b758 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 4 Mar 2016 12:22:27 +0000 Subject: [PATCH 021/223] Fixed failing comment test --- features/steps/project/merge_requests.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 8bf423cc64b2..22eea8f2cdbc 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -419,7 +419,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps page.within(".js-discussion-note-form") do fill_in "note_note", with: "Line is correct" - click_button "Add Comment" + click_button "Comment" end page.within ".files [id^=diff]:nth-child(2) .note-body > .note-text" do -- GitLab From 6143e62c4e3f2961d6cfc2d4be6e2a97e24dfff6 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 4 Mar 2016 13:52:29 +0000 Subject: [PATCH 022/223] Updated tests --- features/steps/project/issues/award_emoji.rb | 2 +- features/steps/project/issues/issues.rb | 2 +- features/steps/project/merge_requests.rb | 4 ++-- features/steps/shared/diff_note.rb | 4 ++-- features/steps/shared/issuable.rb | 2 +- features/steps/shared/note.rb | 6 +++--- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb index 277c63914d12..937fbbd34ebe 100644 --- a/features/steps/project/issues/award_emoji.rb +++ b/features/steps/project/issues/award_emoji.rb @@ -79,7 +79,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps step 'I leave comment with a single emoji' do page.within('.js-main-target-form') do fill_in 'note[note]', with: ':smile:' - click_button 'Add Comment' + click_button 'Comment' end end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index 565bf088b416..3a189a9a1e9c 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -267,7 +267,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps step 'I leave a comment with code block' do page.within(".js-main-target-form") do fill_in "note[note]", with: "```\nCommand [1]: /usr/local/bin/git , see [text](doc/text)\n```" - click_button "Add Comment" + click_button "Comment" sleep 0.05 end end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 22eea8f2cdbc..870656adef25 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -432,7 +432,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps page.within(".js-discussion-note-form") do fill_in "note_note", with: "Line is wrong on here" - click_button "Add Comment" + click_button "Comment" end end @@ -536,7 +536,7 @@ def init_diff_note def leave_comment(message) page.within(".js-discussion-note-form", visible: true) do fill_in "note_note", with: message - click_button "Add Comment" + click_button "Comment" end page.within(".notes_holder", visible: true) do expect(page).to have_content message diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 06e69441894a..906b66a4a632 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -93,14 +93,14 @@ module SharedDiffNote page.within("form[id$='#{sample_commit.line_code}']") do fill_in 'note[note]', with: ':smile:' - click_button('Add Comment') + click_button('Comment') end end end step 'I submit the diff comment' do page.within(diff_file_selector) do - click_button("Add Comment") + click_button("Comment") end end diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb index ae10c6069a93..e59bfbea9982 100644 --- a/features/steps/shared/issuable.rb +++ b/features/steps/shared/issuable.rb @@ -182,7 +182,7 @@ def leave_reference_comment(issuable:, from_project_name:) page.within('.js-main-target-form') do fill_in 'note[note]', with: "##{issuable.to_reference(project)}" - click_button 'Add Comment' + click_button 'Comment' end end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index eb6df61b8e6b..6870d364ac07 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -17,7 +17,7 @@ module SharedNote step 'I leave a comment like "XML attached"' do page.within(".js-main-target-form") do fill_in "note[note]", with: "XML attached" - click_button "Add Comment" + click_button "Comment" end end @@ -30,7 +30,7 @@ module SharedNote step 'I submit the comment' do page.within(".js-main-target-form") do - click_button "Add Comment" + click_button "Comment" end end @@ -115,7 +115,7 @@ module SharedNote step 'I leave a comment with a header containing "Comment with a header"' do page.within(".js-main-target-form") do fill_in "note[note]", with: "# Comment with a header" - click_button "Add Comment" + click_button "Comment" sleep 0.05 end end -- GitLab From 38b4ff94102149f1a289510297b42253030b4923 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Fri, 4 Mar 2016 15:17:12 +0100 Subject: [PATCH 023/223] Show at most 100 commits in the web UI When rendering 1 commit takes over 5ms (on my laptop: around 9ms), it saves seconds in rendering time to show 100 instead of 500 commits. --- app/models/merge_request_diff.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index df08d3a6dfb3..33884118595c 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -17,7 +17,7 @@ class MergeRequestDiff < ActiveRecord::Base include Sortable # Prevent store of diff if commits amount more then 500 - COMMITS_SAFE_SIZE = 500 + COMMITS_SAFE_SIZE = 100 belongs_to :merge_request -- GitLab From 71bfd80195164351ae080a30a69b08284f86a663 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Fri, 4 Mar 2016 15:20:30 +0100 Subject: [PATCH 024/223] Limit the number of commits shown in MRs This prevents timeouts when creating a MR with 1000s of commits. --- app/views/projects/commits/_commits.html.haml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 6c6312280023..e708b2df5403 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -1,7 +1,13 @@ - unless defined?(project) - project = @project +- if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE + - commits = @commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE) + - overflow = true +- else + - commits = @commits + - overflow = false -- @commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits| +- commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits| .row.commits-row .col-md-2.hidden-xs.hidden-sm %h5.commits-row-date @@ -13,3 +19,7 @@ %ul.bordered-list = render commits, project: project %hr.lists-separator + +- if overflow + .alert.alert-warning + Not shown: #{@commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE} more commits -- GitLab From f44ff78d665d7278aede82a529df5b01b7675001 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Fri, 4 Mar 2016 15:45:58 +0100 Subject: [PATCH 025/223] Put 'hidden commits' logic in CommitsHelper --- app/helpers/commits_helper.rb | 12 ++++++++++++ app/views/projects/commits/_commit_list.html.haml | 11 +++++++---- app/views/projects/commits/_commits.html.haml | 12 ++++-------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index a09e91578b69..4353ceaa757e 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -211,4 +211,16 @@ def truncate_sha(sha) def clean(string) Sanitize.clean(string, remove_contents: true) end + + def limited_commits(commits) + if commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE + # Not 100% sure we need to decorate but it is idempotent and not so slow + [ + commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE), + commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE + ] + else + [commits, 0] + end + end end diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml index ce60fbdf0321..5fe4d918833e 100644 --- a/app/views/projects/commits/_commit_list.html.haml +++ b/app/views/projects/commits/_commit_list.html.haml @@ -1,11 +1,14 @@ +- commits, hidden = limited_commits(@commits, @project) +- commits = Commit.decorate(commits, @project) + %div.panel.panel-default .panel-heading Commits (#{@commits.count}) - - if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE + - if hidden > 0 %ul.well-list - - Commit.decorate(@commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE), @project).each do |commit| + - commits.each do |commit| = render "projects/commits/inline_commit", commit: commit, project: @project %li.warning-row.unstyled - other #{@commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE} commits hidden to prevent performance issues. + other #{hidden} commits hidden to prevent performance issues. - else - %ul.well-list= render Commit.decorate(@commits, @project), project: @project + %ul.well-list= render commits, project: @project diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index e708b2df5403..998251094a46 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -1,11 +1,7 @@ - unless defined?(project) - project = @project -- if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE - - commits = @commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE) - - overflow = true -- else - - commits = @commits - - overflow = false + +- commits, hidden = limited_commits(@commits) - commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits| .row.commits-row @@ -20,6 +16,6 @@ = render commits, project: project %hr.lists-separator -- if overflow +- if hidden > 0 .alert.alert-warning - Not shown: #{@commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE} more commits + Not shown: #{hidden} more commits -- GitLab From 13eb53e5c79e17976f5518f0c55fe056355c7719 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Fri, 4 Mar 2016 15:47:11 +0100 Subject: [PATCH 026/223] Remove outdated comment --- app/helpers/commits_helper.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 4353ceaa757e..f994c9e61707 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -214,7 +214,6 @@ def clean(string) def limited_commits(commits) if commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE - # Not 100% sure we need to decorate but it is idempotent and not so slow [ commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE), commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE -- GitLab From 7582e59dc0fa2d1b374ea40554e4ca0fe2245dc8 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Fri, 4 Mar 2016 16:11:33 +0100 Subject: [PATCH 027/223] Add option to show archived projects on dahsboard Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- app/controllers/concerns/filter_projects.rb | 15 ++++++++++++ .../dashboard/projects_controller.rb | 24 +++++++------------ .../explore/projects_controller.rb | 11 ++++----- .../explore/projects/_dropdown.html.haml | 8 +++++++ 4 files changed, 36 insertions(+), 22 deletions(-) create mode 100644 app/controllers/concerns/filter_projects.rb diff --git a/app/controllers/concerns/filter_projects.rb b/app/controllers/concerns/filter_projects.rb new file mode 100644 index 000000000000..f63b703d1012 --- /dev/null +++ b/app/controllers/concerns/filter_projects.rb @@ -0,0 +1,15 @@ +# == FilterProjects +# +# Controller concern to handle projects filtering +# * by name +# * by archived state +# +module FilterProjects + extend ActiveSupport::Concern + + def filter_projects(projects) + projects = projects.search(params[:filter_projects]) if params[:filter_projects].present? + projects = projects.non_archived if params[:archived].blank? + projects + end +end diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index dc880b634e53..fc51c3241af6 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -1,18 +1,15 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController + include FilterProjects + before_action :event_filter def index - @projects = current_user.authorized_projects.sorted_by_activity.non_archived - @projects = @projects.sort(@sort = params[:sort]) + @projects = current_user.authorized_projects.sorted_by_activity + @projects = filter_projects(@projects) @projects = @projects.includes(:namespace) + @projects = @projects.sort(@sort = params[:sort]) + @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? - terms = params[:filter_projects] - - if terms.present? - @projects = @projects.search(terms) - end - - @projects = @projects.page(params[:page]).per(PER_PAGE) if terms.blank? @last_push = current_user.recent_push respond_to do |format| @@ -32,16 +29,11 @@ def index def starred @projects = current_user.starred_projects.sorted_by_activity + @projects = filter_projects(@projects) @projects = @projects.includes(:namespace, :forked_from_project, :tags) @projects = @projects.sort(@sort = params[:sort]) + @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? - terms = params[:filter_projects] - - if terms.present? - @projects = @projects.search(terms) - end - - @projects = @projects.page(params[:page]).per(PER_PAGE) if terms.blank? @last_push = current_user.recent_push @groups = [] diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index a384f3004db6..5b811db3068e 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -1,12 +1,12 @@ class Explore::ProjectsController < Explore::ApplicationController + include FilterProjects + def index @projects = ProjectsFinder.new.execute(current_user) @tags = @projects.tags_on(:tags) @projects = @projects.tagged_with(params[:tag]) if params[:tag].present? @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? - @projects = @projects.non_archived - @projects = @projects.search(params[:search]) if params[:search].present? - @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? + @projects = filter_projects(@projects) @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? @@ -22,8 +22,7 @@ def index def trending @projects = TrendingProjectsFinder.new.execute(current_user) - @projects = @projects.non_archived - @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? + @projects = filter_projects(@projects) @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? respond_to do |format| @@ -38,7 +37,7 @@ def trending def starred @projects = ProjectsFinder.new.execute(current_user) - @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? + @projects = filter_projects(@projects) @projects = @projects.reorder('star_count DESC') @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? diff --git a/app/views/explore/projects/_dropdown.html.haml b/app/views/explore/projects/_dropdown.html.haml index a4b4cd8d6c7a..8859cf1eb155 100644 --- a/app/views/explore/projects/_dropdown.html.haml +++ b/app/views/explore/projects/_dropdown.html.haml @@ -18,3 +18,11 @@ = sort_title_recently_updated = link_to explore_projects_filter_path(sort: sort_value_oldest_updated) do = sort_title_oldest_updated + %li.divider + %li + - if params[:archived].present? + = link_to explore_projects_filter_path(sort: @sort, archived: nil) do + Hide archived projects + - else + = link_to explore_projects_filter_path(sort: @sort, archived: true) do + Show archived projects -- GitLab From 4f75a41732811f5df93bd649556663aab2f9c6cd Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Fri, 4 Mar 2016 16:42:37 +0100 Subject: [PATCH 028/223] Make projects sort dropdown shared so it can be reused for other pages Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- app/helpers/explore_helper.rb | 12 ++---------- app/views/dashboard/_projects_head.html.haml | 2 +- app/views/explore/projects/_filter.html.haml | 10 +++++----- .../projects/_dropdown.html.haml | 14 +++++++------- 4 files changed, 15 insertions(+), 23 deletions(-) rename app/views/{explore => shared}/projects/_dropdown.html.haml (51%) diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb index 3648757428b2..337b0aacbb52 100644 --- a/app/helpers/explore_helper.rb +++ b/app/helpers/explore_helper.rb @@ -1,5 +1,5 @@ module ExploreHelper - def explore_projects_filter_path(options={}) + def filter_projects_path(options={}) exist_opts = { sort: params[:sort], scope: params[:scope], @@ -9,15 +9,7 @@ def explore_projects_filter_path(options={}) } options = exist_opts.merge(options) - - path = if explore_controller? - explore_projects_path - elsif current_action?(:starred) - starred_dashboard_projects_path - else - dashboard_projects_path - end - + path = request.path path << "?#{options.to_param}" path end diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 40f88261c101..9da3fcbd986c 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -15,7 +15,7 @@ .nav-controls = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| = search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field', tabindex: "2" - = render 'explore/projects/dropdown' + = render 'shared/projects/dropdown' - if current_user.can_create_project? = link_to new_project_path, class: 'btn btn-new' do = icon('plus') diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index 39e3e8e2738b..cd485da5104f 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -10,11 +10,11 @@ %b.caret %ul.dropdown-menu %li - = link_to explore_projects_filter_path(visibility_level: nil) do + = link_to filter_projects_path(visibility_level: nil) do Any - Gitlab::VisibilityLevel.values.each do |level| %li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' } - = link_to explore_projects_filter_path(visibility_level: level) do + = link_to filter_projects_path(visibility_level: level) do = visibility_level_icon(level) = visibility_level_label(level) @@ -30,11 +30,11 @@ %b.caret %ul.dropdown-menu %li - = link_to explore_projects_filter_path(tag: nil) do + = link_to filter_projects_path(tag: nil) do Any - @tags.each do |tag| %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' } - = link_to explore_projects_filter_path(tag: tag.name) do - %i.fa.fa-tag + = link_to filter_projects_path(tag: tag.name) do + = icon('tag') = tag.name diff --git a/app/views/explore/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml similarity index 51% rename from app/views/explore/projects/_dropdown.html.haml rename to app/views/shared/projects/_dropdown.html.haml index 8859cf1eb155..3d721c40e24a 100644 --- a/app/views/explore/projects/_dropdown.html.haml +++ b/app/views/shared/projects/_dropdown.html.haml @@ -8,21 +8,21 @@ %b.caret %ul.dropdown-menu.dropdown-menu-align-right %li - = link_to explore_projects_filter_path(sort: sort_value_name) do + = link_to filter_projects_path(sort: sort_value_name) do = sort_title_name - = link_to explore_projects_filter_path(sort: sort_value_recently_created) do + = link_to filter_projects_path(sort: sort_value_recently_created) do = sort_title_recently_created - = link_to explore_projects_filter_path(sort: sort_value_oldest_created) do + = link_to filter_projects_path(sort: sort_value_oldest_created) do = sort_title_oldest_created - = link_to explore_projects_filter_path(sort: sort_value_recently_updated) do + = link_to filter_projects_path(sort: sort_value_recently_updated) do = sort_title_recently_updated - = link_to explore_projects_filter_path(sort: sort_value_oldest_updated) do + = link_to filter_projects_path(sort: sort_value_oldest_updated) do = sort_title_oldest_updated %li.divider %li - if params[:archived].present? - = link_to explore_projects_filter_path(sort: @sort, archived: nil) do + = link_to filter_projects_path(sort: @sort, archived: nil) do Hide archived projects - else - = link_to explore_projects_filter_path(sort: @sort, archived: true) do + = link_to filter_projects_path(sort: @sort, archived: true) do Show archived projects -- GitLab From c94fa4b7d012a882b6ea141e7aa555dd350c8cfc Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Fri, 4 Mar 2016 16:42:58 +0100 Subject: [PATCH 029/223] Add projects list sort dropdown to group page Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- app/controllers/groups_controller.rb | 6 ++++-- app/views/groups/_projects.html.haml | 9 +++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index ca5ce1e20465..f05c29e9974f 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,4 +1,5 @@ class GroupsController < Groups::ApplicationController + include FilterProjects include IssuesAction include MergeRequestsAction @@ -41,7 +42,8 @@ def create def show @last_push = current_user.recent_push if current_user @projects = @projects.includes(:namespace) - @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? + @projects = filter_projects(@projects) + @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? respond_to do |format| @@ -98,7 +100,7 @@ def group end def load_projects - @projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity.non_archived + @projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity end # Dont allow unauthorized access to group diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index 794aa57b55a3..7cd8e9bea46d 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -3,9 +3,10 @@ = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| - if @projects.present? = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false - - if can? current_user, :create_projects, @group - = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do - = icon('plus') - New Project + = render 'shared/projects/dropdown' + - if can? current_user, :create_projects, @group + = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do + = icon('plus') + New Project = render 'shared/projects/list', projects: @projects, stars: false, skip_namespace: true -- GitLab From 4b0bedee7ad6d9006050cc8a0ca0fb81044230ab Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Fri, 4 Mar 2016 16:56:56 +0100 Subject: [PATCH 030/223] Add test for archive toggle feature Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- CHANGELOG | 1 + features/dashboard/archived_projects.feature | 5 +++++ features/steps/dashboard/archived_projects.rb | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 1c840f102e1c..647538e6583f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -17,6 +17,7 @@ v 8.6.0 (unreleased) - Increase the notes polling timeout over time (Roberto Dip) - Show labels in dashboard and group milestone views - Add main language of a project in the list of projects (Tiago Botelho) + - Add ability to show archived projects on dashboard, explore and group pages v 8.5.4 - Do not cache requests for badges (including builds badge) diff --git a/features/dashboard/archived_projects.feature b/features/dashboard/archived_projects.feature index 69b3a7764419..bed9282f1c65 100644 --- a/features/dashboard/archived_projects.feature +++ b/features/dashboard/archived_projects.feature @@ -10,3 +10,8 @@ Feature: Dashboard Archived Projects Scenario: I should see non-archived projects on dashboard Then I should see "Shop" project link And I should not see "Forum" project link + + Scenario: I toggle show of archived projects on dashboard + When I click "Show archived projects" link + Then I should see "Shop" project link + And I should see "Forum" project link diff --git a/features/steps/dashboard/archived_projects.rb b/features/steps/dashboard/archived_projects.rb index 36e092f50c60..6510f8d9b326 100644 --- a/features/steps/dashboard/archived_projects.rb +++ b/features/steps/dashboard/archived_projects.rb @@ -19,4 +19,8 @@ class Spinach::Features::DashboardArchivedProjects < Spinach::FeatureSteps step 'I should see "Forum" project link' do expect(page).to have_link "Forum" end + + step 'I click "Show archived projects" link' do + click_link "Show archived projects" + end end -- GitLab From 91745286392d33cf97385f937d55992cdcf03ab2 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Fri, 4 Mar 2016 17:53:13 +0100 Subject: [PATCH 031/223] Show active sorting method for projects list Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- app/helpers/sorting_helper.rb | 10 +++++ app/views/shared/projects/_dropdown.html.haml | 37 ++++++++----------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index f9026b887dad..2f2d2721d6df 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -16,6 +16,16 @@ def sort_options_hash } end + def projects_sort_options_hash + { + sort_value_name => sort_title_name, + sort_value_recently_updated => sort_title_recently_updated, + sort_value_oldest_updated => sort_title_oldest_updated, + sort_value_recently_created => sort_title_recently_created, + sort_value_oldest_created => sort_title_oldest_created, + } + end + def sort_title_oldest_updated 'Oldest updated' end diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml index 3d721c40e24a..89569906dba7 100644 --- a/app/views/shared/projects/_dropdown.html.haml +++ b/app/views/shared/projects/_dropdown.html.haml @@ -1,28 +1,23 @@ +- @sort ||= sort_value_recently_updated +- archived = params[:archived] .dropdown.inline %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} %span.light - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_recently_updated + = projects_sort_options_hash[@sort] %b.caret %ul.dropdown-menu.dropdown-menu-align-right - %li - = link_to filter_projects_path(sort: sort_value_name) do - = sort_title_name - = link_to filter_projects_path(sort: sort_value_recently_created) do - = sort_title_recently_created - = link_to filter_projects_path(sort: sort_value_oldest_created) do - = sort_title_oldest_created - = link_to filter_projects_path(sort: sort_value_recently_updated) do - = sort_title_recently_updated - = link_to filter_projects_path(sort: sort_value_oldest_updated) do - = sort_title_oldest_updated + - projects_sort_options_hash.each do |value, title| + %li + = link_to filter_projects_path(sort: value, archived: archived) do + = icon('check') if @sort == value + = title + %li.divider %li - - if params[:archived].present? - = link_to filter_projects_path(sort: @sort, archived: nil) do - Hide archived projects - - else - = link_to filter_projects_path(sort: @sort, archived: true) do - Show archived projects + = link_to filter_projects_path(sort: @sort, archived: nil) do + = icon('check') unless params[:archived].present? + Hide archived projects + %li + = link_to filter_projects_path(sort: @sort, archived: true) do + = icon('check') if params[:archived].present? + Show archived projects -- GitLab From b33de3c0eafcec3c69fb1791b6fd55f95674fecf Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Fri, 4 Mar 2016 19:24:47 +0100 Subject: [PATCH 032/223] Fix test Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- features/explore/projects.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/explore/projects.feature b/features/explore/projects.feature index 7df6b6f09bab..092e18d1b869 100644 --- a/features/explore/projects.feature +++ b/features/explore/projects.feature @@ -140,4 +140,4 @@ Feature: Explore Projects When I visit the explore starred projects Then I should see project "Community" And I should see project "Internal" - And I should see project "Archive" + And I should not see project "Archive" -- GitLab From 61baeb42dea1588244c338c633f11d1bd05575c7 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Fri, 4 Mar 2016 20:12:00 +0100 Subject: [PATCH 033/223] Align projects sort checkbox Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- .../stylesheets/framework/dropdowns.scss | 19 +++++++++++++++++++ app/views/shared/projects/_dropdown.html.haml | 4 +++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index d4878b333f94..d2c7cdc3e922 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -100,3 +100,22 @@ font-size: 13px; line-height: 22px; } + +.dropdown-with-checkmark { + li > .legend { + font-size: 13px; + color: $secondary-text; + margin-bottom: 5px; + } + + li > a { + padding-left: 20px; + line-height: 28px; + + i { + position: absolute; + left: 1px; + line-height: 28px; + } + } +} diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml index 89569906dba7..daaac092dce9 100644 --- a/app/views/shared/projects/_dropdown.html.haml +++ b/app/views/shared/projects/_dropdown.html.haml @@ -5,7 +5,9 @@ %span.light = projects_sort_options_hash[@sort] %b.caret - %ul.dropdown-menu.dropdown-menu-align-right + %ul.dropdown-menu.dropdown-menu-align-right.dropdown-with-checkmark + %li + .legend Sort by - projects_sort_options_hash.each do |value, title| %li = link_to filter_projects_path(sort: value, archived: archived) do -- GitLab From cf130f1af659c3b830d2f8d5011923266cc63f78 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Sat, 5 Mar 2016 11:51:56 +0000 Subject: [PATCH 034/223] Updated project list dropdown to use dropdown classes --- .../stylesheets/framework/dropdowns.scss | 28 ++++--------------- app/views/shared/projects/_dropdown.html.haml | 15 ++++------ 2 files changed, 11 insertions(+), 32 deletions(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index d2c7cdc3e922..3dc524ccca44 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -81,8 +81,9 @@ &::before { content: "\f00c"; position: absolute; - left: 4px; - top: 8px; + left: 5px; + top: 50%; + margin-top: -7px; font: normal normal normal 14px/1 FontAwesome; font-size: inherit; text-rendering: auto; @@ -94,28 +95,9 @@ } .dropdown-header { - padding-left: 10px; - padding-right: 10px; + padding-left: 5px; + padding-right: 5px; color: $dropdown-header-color; font-size: 13px; line-height: 22px; } - -.dropdown-with-checkmark { - li > .legend { - font-size: 13px; - color: $secondary-text; - margin-bottom: 5px; - } - - li > a { - padding-left: 20px; - line-height: 28px; - - i { - position: absolute; - left: 1px; - line-height: 28px; - } - } -} diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml index daaac092dce9..e7e04621ff44 100644 --- a/app/views/shared/projects/_dropdown.html.haml +++ b/app/views/shared/projects/_dropdown.html.haml @@ -5,21 +5,18 @@ %span.light = projects_sort_options_hash[@sort] %b.caret - %ul.dropdown-menu.dropdown-menu-align-right.dropdown-with-checkmark - %li - .legend Sort by + %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable + %li.dropdown-header + Sort by - projects_sort_options_hash.each do |value, title| %li - = link_to filter_projects_path(sort: value, archived: archived) do - = icon('check') if @sort == value + = link_to filter_projects_path(sort: value, archived: archived), class: ("is-active" if @sort == value) do = title %li.divider %li - = link_to filter_projects_path(sort: @sort, archived: nil) do - = icon('check') unless params[:archived].present? + = link_to filter_projects_path(sort: @sort, archived: nil), class: ("is-active" unless params[:archived].present?) do Hide archived projects %li - = link_to filter_projects_path(sort: @sort, archived: true) do - = icon('check') if params[:archived].present? + = link_to filter_projects_path(sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do Show archived projects -- GitLab From b1285fbc6f962c87f574d10e09bb11dd53d5645c Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Sat, 5 Mar 2016 17:17:49 -0500 Subject: [PATCH 035/223] Remove `Snippet#expires_at` This was removed from the interface in https://github.com/gitlabhq/gitlabhq/pull/6027 but its implementation lingered around for two years. --- app/finders/snippets_finder.rb | 6 +++--- app/helpers/snippets_helper.rb | 10 ---------- app/models/personal_snippet.rb | 1 - app/models/project_snippet.rb | 3 --- app/models/snippet.rb | 7 ------- .../20160305220806_remove_expires_at_from_snippets.rb | 5 +++++ db/schema.rb | 4 +--- doc/api/notes.md | 1 - doc/api/project_snippets.md | 1 - doc/web_hooks/web_hooks.md | 1 - lib/api/entities.rb | 2 +- spec/models/project_snippet_spec.rb | 1 - spec/models/snippet_spec.rb | 1 - 13 files changed, 10 insertions(+), 33 deletions(-) create mode 100644 db/migrate/20160305220806_remove_expires_at_from_snippets.rb diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index 07b5759443b2..a41172816b81 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -4,7 +4,7 @@ def execute(current_user, params = {}) case filter when :all then - snippets(current_user).fresh.non_expired + snippets(current_user).fresh when :by_user then by_user(current_user, params[:user], params[:scope]) when :by_project @@ -27,7 +27,7 @@ def snippets(current_user) end def by_user(current_user, user, scope) - snippets = user.snippets.fresh.non_expired + snippets = user.snippets.fresh return snippets.are_public unless current_user @@ -48,7 +48,7 @@ def by_user(current_user, user, scope) end def by_project(current_user, project) - snippets = project.snippets.fresh.non_expired + snippets = project.snippets.fresh if current_user if project.team.member?(current_user.id) diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 41ae40489929..0a5a8eb5aeec 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -1,14 +1,4 @@ module SnippetsHelper - def lifetime_select_options - options = [ - ['forever', nil], - ['1 day', "#{Date.current + 1.day}"], - ['1 week', "#{Date.current + 1.week}"], - ['1 month', "#{Date.current + 1.month}"] - ] - options_for_select(options) - end - def reliable_snippet_path(snippet) if snippet.project_id? namespace_project_snippet_path(snippet.project.namespace, diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb index 9cee3b70cb3e..452f3913eef6 100644 --- a/app/models/personal_snippet.rb +++ b/app/models/personal_snippet.rb @@ -10,7 +10,6 @@ # created_at :datetime # updated_at :datetime # file_name :string(255) -# expires_at :datetime # type :string(255) # visibility_level :integer default(0), not null # diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb index 9e2c1b0e18e8..1f7d85a5f3d4 100644 --- a/app/models/project_snippet.rb +++ b/app/models/project_snippet.rb @@ -10,7 +10,6 @@ # created_at :datetime # updated_at :datetime # file_name :string(255) -# expires_at :datetime # type :string(255) # visibility_level :integer default(0), not null # @@ -23,6 +22,4 @@ class ProjectSnippet < Snippet # Scopes scope :fresh, -> { order("created_at DESC") } - scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) } - scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) } end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index f876be7a4c85..dd3925c7a7d4 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -10,7 +10,6 @@ # created_at :datetime # updated_at :datetime # file_name :string(255) -# expires_at :datetime # type :string(255) # visibility_level :integer default(0), not null # @@ -46,8 +45,6 @@ class Snippet < ActiveRecord::Base scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) } scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) } scope :fresh, -> { order("created_at DESC") } - scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) } - scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) } participant :author, :notes @@ -111,10 +108,6 @@ def mode nil end - def expired? - expires_at && expires_at < Time.current - end - def visibility_level_field visibility_level end diff --git a/db/migrate/20160305220806_remove_expires_at_from_snippets.rb b/db/migrate/20160305220806_remove_expires_at_from_snippets.rb new file mode 100644 index 000000000000..fc12b5b09e69 --- /dev/null +++ b/db/migrate/20160305220806_remove_expires_at_from_snippets.rb @@ -0,0 +1,5 @@ +class RemoveExpiresAtFromSnippets < ActiveRecord::Migration + def change + remove_column :snippets, :expires_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 71d9257a31e3..2d6b9b5a4cc6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160222153918) do +ActiveRecord::Schema.define(version: 20160305220806) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -777,7 +777,6 @@ t.datetime "created_at" t.datetime "updated_at" t.string "file_name" - t.datetime "expires_at" t.string "type" t.integer "visibility_level", default: 0, null: false end @@ -785,7 +784,6 @@ add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree add_index "snippets", ["created_at", "id"], name: "index_snippets_on_created_at_and_id", using: :btree add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree - add_index "snippets", ["expires_at"], name: "index_snippets_on_expires_at", using: :btree add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree add_index "snippets", ["updated_at"], name: "index_snippets_on_updated_at", using: :btree add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree diff --git a/doc/api/notes.md b/doc/api/notes.md index d4d63e825abe..85d4f0bafa27 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -145,7 +145,6 @@ Parameters: "state": "active", "created_at": "2013-09-30T13:46:01Z" }, - "expires_at": null, "updated_at": "2013-10-02T07:34:20Z", "created_at": "2013-10-02T07:34:20Z" } diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md index a7acf37b5bcc..fb802102e3a5 100644 --- a/doc/api/project_snippets.md +++ b/doc/api/project_snippets.md @@ -51,7 +51,6 @@ Parameters: "state": "active", "created_at": "2012-05-23T08:00:58Z" }, - "expires_at": null, "updated_at": "2012-06-28T10:52:04Z", "created_at": "2012-06-28T10:52:04Z" } diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index b82306bd1dab..e2b53c45ab18 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -582,7 +582,6 @@ X-Gitlab-Event: Note Hook "created_at": "2015-04-09 02:40:38 UTC", "updated_at": "2015-04-09 02:40:38 UTC", "file_name": "test.rb", - "expires_at": null, "type": "ProjectSnippet", "visibility_level": 0 } diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 0779fb881a03..5b5b8bd044b8 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -143,7 +143,7 @@ class RepoCommitDetail < RepoCommit class ProjectSnippet < Grape::Entity expose :id, :title, :file_name expose :author, using: Entities::UserBasic - expose :expires_at, :updated_at, :created_at + expose :updated_at, :created_at end class ProjectEntity < Grape::Entity diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb index cc92eb0bd9f2..e0feb606f781 100644 --- a/spec/models/project_snippet_spec.rb +++ b/spec/models/project_snippet_spec.rb @@ -10,7 +10,6 @@ # created_at :datetime # updated_at :datetime # file_name :string(255) -# expires_at :datetime # type :string(255) # visibility_level :integer default(0), not null # diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index eb2dbbdc5a4e..7e5b5499aeaa 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -10,7 +10,6 @@ # created_at :datetime # updated_at :datetime # file_name :string(255) -# expires_at :datetime # type :string(255) # visibility_level :integer default(0), not null # -- GitLab From 2ceceed7008fe7d0268951d131359682c2908f67 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Mon, 7 Mar 2016 10:41:49 +0100 Subject: [PATCH 036/223] Finish refactor --- app/views/projects/commits/_commit_list.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml index 5fe4d918833e..62e5f346777c 100644 --- a/app/views/projects/commits/_commit_list.html.haml +++ b/app/views/projects/commits/_commit_list.html.haml @@ -1,4 +1,4 @@ -- commits, hidden = limited_commits(@commits, @project) +- commits, hidden = limited_commits(@commits) - commits = Commit.decorate(commits, @project) %div.panel.panel-default -- GitLab From 3f4d24d3c943de750065a2c37f5d292018c06507 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Mon, 7 Mar 2016 10:42:27 +0100 Subject: [PATCH 037/223] Change "some commits hidden" message --- app/views/projects/commits/_commit_list.html.haml | 2 +- app/views/projects/commits/_commits.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml index 62e5f346777c..c0988eca6de2 100644 --- a/app/views/projects/commits/_commit_list.html.haml +++ b/app/views/projects/commits/_commit_list.html.haml @@ -9,6 +9,6 @@ - commits.each do |commit| = render "projects/commits/inline_commit", commit: commit, project: @project %li.warning-row.unstyled - other #{hidden} commits hidden to prevent performance issues. + #{number_with_delimiter(hidden)} additional commits have been omitted. - else %ul.well-list= render commits, project: @project diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 998251094a46..787fe443ac8a 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -18,4 +18,4 @@ - if hidden > 0 .alert.alert-warning - Not shown: #{hidden} more commits + #{number_with_delimiter(hidden)} additional commits have been omitted. -- GitLab From bd1faee5113844069b52fff5b1a5e59f21a9c3ad Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 7 Mar 2016 09:44:55 +0000 Subject: [PATCH 038/223] Affix the build scroll controls Closes #4709 --- app/assets/javascripts/ci_build.js.coffee | 14 +++++++++++++ app/assets/javascripts/dispatcher.js.coffee | 2 ++ app/assets/stylesheets/pages/builds.scss | 23 +++++++++++++++++---- app/views/projects/builds/show.html.haml | 2 +- 4 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 app/assets/javascripts/ci_build.js.coffee diff --git a/app/assets/javascripts/ci_build.js.coffee b/app/assets/javascripts/ci_build.js.coffee new file mode 100644 index 000000000000..e110eece16be --- /dev/null +++ b/app/assets/javascripts/ci_build.js.coffee @@ -0,0 +1,14 @@ +class @CiBuild + constructor: -> + @initScrollButtonAffix() + + initScrollButtonAffix: -> + buildScroll = $('#js-build-scroll') + body = $('body') + buildTrace = $('#build-trace') + + buildScroll.affix( + offset: + bottom: -> + body.outerHeight() - (buildTrace.outerHeight() + buildTrace.offset().top) + ) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 54b28f2dd8d0..77056bba8d27 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -103,6 +103,8 @@ class Dispatcher new ProjectFork() when 'projects:artifacts:browse' new BuildArtifacts() + when 'projects:builds:show' + new CiBuild() switch path.first() when 'admin' diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 3c2997c1d5ad..75f298019e32 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -27,10 +27,25 @@ } .scroll-controls { - position: fixed; - bottom: 10px; - left: 250px; - z-index: 100; + &.affix-top { + position: absolute; + top: 10px; + right: 25px; + } + + &.affix-bottom { + position: absolute; + right: 25px; + } + + &.affix { + right: 30px; + bottom: 15px; + + @media (min-width: $screen-md-min) { + right: 26%; + } + } a { display: block; diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 8eec78a557c4..be7cc0f256c1 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -70,7 +70,7 @@ .autoscroll-container %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll .clearfix - .scroll-controls + #js-build-scroll.scroll-controls = link_to '#up-build-trace', class: 'btn' do %i.fa.fa-angle-up = link_to '#down-build-trace', class: 'btn' do -- GitLab From 891b9ed6ad71a7e37cf2445fbbc6251b2438959f Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Mon, 7 Mar 2016 14:11:38 +0100 Subject: [PATCH 039/223] Revert changes in the Project model --- app/models/project.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 97894f8c1481..c0f2ab91fa41 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -56,7 +56,6 @@ class Project < ActiveRecord::Base extend Gitlab::ConfigHelper UNKNOWN_IMPORT_URL = 'http://unknown.git' - AVATAR_BRANCH = 'master' default_value_for :archived, false default_value_for :visibility_level, gitlab_config_features.visibility_level @@ -546,9 +545,9 @@ def avatar_type end def avatar_in_git - @avatar_file ||= 'logo.png' if repository.blob_at_branch(AVATAR_BRANCH, 'logo.png') - @avatar_file ||= 'logo.jpg' if repository.blob_at_branch(AVATAR_BRANCH, 'logo.jpg') - @avatar_file ||= 'logo.gif' if repository.blob_at_branch(AVATAR_BRANCH, 'logo.gif') + @avatar_file ||= 'logo.png' if repository.blob_at_branch('master', 'logo.png') + @avatar_file ||= 'logo.jpg' if repository.blob_at_branch('master', 'logo.jpg') + @avatar_file ||= 'logo.gif' if repository.blob_at_branch('master', 'logo.gif') @avatar_file end -- GitLab From 55c64c537a1e22b383dd226855543d916ad3bfcb Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Mon, 7 Mar 2016 14:27:53 +0100 Subject: [PATCH 040/223] Refactor caching code --- app/controllers/projects/avatars_controller.rb | 5 +++-- app/controllers/projects/raw_controller.rb | 4 ++-- app/helpers/blob_helper.rb | 17 ++++++++++------- app/models/blob.rb | 3 +++ 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb index 6de7888888fc..a6bebc46b061 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -7,8 +7,9 @@ def show @blob = @repository.blob_at_branch('master', @project.avatar_in_git) if @blob headers['X-Content-Type-Options'] = 'nosniff' - set_cache_headers - check_etag! + + return if cached_blob? + headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob)) headers['Content-Disposition'] = 'inline' headers['Content-Type'] = safe_content_type(@blob) diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index b6ff08262d74..10de0e605306 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -12,8 +12,8 @@ def show if @blob headers['X-Content-Type-Options'] = 'nosniff' - check_etag! - set_cache_headers + + return if cached_blob? if @blob.lfs_pointer? send_lfs_object diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index adb56e49c62e..e5c0ed4b7bd2 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -153,7 +153,10 @@ def safe_content_type(blob) end end - def set_cache_headers + def cached_blob? + stale = stale?(etag: @blob.id) # The #stale? method sets cache headers. + + # Because we are opionated we set the cache headers ourselves. if @project.visibility_level == Project::PUBLIC cache_control = 'public, ' else @@ -162,19 +165,19 @@ def set_cache_headers if @ref && @commit && @ref == @commit.id # This is a link to a commit by its commit SHA. That means that the blob - # is immutable. - cache_control << 'max-age=600' # 10 minutes + # is immutable. The only reason to invalidate the cache is if the commit + # was deleted or if the user lost access to the repository. + max_age = Blob::CACHE_TIME_IMMUTABLE else # A branch or tag points at this blob. That means that the expected blob # value may change over time. - cache_control << 'max-age=60' # 1 minute + max_age = Blob::CACHE_TIME end + cache_control << "max-age=#{max_age}" headers['Cache-Control'] = cache_control headers['ETag'] = @blob.id - end - def check_etag! - stale?(etag: @blob.id) + !stale end end diff --git a/app/models/blob.rb b/app/models/blob.rb index 8ee9f3006b2b..72e6c5fa3fd9 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -1,5 +1,8 @@ # Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects class Blob < SimpleDelegator + CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute + CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour + # Wrap a Gitlab::Git::Blob object, or return nil when given nil # # This method prevents the decorated object from evaluating to "truthy" when -- GitLab From db8edee69a5ab83bc64a96380beb8b4e0857c67b Mon Sep 17 00:00:00 2001 From: Florent Baldino <fbaldino@onisep.fr> Date: Mon, 7 Mar 2016 15:35:26 +0100 Subject: [PATCH 041/223] Add shortcut to toggle markdown preview --- CHANGELOG | 1 + .../javascripts/markdown_preview.js.coffee | 46 ++++++++++++++++--- app/assets/javascripts/shortcuts.js.coffee | 15 ++++++ app/views/help/_shortcuts.html.haml | 4 ++ 4 files changed, 59 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 647538e6583f..602ec072eabb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,7 @@ v 8.6.0 (unreleased) - Show labels in dashboard and group milestone views - Add main language of a project in the list of projects (Tiago Botelho) - Add ability to show archived projects on dashboard, explore and group pages + - Add shortcut to toggle markdown preview (Florent Baldino) v 8.5.4 - Do not cache requests for badges (including builds badge) diff --git a/app/assets/javascripts/markdown_preview.js.coffee b/app/assets/javascripts/markdown_preview.js.coffee index 98fc8f173401..3aa61de2f5a5 100644 --- a/app/assets/javascripts/markdown_preview.js.coffee +++ b/app/assets/javascripts/markdown_preview.js.coffee @@ -6,6 +6,7 @@ class @MarkdownPreview # Minimum number of users referenced before triggering a warning referenceThreshold: 10 + ajaxCache: {} showPreview: (form) -> preview = form.find('.js-md-preview') @@ -24,12 +25,16 @@ class @MarkdownPreview renderMarkdown: (text, success) -> return unless window.markdown_preview_path + return success(@ajaxCache[text]) if @ajaxCache[text] + $.ajax type: 'POST' url: window.markdown_preview_path data: { text: text } dataType: 'json' - success: success + success: (response) => + @ajaxCache[text] = response + success(response) hideReferencedUsers: (form) -> referencedUsers = form.find('.referenced-users') @@ -49,6 +54,7 @@ markdownPreview = new MarkdownPreview() previewButtonSelector = '.js-md-preview-button' writeButtonSelector = '.js-md-write-button' +lastTextareaPreviewed = null $.fn.setupMarkdownPreview = -> $form = $(this) @@ -58,10 +64,10 @@ $.fn.setupMarkdownPreview = -> form_textarea.on 'input', -> markdownPreview.hideReferencedUsers($form) form_textarea.on 'blur', -> markdownPreview.showPreview($form) -$(document).on 'click', previewButtonSelector, (e) -> - e.preventDefault() +$(document).on 'markdown-preview:show', (e, $form) -> + return unless $form - $form = $(this).closest('form') + lastTextareaPreviewed = $form.find('textarea.markdown-area') # toggle tabs $form.find(writeButtonSelector).parent().removeClass('active') @@ -73,10 +79,10 @@ $(document).on 'click', previewButtonSelector, (e) -> markdownPreview.showPreview($form) -$(document).on 'click', writeButtonSelector, (e) -> - e.preventDefault() +$(document).on 'markdown-preview:hide', (e, $form) -> + return unless $form - $form = $(this).closest('form') + lastTextareaPreviewed = null # toggle tabs $form.find(writeButtonSelector).parent().addClass('active') @@ -84,4 +90,30 @@ $(document).on 'click', writeButtonSelector, (e) -> # toggle content $form.find('.md-write-holder').show() + $form.find('textarea.markdown-area').focus() $form.find('.md-preview-holder').hide() + +$(document).on 'markdown-preview:toggle', (e, keyboardEvent) -> + $target = $(keyboardEvent.target) + + if $target.is('textarea.markdown-area') + $(document).triggerHandler('markdown-preview:show', [$target.closest('form')]) + keyboardEvent.preventDefault() + else if lastTextareaPreviewed + $target = lastTextareaPreviewed + $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]) + keyboardEvent.preventDefault() + +$(document).on 'click', previewButtonSelector, (e) -> + e.preventDefault() + + $form = $(this).closest('form') + + $(document).triggerHandler('markdown-preview:show', [$form]) + +$(document).on 'click', writeButtonSelector, (e) -> + e.preventDefault() + + $form = $(this).closest('form') + + $(document).triggerHandler('markdown-preview:hide', [$form]) diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee index 9c7c2474aa47..100e3aac5352 100644 --- a/app/assets/javascripts/shortcuts.js.coffee +++ b/app/assets/javascripts/shortcuts.js.coffee @@ -4,11 +4,15 @@ class @Shortcuts Mousetrap.reset() Mousetrap.bind('?', @selectiveHelp) Mousetrap.bind('s', Shortcuts.focusSearch) + Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview) Mousetrap.bind('t', -> Turbolinks.visit(findFileURL)) if findFileURL? selectiveHelp: (e) => Shortcuts.showHelp(e, @enabledHelp) + toggleMarkdownPreview: (e) => + $(document).triggerHandler('markdown-preview:toggle', [e]) + @showHelp: (e, location) -> if $('#modal-shortcuts').length > 0 $('#modal-shortcuts').modal('show') @@ -35,3 +39,14 @@ $(document).on 'click.more_help', '.js-more-help-button', (e) -> $(@).remove() $('.hidden-shortcut').show() e.preventDefault() + +Mousetrap.stopCallback = (-> + defaultStopCallback = Mousetrap.stopCallback + + return (e, element, combo) -> + # allowed shortcuts if textarea, input, contenteditable are focused + if ['ctrl+shift+p', 'command+shift+p'].indexOf(combo) != -1 + return false + else + return defaultStopCallback.apply(@, arguments) +)() diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 82d2d4aabedc..e6e3bfbea717 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -22,6 +22,10 @@ %td.shortcut .key ? %td Show this dialog + %tr + %td.shortcut + .key ctrl shift p + %td Toggle Markdown preview %tbody %tr %th -- GitLab From cb14eda471bce1901890dd0aec9bbe2ae9e89021 Mon Sep 17 00:00:00 2001 From: Christian Mehlmauer <firefart@gmail.com> Date: Mon, 7 Mar 2016 15:39:12 +0100 Subject: [PATCH 042/223] make cleanup optional --- doc/update/8.4-to-8.5.md | 2 +- doc/update/patch_versions.md | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/update/8.4-to-8.5.md b/doc/update/8.4-to-8.5.md index 57131ee9e719..0a9cb5683e7a 100644 --- a/doc/update/8.4-to-8.5.md +++ b/doc/update/8.4-to-8.5.md @@ -64,7 +64,7 @@ sudo -u git -H bundle install --without postgres development test --deployment # PostgreSQL installations (note: the line below states '--without mysql') sudo -u git -H bundle install --without mysql development test --deployment -# Clean up old gems +# Optional: clean up old gems sudo -u git -H bundle clean # Run database migrations diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index ce7ae0d285c0..f446ed0a35b9 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -62,8 +62,13 @@ sudo -u git -H bundle install --without development test mysql --deployment # MySQL sudo -u git -H bundle install --without development test postgres --deployment +# Optional: clean up old gems sudo -u git -H bundle clean + +# Run database migrations sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production ``` -- GitLab From 987a9fa9cb5f52266bb92a4c0f59885d8541800c Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Mon, 7 Mar 2016 16:49:46 +0100 Subject: [PATCH 043/223] Use Rails etag/cache_control helpers --- app/helpers/blob_helper.rb | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index e5c0ed4b7bd2..0f77b3b299a1 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -157,27 +157,20 @@ def cached_blob? stale = stale?(etag: @blob.id) # The #stale? method sets cache headers. # Because we are opionated we set the cache headers ourselves. - if @project.visibility_level == Project::PUBLIC - cache_control = 'public, ' - else - cache_control = 'private, ' - end + response.cache_control[:public] = @project.public? if @ref && @commit && @ref == @commit.id # This is a link to a commit by its commit SHA. That means that the blob # is immutable. The only reason to invalidate the cache is if the commit # was deleted or if the user lost access to the repository. - max_age = Blob::CACHE_TIME_IMMUTABLE + response.cache_control[:max_age] = Blob::CACHE_TIME_IMMUTABLE else # A branch or tag points at this blob. That means that the expected blob # value may change over time. - max_age = Blob::CACHE_TIME + response.cache_control[:max_age] = Blob::CACHE_TIME end - cache_control << "max-age=#{max_age}" - headers['Cache-Control'] = cache_control - headers['ETag'] = @blob.id - + response.etag = @blob.id !stale end end -- GitLab From 9a9def0dcd16bcbf3e341236a2443527abd5de0e Mon Sep 17 00:00:00 2001 From: Iuri de Silvio <iurisilvio@gmail.com> Date: Mon, 22 Feb 2016 23:59:13 +0000 Subject: [PATCH 044/223] Fix bug where Bitbucket `closed` issues were imported as `opened` --- CHANGELOG | 1 + lib/gitlab/bitbucket_import/importer.rb | 2 +- .../gitlab/bitbucket_import/importer_spec.rb | 88 +++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 spec/lib/gitlab/bitbucket_import/importer_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 602ec072eabb..47a61317d555 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,7 @@ v 8.6.0 (unreleased) - Add support for cross-project label references - Update documentation to reflect Guest role not being enforced on internal projects - Allow search for logged out users + - Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio) - Don't show Issues/MRs from archived projects in Groups view - Increase the notes polling timeout over time (Roberto Dip) - Show labels in dashboard and group milestone views diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 3f483847efaa..46e51a4bf6d9 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -76,7 +76,7 @@ def import_issues project.issues.create!( description: body, title: issue["title"], - state: %w(resolved invalid duplicate wontfix).include?(issue["status"]) ? 'closed' : 'opened', + state: %w(resolved invalid duplicate wontfix closed).include?(issue["status"]) ? 'closed' : 'opened', author_id: gl_user_id(project, reporter) ) end diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb new file mode 100644 index 000000000000..c413132abe56 --- /dev/null +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe Gitlab::BitbucketImport::Importer, lib: true do + before do + Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "bitbucket") + end + + let(:statuses) do + [ + "open", + "resolved", + "on hold", + "invalid", + "duplicate", + "wontfix", + "closed" # undocumented status + ] + end + let(:sample_issues_statuses) do + issues = [] + + statuses.map.with_index do |status, index| + issues << { + local_id: index, + status: status, + title: "Issue #{index}", + content: "Some content to issue #{index}" + } + end + + issues + end + + let(:project_identifier) { 'namespace/repo' } + let(:data) do + { + bb_session: { + bitbucket_access_token: "123456", + bitbucket_access_token_secret: "secret" + } + } + end + let(:project) do + create( + :project, + import_source: project_identifier, + import_data: ProjectImportData.new(data: data) + ) + end + let(:importer) { Gitlab::BitbucketImport::Importer.new(project) } + let(:issues_statuses_sample_data) do + { + count: sample_issues_statuses.count, + issues: sample_issues_statuses + } + end + + context 'issues statuses' do + before do + stub_request( + :get, + "https://bitbucket.org/api/1.0/repositories/#{project_identifier}" + ).to_return(status: 200, body: { has_issues: true }.to_json) + + stub_request( + :get, + "https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues?limit=50&sort=utc_created_on&start=0" + ).to_return(status: 200, body: issues_statuses_sample_data.to_json) + + sample_issues_statuses.each_with_index do |issue, index| + stub_request( + :get, + "https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues/#{issue[:local_id]}/comments" + ).to_return( + status: 200, + body: [{ author_info: { username: "username" }, utc_created_on: index }].to_json + ) + end + end + + it 'map statuses to open or closed' do + importer.execute + + expect(project.issues.where(state: "closed").size).to eq(5) + expect(project.issues.where(state: "opened").size).to eq(2) + end + end +end -- GitLab From 126fe1511713501980a827df74732d2eb60ae202 Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Mon, 7 Mar 2016 15:03:55 -0500 Subject: [PATCH 045/223] Bump poltergeist to `~> 1.9.0` --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index c66ef3cffada..62b2e55b5374 100644 --- a/Gemfile +++ b/Gemfile @@ -273,7 +273,7 @@ group :development, :test do gem 'capybara', '~> 2.4.0' gem 'capybara-screenshot', '~> 1.0.0' - gem 'poltergeist', '~> 1.8.1' + gem 'poltergeist', '~> 1.9.0' gem 'teaspoon', '~> 1.0.0' gem 'teaspoon-jasmine', '~> 2.2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 22c86e4ae8fc..cc5e16c37d31 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -552,7 +552,7 @@ GEM parser (2.2.3.0) ast (>= 1.1, < 3.0) pg (0.18.4) - poltergeist (1.8.1) + poltergeist (1.9.0) capybara (~> 2.1) cliver (~> 0.3.1) multi_json (~> 1.0) @@ -978,7 +978,7 @@ DEPENDENCIES org-ruby (~> 0.9.12) paranoia (~> 2.0) pg (~> 0.18.2) - poltergeist (~> 1.8.1) + poltergeist (~> 1.9.0) pry-rails quiet_assets (~> 1.0.2) rack-attack (~> 4.3.1) -- GitLab From 9c542224f40734b42cf7ace15aa04ab2e948eccd Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Mon, 7 Mar 2016 15:24:27 -0500 Subject: [PATCH 046/223] Remove redundant integration tests These three tests were essentially checking that adding a note to something updated its `noteable`'s `updated_at` attribute. This is well-tested Rails behavior and we shouldn't feel the need to write an integration test to verify it. At most we should be ensuring that the association definition adds the `touch: true` option, which we now do in Note's unit test. --- features/project/issues/issues.feature | 8 -------- features/project/merge_requests.feature | 19 ------------------- features/steps/project/issues/issues.rb | 4 ---- features/steps/project/merge_requests.rb | 8 -------- features/steps/shared/note.rb | 7 ------- spec/models/note_spec.rb | 2 +- 6 files changed, 1 insertion(+), 47 deletions(-) diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index 89af58dcef35..ff21c7d1b83d 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -58,14 +58,6 @@ Feature: Project Issues Then I should see comment "XML attached" And I should see an error alert section within the comment form - @javascript - Scenario: Visiting Issues after leaving a comment - Given I visit issue page "Release 0.4" - And I leave a comment like "XML attached" - And I visit project "Shop" issues page - And I sort the list by "Last updated" - Then I should see "Release 0.4" at the top - @javascript Scenario: Visiting Issues after being sorted the list Given I visit project "Shop" issues page diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index a69089f00c4c..f8d9fe1854db 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -86,15 +86,6 @@ Feature: Project Merge Requests And I leave a comment like "XML attached" Then I should see comment "XML attached" - @javascript - Scenario: Visiting Merge Requests after leaving a comment - Given project "Shop" have "Bug NS-05" open merge request with diffs inside - And I visit merge request page "Bug NS-04" - And I leave a comment like "XML attached" - And I visit project "Shop" merge requests page - And I sort the list by "Last updated" - Then I should see "Bug NS-04" at the top - @javascript Scenario: Visiting Merge Requests after being sorted the list Given I visit project "Shop" merge requests page @@ -128,16 +119,6 @@ Feature: Project Merge Requests And I sort the list by "Least popular" Then The list should be sorted by "Least popular" - @javascript - Scenario: Visiting Merge Requests after commenting on diffs - Given project "Shop" have "Bug NS-05" open merge request with diffs inside - And I visit merge request page "Bug NS-05" - And I click on the Changes tab - And I leave a comment like "Line is wrong" on diff - And I visit project "Shop" merge requests page - And I sort the list by "Last updated" - Then I should see "Bug NS-05" at the top - @javascript Scenario: I comment on a merge request diff Given project "Shop" have "Bug NS-05" open merge request with diffs inside diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index 3a189a9a1e9c..41defe3de30b 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -355,10 +355,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end end - step 'I should see "Release 0.4" at the top' do - expect(page.find('ul.content-list.issues-list li.issue:first-child')).to have_content("Release 0.4") - end - def filter_issue(text) fill_in 'issue_search', with: text end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 870656adef25..df4259b9ddf2 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -517,14 +517,6 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end end - step 'I should see "Bug NS-05" at the top' do - expect(page.find('ul.content-list.mr-list li.merge-request:first-child')).to have_content("Bug NS-05") - end - - step 'I should see "Bug NS-04" at the top' do - expect(page.find('ul.content-list.mr-list li.merge-request:first-child')).to have_content("Bug NS-04") - end - def merge_request @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05") end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index 6870d364ac07..fb0462d6e042 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -144,11 +144,4 @@ module SharedNote expect(page).to have_content("+1 Awesome!") end end - - step 'I sort the list by "Last updated"' do - find('button.dropdown-toggle.btn').click - page.within('ul.dropdown-menu.dropdown-menu-align-right li') do - click_link "Last updated" - end - end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 583937ca748b..33085dac4eae 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -24,7 +24,7 @@ describe Note, models: true do describe 'associations' do it { is_expected.to belong_to(:project) } - it { is_expected.to belong_to(:noteable) } + it { is_expected.to belong_to(:noteable).touch(true) } it { is_expected.to belong_to(:author).class_name('User') } it { is_expected.to have_many(:todos).dependent(:destroy) } -- GitLab From e3c4663cb8243dab184659bc29de3bb6b78d6fbe Mon Sep 17 00:00:00 2001 From: Patricio Cano <suprnova32@gmail.com> Date: Mon, 7 Mar 2016 16:29:22 -0500 Subject: [PATCH 047/223] Fixed headers for anchors [ci skip] --- doc/integration/ldap.md | 11 +++-------- doc/integration/saml.md | 12 +++++------- doc/project_services/jira.md | 4 ++-- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index ac15a2cdffcf..cf1f98492ea8 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -207,9 +207,7 @@ the LDAP server's SSL certificate is performed. ## Troubleshooting -### Common problems - -**Invalid credentials when logging in** +### Invalid credentials when logging in Make sure the user you are binding with has enough permissions to read the user's tree and traverse it. @@ -219,15 +217,12 @@ Also make sure that the `user_filter` is not blocking otherwise valid users. To make sure that the LDAP settings are correct and GitLab can see your users, execute the following command: -For Omnibus installations: ```bash +# For Omnibus installations sudo gitlab-rake gitlab:ldap:check -``` -For installations from source: - -```bash +# For installations from source sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production ``` diff --git a/doc/integration/saml.md b/doc/integration/saml.md index 16e47bb99b03..148c4ac18867 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -133,9 +133,7 @@ will be returned to GitLab and will be signed in. ## Troubleshooting -### Common problems - -**500 error after login** +### 500 error after login If you see a "500 error" in GitLab when you are redirected back from the SAML sign in page, this likely indicates that GitLab could not get the email address for the SAML user. @@ -143,7 +141,7 @@ this likely indicates that GitLab could not get the email address for the SAML u Make sure the IdP provides a claim containing the user's email address, using claim name `email` or `mail`. -**Redirect back to login screen with no evident error** +### Redirect back to login screen with no evident error If after signing in into your SAML server you are redirected back to the sign in page and no error is displayed, check your `production.log` file. It will most likely contain the @@ -155,13 +153,13 @@ To bypass this you can add `skip_before_action :verify_authenticity_token` to th where it can then be seen in the usual logs, or as a flash message in the login screen. -**Invalid audience** +### Invalid audience This error means that the IdP doesn't recognize GitLab as a valid sender and receiver of SAML requests. Make sure to add the GitLab callback URL to the approved audiences of the IdP server. -**Missing claims** +### Missing claims The IdP server needs to pass certain information in order for GitLab to either create an account, or match the login information to an existing account. `email` @@ -170,7 +168,7 @@ is not providing this information, all SAML requests will fail. Make sure this information is provided. -**Key validation error, Digest mismatch or Fingerprint mismatch** +### Key validation error, Digest mismatch or Fingerprint mismatch These errors all come from a similar place, the SAML certificate. SAML requests need to be validated using a fingerprint, a certificate or a validator. diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index 623ec857cbec..27170c1eb194 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -222,13 +222,13 @@ You can see from the above image that there are four references to GitLab: ## Troubleshooting -**GitLab is unable to comment on a ticket** +### GitLab is unable to comment on a ticket Make sure that the user you set up for GitLab to communicate with JIRA has the correct access permission to post comments on a ticket and to also transition the ticket, if you'd like GitLab to also take care of closing them. -**GitLab is unable to close a ticket** +### GitLab is unable to close a ticket Make sure the the `Transition ID` you set within the JIRA settings matches the one your project needs to close a ticket. -- GitLab From 144cd5802e3dfcc86b3e01252d8e51e1301279ca Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Mon, 7 Mar 2016 21:00:53 -0500 Subject: [PATCH 048/223] Document changes to the initial admin password [ci skip] --- CHANGELOG | 3 +++ doc/install/installation.md | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 47a61317d555..54fccbcaa316 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,9 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.6.0 (unreleased) - Contributions to forked projects are included in calendar - Improve the formatting for the user page bio (Connor Shea) + - Removed the default password from the initial admin account created during + setup. A password can be provided during setup (see installation docs), or + GitLab will ask the user to create a new one upon first visit. - Fix issue when pushing to projects ending in .wiki - Fix avatar stretching by providing a cropping feature (Johann Pardanaud) - Don't load all of GitLab in mail_room diff --git a/doc/install/installation.md b/doc/install/installation.md index c1787a7c6a8c..0fd54be58b09 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -467,12 +467,15 @@ NOTE: Supply `SANITIZE=true` environment variable to `gitlab:check` to omit proj ### Initial Login -Visit YOUR_SERVER in your web browser for your first GitLab login. The setup has created a default admin account for you. You can use it to log in: +Visit YOUR_SERVER in your web browser for your first GitLab login. - root - 5iveL!fe +If you didn't [provide a root password during setup](#initialize-database-and-activate-advanced-features), +you'll be redirected to a password reset screen to provide the password for the +initial administrator account. Enter your desired password and you'll be +redirected back to the login screen. -**Important Note:** On login you'll be prompted to change the password. +The default account's username is **root**. Provide the password you created +earlier and login. After login you can change the username if you wish. **Enjoy!** -- GitLab From 22f6363f743c73d4ea1f1790b68ff87d5b90622c Mon Sep 17 00:00:00 2001 From: Pascal Bach <pascal.bach@siemens.com> Date: Wed, 24 Feb 2016 15:48:56 +0100 Subject: [PATCH 049/223] Support YAML alias/anchor usage in .gitlab-ci.yml This allows to reuse one job as a template for another one: ``` job1: &JOBTMPL script: execute-script-for-job job2: *JOBTMPL ``` This also helps to solve some of the issues in #342 Signed-off-by: Pascal Bach <pascal.bach@siemens.com> Signed-off-by: Fabio Huser <fabio.huser@siemens.com> --- lib/ci/gitlab_ci_yaml_processor.rb | 2 +- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 39 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 1a3f662811a0..28e074cd2893 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -10,7 +10,7 @@ class ValidationError < StandardError;end attr_reader :before_script, :image, :services, :variables, :path, :cache def initialize(config, path = nil) - @config = YAML.safe_load(config, [Symbol]) + @config = YAML.safe_load(config, [Symbol], [], true) @path = path unless @config.is_a? Hash diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index f3394910c5b3..1e98280d045c 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -427,6 +427,45 @@ module Ci end end + describe "YAML Alias/Anchor" do + it "is correctly supported for jobs" do + config = <<EOT +job1: &JOBTMPL + script: execute-script-for-job + +job2: *JOBTMPL +EOT + + config_processor = GitlabCiYamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(2) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + except: nil, + stage: "test", + stage_idx: 1, + name: :job1, + only: nil, + commands: "\nexecute-script-for-job", + tag_list: [], + options: {}, + when: "on_success", + allow_failure: false + }) + expect(config_processor.builds_for_stage_and_ref("test", "master").second).to eq({ + except: nil, + stage: "test", + stage_idx: 1, + name: :job2, + only: nil, + commands: "\nexecute-script-for-job", + tag_list: [], + options: {}, + when: "on_success", + allow_failure: false + }) + end + end + describe "Error handling" do it "fails to parse YAML" do expect{GitlabCiYamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError) -- GitLab From 919d5771366508be46a8d4e2265aabc680b71924 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 8 Mar 2016 08:41:01 +0000 Subject: [PATCH 050/223] Snippet visibility badge spacing Closes #14074 --- app/assets/stylesheets/pages/snippets.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index 0161642d8713..7d414ae003d2 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -26,5 +26,5 @@ margin-right: 10px; font-size: $gl-font-size; border: 1px solid; - line-height: 40px; + line-height: 32px; } -- GitLab From 80c49e387f09b9371f382f7e480deebc8db3e378 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 8 Mar 2016 11:18:07 +0100 Subject: [PATCH 051/223] Updated gitlab_git to 9.0.1 This includes gitlab-org/gitlab_git!69 and will hopefully solve gitlab-org/gitlab-ce#13808. --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index cc5e16c37d31..4f6eea577fdf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -358,7 +358,7 @@ GEM posix-spawn (~> 0.3) gitlab_emoji (0.3.1) gemojione (~> 2.2, >= 2.2.1) - gitlab_git (9.0.0) + gitlab_git (9.0.1) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) -- GitLab From 9b20cee34b974f615a932ef657ac196c78f17520 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Fri, 4 Mar 2016 13:08:24 +0100 Subject: [PATCH 052/223] Added basic SQL guidelines [ci skip] --- doc/development/README.md | 1 + doc/development/sql.md | 219 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 doc/development/sql.md diff --git a/doc/development/README.md b/doc/development/README.md index b9a0d81e5ba9..f5c3107ff441 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -9,4 +9,5 @@ - [Rake tasks](rake_tasks.md) for development - [Shell commands](shell_commands.md) in the GitLab codebase - [Sidekiq debugging](sidekiq_debugging.md) +- [SQL guidelines](sql.md) for SQL guidelines - [UI guide](ui_guide.md) for building GitLab with existing css styles and elements diff --git a/doc/development/sql.md b/doc/development/sql.md new file mode 100644 index 000000000000..23fd7604957c --- /dev/null +++ b/doc/development/sql.md @@ -0,0 +1,219 @@ +# SQL Query Guidelines + +This document describes various guidelines to follow when writing SQL queries, +either using ActiveRecord/Arel or raw SQL queries. + +## Using LIKE Statements + +The most common way to search for data is using the `LIKE` statement. For +example, to get all issues with a title starting with "WIP:" you'd write the +following query: + +```sql +SELECT * +FROM issues +WHERE title LIKE 'WIP:%'; +``` + +On PostgreSQL the `LIKE` statement is case-sensitive. On MySQL this depends on +the case-sensitivity of the collation, which is usually case-insensitive. To +perform a case-insensitive `LIKE` on PostgreSQL you have to use `ILIKE` instead. +This statement in turn isn't supported on MySQL. + +To work around this problem you should write `LIKE` queries using Arel instead +of raw SQL fragments as Arel automatically uses `ILIKE` on PostgreSQL and `LIKE` +on MySQL. This means that instead of this: + +```ruby +Issue.where('title LIKE ?', 'WIP:%') +``` + +You'd write this instead: + +```ruby +Issue.where(Issue.arel_table[:title].matches('WIP:%')) +``` + +Here `matches` generates the correct `LIKE` / `ILIKE` statement depending on the +database being used. + +If you need to chain multiple `OR` conditions you can also do this using Arel: + +```ruby +table = Issue.arel_table + +Issue.where(table[:title].matches('WIP:%').or(table[:foo].matches('WIP:%'))) +``` + +For PostgreSQL this produces: + +```sql +SELECT * +FROM issues +WHERE (title ILIKE 'WIP:%' OR foo ILIKE 'WIP:%') +``` + +In turn for MySQL this produces: + +```sql +SELECT * +FROM issues +WHERE (title LIKE 'WIP:%' OR foo LIKE 'WIP:%') +``` + +## LIKE & Indexes + +Neither PostgreSQL nor MySQL use any indexes when using `LIKE` / `ILIKE` with a +wildcard at the start. For example, this will not use any indexes: + +```sql +SELECT * +FROM issues +WHERE title ILIKE '%WIP:%'; +``` + +Because the value for `ILIKE` starts with a wildcard the database is not able to +use an index as it doesn't know where to start scanning the indexes. + +MySQL provides no known solution to this problem. Luckily PostgreSQL _does_ +provide a solution: trigram GIN indexes. These indexes can be created as +follows: + +```sql +CREATE INDEX [CONCURRENTLY] index_name_here +ON table_name +USING GIN(column_name gin_trgm_ops); +``` + +The key here is the `GIN(column_name gin_trgm_ops)` part. This creates a [GIN +index][gin-index] with the operator class set to `gin_trgm_ops`. These indexes +_can_ be used by `ILIKE` / `LIKE` and can lead to greatly improved performance. +One downside of these indexes is that they can easily get quite large (depending +on the amount of data indexed). + +To keep naming of these indexes consistent please use the following naming +pattern: + + index_TABLE_on_COLUMN_trigram + +For example, a GIN/trigram index for `issues.title` would be called +`index_issues_on_title_trigram`. + +Due to these indexes taking quite some time to be built they should be built +concurrently. This can be done by using `CREATE INDEX CONCURRENTLY` instead of +just `CREATE INDEX`. Concurrent indexes can _not_ be created inside a +transaction. Transactions for migrations can be disabled using the following +pattern: + +```ruby +class MigrationName < ActiveRecord::Migration + disable_ddl_transaction! +end +``` + +For example: + +```ruby +class AddUsersLowerUsernameEmailIndexes < ActiveRecord::Migration + disable_ddl_transaction! + + def up + return unless Gitlab::Database.postgresql? + + execute 'CREATE INDEX CONCURRENTLY index_on_users_lower_username ON users (LOWER(username));' + execute 'CREATE INDEX CONCURRENTLY index_on_users_lower_email ON users (LOWER(email));' + end + + def down + return unless Gitlab::Database.postgresql? + + remove_index :users, :index_on_users_lower_username + remove_index :users, :index_on_users_lower_email + end +end +``` + +## Plucking IDs + +This can't be stressed enough: **never** use ActiveRecord's `pluck` to pluck a +set of values into memory only to use them as an argument for another query. For +example, this will make the database **very** sad: + +```ruby +projects = Project.all.pluck(:id) + +MergeRequest.where(source_project_id: projects) +``` + +Instead you can just use sub-queries which perform far better: + +```ruby +MergeRequest.where(source_project_id: Project.all.select(:id)) +``` + +The _only_ time you should use `pluck` is when you actually need to operate on +the values in Ruby itself (e.g. write them to a file). In almost all other cases +you should ask yourself "Can I not just use a sub-query?". + +## Use UNIONs + +UNIONs aren't very commonly used in most Rails applications but they're very +powerful and useful. In most applications queries tend to use a lot of JOINs to +get related data or data based on certain criteria, but JOIN performance can +quickly deteriorate as the data involved grows. + +For example, if you want to get a list of projects where the name contains a +value _or_ the name of the namespace contains a value most people would write +the following query: + +```sql +SELECT * +FROM projects +JOIN namespaces ON namespaces.id = projects.namespace_id +WHERE projects.name ILIKE '%gitlab%' +OR namespaces.name ILIKE '%gitlab%'; +``` + +Using a large database this query can easily take around 800 milliseconds to +run. Using a UNION we'd write the following instead: + +```sql +SELECT projects.* +FROM projects +WHERE projects.name ILIKE '%gitlab%' + +UNION + +SELECT projects.* +FROM projects +JOIN namespaces ON namespaces.id = projects.namespace_id +WHERE namespaces.name ILIKE '%gitlab%'; +``` + +This query in turn only takes around 15 milliseconds to complete while returning +the exact same records. + +This doesn't mean you should start using UNIONs everywhere, but it's something +to keep in mind when using lots of JOINs in a query and filtering out records +based on the joined data. + +GitLab comes with a `Gitlab::SQL::Union` class that can be used to build a UNION +of multiple `ActiveRecord::Relation` objects. You can use this class as +follows: + +```ruby +union = Gitlab::SQL::Union.new([projects, more_projects, ...]) + +Project.from("(#{union.to_sql}) projects") +``` + +## Ordering by Creation Date + +When ordering records based on the time they were created you can simply order +by the `id` column instead of ordering by `created_at`. Because IDs are always +unique and incremented in the order that rows are created this will produce the +exact same results. This also means there's no need to add an index on +`created_at` to ensure consistent performance as `id` is already indexed by +default. + +[gin-index]: http://www.postgresql.org/docs/current/static/gin.html -- GitLab From d114b73209310a732318d3320470d4342b7f5c9d Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Tue, 8 Mar 2016 15:57:45 +0100 Subject: [PATCH 053/223] add SHA256 to secure_compare --- app/models/project.rb | 4 ++-- app/models/project_services/ci_service.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index c0f2ab91fa41..3451779e18d4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -908,13 +908,13 @@ def any_runners?(&block) end def valid_runners_token? token - self.runners_token && ActiveSupport::SecurityUtils.secure_compare(token, self.runners_token) + self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token) end # TODO (ayufan): For now we use runners_token (backward compatibility) # In 8.4 every build will have its own individual token valid for time of build def valid_build_token? token - self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.secure_compare(token, self.runners_token) + self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token) end def build_coverage_enabled? diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index f328deda3542..d9f0849d1470 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -26,7 +26,7 @@ class CiService < Service default_value_for :category, 'ci' def valid_token?(token) - self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.secure_compare(token, self.token) + self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) end def supported_events -- GitLab From aff88ac760092ece8b2c154ade11ff7c8fdd97a0 Mon Sep 17 00:00:00 2001 From: Yatish Mehta <yatish.mehta@coupa.com> Date: Fri, 4 Mar 2016 14:34:32 -0800 Subject: [PATCH 054/223] Remove instance variable @group multiple assignment --- CHANGELOG | 1 + app/controllers/admin/groups_controller.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 54fccbcaa316..ba95785199ed 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,7 @@ v 8.6.0 (unreleased) - Fix issue when pushing to projects ending in .wiki - Fix avatar stretching by providing a cropping feature (Johann Pardanaud) - Don't load all of GitLab in mail_room + - Memoize @group in Admin::GroupsController (Yatish Mehta) - Indicate how much an MR diverged from the target branch (Pierre de La Morinerie) - Strip leading and trailing spaces in URL validator (evuez) - Add "last_sign_in_at" and "confirmed_at" to GET /users/* API endpoints for admins (evuez) diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 4d3e48f7f817..668396a0f20d 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -55,7 +55,7 @@ def destroy private def group - @group = Group.find_by(path: params[:id]) + @group ||= Group.find_by(path: params[:id]) end def group_params -- GitLab From 71df5b7ba97c7e83708b41a3e961c80aef6711eb Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 8 Mar 2016 17:38:23 +0100 Subject: [PATCH 055/223] Cache & flush tag/branch counts The methods used for this are Repository#tag_count and Repository#branch_count which cache their output in Redis as well as memoizing it in an instance variable. Both methods have a corresponding methods/hooks to flush the caches at the right time. --- app/models/repository.rb | 33 +++++++++++++-- app/services/git_tag_push_service.rb | 2 +- app/views/projects/branches/destroy.js.haml | 2 +- app/views/projects/commits/_head.html.haml | 4 +- spec/models/repository_spec.rb | 47 +++++++++++++++++++-- spec/services/delete_tag_service_spec.rb | 26 ++++++++++++ 6 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 spec/services/delete_tag_service_spec.rb diff --git a/app/models/repository.rb b/app/models/repository.rb index ff48f993d422..d3ca268a659f 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -160,7 +160,7 @@ def rm_branch(user, branch_name) end def rm_tag(tag_name) - expire_tags_cache + before_remove_tag gitlab_shell.rm_tag(path_with_namespace, tag_name) end @@ -183,6 +183,14 @@ def commit_count end end + def branch_count + @branch_count ||= cache.fetch(:branch_count) { raw_repository.branch_count } + end + + def tag_count + @tag_count ||= cache.fetch(:tag_count) { raw_repository.rugged.tags.count } + end + # Return repo size in megabytes # Cached in redis def size @@ -278,6 +286,16 @@ def expire_has_visible_content_cache @has_visible_content = nil end + def expire_branch_count_cache + cache.expire(:branch_count) + @branch_count = nil + end + + def expire_tag_count_cache + cache.expire(:tag_count) + @tag_count = nil + end + def rebuild_cache cache_keys.each do |key| cache.expire(key) @@ -313,9 +331,16 @@ def before_change_head expire_root_ref_cache end - # Runs code before creating a new tag. - def before_create_tag + # Runs code before pushing (= creating or removing) a tag. + def before_push_tag expire_cache + expire_tag_count_cache + end + + # Runs code before removing a tag. + def before_remove_tag + expire_tags_cache + expire_tag_count_cache end # Runs code after a repository has been forked/imported. @@ -331,11 +356,13 @@ def after_push_commit(branch_name) # Runs code after a new branch has been created. def after_create_branch expire_has_visible_content_cache + expire_branch_count_cache end # Runs code after an existing branch has been removed. def after_remove_branch expire_has_visible_content_cache + expire_branch_count_cache end def method_missing(m, *args, &block) diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index a62c5fc4fc41..c88c76728051 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -2,7 +2,7 @@ class GitTagPushService attr_accessor :project, :user, :push_data def execute(project, user, oldrev, newrev, ref) - project.repository.before_create_tag + project.repository.before_push_tag @project, @user = project, user @push_data = build_push_data(oldrev, newrev, ref) diff --git a/app/views/projects/branches/destroy.js.haml b/app/views/projects/branches/destroy.js.haml index 882a4d0c5e26..a21ddaf49302 100644 --- a/app/views/projects/branches/destroy.js.haml +++ b/app/views/projects/branches/destroy.js.haml @@ -1 +1 @@ -$('.js-totalbranch-count').html("#{@repository.branches.size}") +$('.js-totalbranch-count').html("#{@repository.branch_count}") diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index 498c5e05b321..7a5b0d993dbd 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -15,9 +15,9 @@ = nav_link(html_options: {class: branches_tab_class}) do = link_to namespace_project_branches_path(@project.namespace, @project) do Branches - %span.badge.js-totalbranch-count= @repository.branches.size + %span.badge.js-totalbranch-count= @repository.branch_count = nav_link(controller: [:tags, :releases]) do = link_to namespace_project_tags_path(@project.namespace, @project) do Tags - %span.badge.js-totaltags-count= @repository.tags.length + %span.badge.js-totaltags-count= @repository.tag_count diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 150422ac3492..7af807046905 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -405,7 +405,7 @@ end end - describe '#expire_branch_ache' do + describe '#expire_branch_cache' do # This method is private but we need it for testing purposes. Sadly there's # no other proper way of testing caching operations. let(:cache) { repository.send(:cache) } @@ -556,11 +556,12 @@ end end - describe '#before_create_tag' do + describe '#before_push_tag' do it 'flushes the cache' do expect(repository).to receive(:expire_cache) + expect(repository).to receive(:expire_tag_count_cache) - repository.before_create_tag + repository.before_push_tag end end @@ -607,4 +608,44 @@ expect(repository.main_language).to be_nil end end + + describe '#before_remove_tag' do + it 'flushes the tag cache' do + expect(repository).to receive(:expire_tag_count_cache) + + repository.before_remove_tag + end + end + + describe '#branch_count' do + it 'returns the number of branches' do + expect(repository.branch_count).to be_an_instance_of(Fixnum) + end + end + + describe '#tag_count' do + it 'returns the number of tags' do + expect(repository.tag_count).to be_an_instance_of(Fixnum) + end + end + + describe '#expire_branch_count_cache' do + let(:cache) { repository.send(:cache) } + + it 'expires the cache' do + expect(cache).to receive(:expire).with(:branch_count) + + repository.expire_branch_count_cache + end + end + + describe '#expire_tag_count_cache' do + let(:cache) { repository.send(:cache) } + + it 'expires the cache' do + expect(cache).to receive(:expire).with(:tag_count) + + repository.expire_tag_count_cache + end + end end diff --git a/spec/services/delete_tag_service_spec.rb b/spec/services/delete_tag_service_spec.rb new file mode 100644 index 000000000000..5b7ba5218123 --- /dev/null +++ b/spec/services/delete_tag_service_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe DeleteTagService, services: true do + let(:project) { create(:project) } + let(:repository) { project.repository } + let(:user) { create(:user) } + let(:service) { described_class.new(project, user) } + + let(:tag) { double(:tag, name: '8.5', target: 'abc123') } + + describe '#execute' do + before do + allow(repository).to receive(:find_tag).and_return(tag) + end + + it 'removes the tag' do + expect_any_instance_of(Gitlab::Shell).to receive(:rm_tag). + and_return(true) + + expect(repository).to receive(:before_remove_tag) + expect(service).to receive(:success) + + service.execute('8.5') + end + end +end -- GitLab From 440a8efeceef213d7aa95297960502513a9f57dd Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 8 Mar 2016 17:53:00 +0100 Subject: [PATCH 056/223] Call the right hooks when removing branches This ensures that Repository#rm_branch calls before_remove_branch/after_remove_branch instead of just 1 random cache expiration method. --- app/models/repository.rb | 10 ++++++++-- spec/models/repository_spec.rb | 11 +++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index d3ca268a659f..c3ae461a0168 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -144,7 +144,7 @@ def add_tag(tag_name, ref, message = nil) end def rm_branch(user, branch_name) - expire_branches_cache + before_remove_branch branch = find_branch(branch_name) oldrev = branch.try(:target) @@ -155,7 +155,7 @@ def rm_branch(user, branch_name) rugged.branches.delete(branch_name) end - expire_branches_cache + after_remove_branch true end @@ -359,10 +359,16 @@ def after_create_branch expire_branch_count_cache end + # Runs code before removing an existing branch. + def before_remove_branch + expire_branches_cache + end + # Runs code after an existing branch has been removed. def after_remove_branch expire_has_visible_content_cache expire_branch_count_cache + expire_branches_cache end def method_missing(m, *args, &block) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 7af807046905..b2b4d38756f1 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -648,4 +648,15 @@ repository.expire_tag_count_cache end end + + describe '#rm_branch' do + let(:user) { create(:user) } + + it 'removes a branch' do + expect(repository).to receive(:before_remove_branch) + expect(repository).to receive(:after_remove_branch) + + repository.rm_branch(user, 'feature') + end + end end -- GitLab From c387149c3d50bbeaf4164560f822fa38fabceb66 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 8 Mar 2016 17:53:23 +0100 Subject: [PATCH 057/223] Added specs for Repository#rm_tag --- spec/models/repository_spec.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index b2b4d38756f1..40cbb3d635e5 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -659,4 +659,15 @@ repository.rm_branch(user, 'feature') end end + + describe '#rm_tag' do + it 'removes a tag' do + expect(repository).to receive(:before_remove_tag) + + expect_any_instance_of(Gitlab::Shell).to receive(:rm_tag). + with(repository.path_with_namespace, '8.5') + + repository.rm_tag('8.5') + end + end end -- GitLab From c2ddea98a91fb2d5f0d3991a33101164ae72a22b Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 8 Mar 2016 18:01:16 +0100 Subject: [PATCH 058/223] Call the right hooks in Repository#add_tag This ensures Repository#add_tag calls Repository#before_push_tag instead of just 1 random cache expiration method. --- app/models/repository.rb | 3 ++- spec/models/repository_spec.rb | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index c3ae461a0168..c0730b343162 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -138,7 +138,7 @@ def add_branch(user, branch_name, target) end def add_tag(tag_name, ref, message = nil) - expire_tags_cache + before_push_tag gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message) end @@ -334,6 +334,7 @@ def before_change_head # Runs code before pushing (= creating or removing) a tag. def before_push_tag expire_cache + expire_tags_cache expire_tag_count_cache end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 40cbb3d635e5..97778c354a45 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -649,6 +649,17 @@ end end + describe '#add_tag' do + it 'adds a tag' do + expect(repository).to receive(:before_push_tag) + + expect_any_instance_of(Gitlab::Shell).to receive(:add_tag). + with(repository.path_with_namespace, '8.5', 'master', 'foo') + + repository.add_tag('8.5', 'master', 'foo') + end + end + describe '#rm_branch' do let(:user) { create(:user) } -- GitLab From 464d0206fb0ce6bc6d1f027ead9d28114afe70ab Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 8 Mar 2016 18:04:00 +0100 Subject: [PATCH 059/223] Call after_create_branch in Repository#add_branch This ensures the right caches are flushed when adding a branch via the UI, instead of only flushing this one random cache. --- app/models/repository.rb | 3 ++- spec/models/repository_spec.rb | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index c0730b343162..6441cd87e877 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -133,7 +133,7 @@ def add_branch(user, branch_name, target) rugged.branches.create(branch_name, target) end - expire_branches_cache + after_create_branch find_branch(branch_name) end @@ -356,6 +356,7 @@ def after_push_commit(branch_name) # Runs code after a new branch has been created. def after_create_branch + expire_branches_cache expire_has_visible_content_cache expire_branch_count_cache end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 97778c354a45..34866be33955 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -148,6 +148,12 @@ expect(branch.name).to eq('new_feature') end + + it 'calls the after_create_branch hook' do + expect(repository).to receive(:after_create_branch) + + repository.add_branch(user, 'new_feature', 'master') + end end context 'when pre hooks failed' do -- GitLab From 697331f5f9f75da164bd6263e10b3a6476d4febc Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Tue, 8 Mar 2016 16:39:53 -0500 Subject: [PATCH 060/223] Simplify the various Snippet factories --- spec/factories/personal_snippets.rb | 11 ----------- spec/factories/project_snippets.rb | 6 +----- spec/factories/snippets.rb | 12 ++++++++++++ 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/spec/factories/personal_snippets.rb b/spec/factories/personal_snippets.rb index b493a6968ff9..0f13b2c10209 100644 --- a/spec/factories/personal_snippets.rb +++ b/spec/factories/personal_snippets.rb @@ -1,15 +1,4 @@ FactoryGirl.define do factory :personal_snippet, parent: :snippet, class: :PersonalSnippet do - trait :public do - visibility_level PersonalSnippet::PUBLIC - end - - trait :internal do - visibility_level PersonalSnippet::INTERNAL - end - - trait :private do - visibility_level PersonalSnippet::PRIVATE - end end end diff --git a/spec/factories/project_snippets.rb b/spec/factories/project_snippets.rb index 154442bd3dbc..d681a2c8483d 100644 --- a/spec/factories/project_snippets.rb +++ b/spec/factories/project_snippets.rb @@ -1,9 +1,5 @@ FactoryGirl.define do - factory :project_snippet do + factory :project_snippet, parent: :snippet, class: :ProjectSnippet do project - author - title - content - file_name end end diff --git a/spec/factories/snippets.rb b/spec/factories/snippets.rb index b9127b3d75e9..365f12a0c952 100644 --- a/spec/factories/snippets.rb +++ b/spec/factories/snippets.rb @@ -12,5 +12,17 @@ title content file_name + + trait :public do + visibility_level Snippet::PUBLIC + end + + trait :internal do + visibility_level Snippet::INTERNAL + end + + trait :private do + visibility_level Snippet::PRIVATE + end end end -- GitLab From 5be89f9fbc1d0da72994ecb536f51e88b60d8c4f Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Tue, 8 Mar 2016 16:40:45 -0500 Subject: [PATCH 061/223] Make better use of the `visibility_level` factory traits --- .../projects/forks_controller_spec.rb | 2 +- spec/finders/snippets_finder_spec.rb | 25 +++++++++---------- spec/helpers/visibility_level_helper_spec.rb | 6 ++--- spec/models/project_spec.rb | 2 +- spec/services/projects/update_service_spec.rb | 4 +-- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb index 883bbaedd4e2..70ed8f3a62e1 100644 --- a/spec/controllers/projects/forks_controller_spec.rb +++ b/spec/controllers/projects/forks_controller_spec.rb @@ -2,7 +2,7 @@ describe Projects::ForksController do let(:user) { create(:user) } - let(:project) { create(:project, visibility_level: Project::PUBLIC) } + let(:project) { create(:project, :public) } let(:forked_project) { Projects::ForkService.new(project, user).execute } let(:group) { create(:group, owner: forked_project.creator) } diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index 1b4ffc2d7176..7fdc5e5d7aab 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -5,15 +5,14 @@ let(:user1) { create :user } let(:group) { create :group } - let(:project1) { create(:empty_project, :public, group: group) } - let(:project2) { create(:empty_project, :private, group: group) } - + let(:project1) { create(:empty_project, :public, group: group) } + let(:project2) { create(:empty_project, :private, group: group) } context ':all filter' do before do - @snippet1 = create(:personal_snippet, visibility_level: Snippet::PRIVATE) - @snippet2 = create(:personal_snippet, visibility_level: Snippet::INTERNAL) - @snippet3 = create(:personal_snippet, visibility_level: Snippet::PUBLIC) + @snippet1 = create(:personal_snippet, :private) + @snippet2 = create(:personal_snippet, :internal) + @snippet3 = create(:personal_snippet, :public) end it "returns all private and internal snippets" do @@ -31,9 +30,9 @@ context ':by_user filter' do before do - @snippet1 = create(:personal_snippet, visibility_level: Snippet::PRIVATE, author: user) - @snippet2 = create(:personal_snippet, visibility_level: Snippet::INTERNAL, author: user) - @snippet3 = create(:personal_snippet, visibility_level: Snippet::PUBLIC, author: user) + @snippet1 = create(:personal_snippet, :private, author: user) + @snippet2 = create(:personal_snippet, :internal, author: user) + @snippet3 = create(:personal_snippet, :public, author: user) end it "returns all public and internal snippets" do @@ -75,9 +74,9 @@ context 'by_project filter' do before do - @snippet1 = create(:project_snippet, visibility_level: Snippet::PRIVATE, project: project1) - @snippet2 = create(:project_snippet, visibility_level: Snippet::INTERNAL, project: project1) - @snippet3 = create(:project_snippet, visibility_level: Snippet::PUBLIC, project: project1) + @snippet1 = create(:project_snippet, :private, project: project1) + @snippet2 = create(:project_snippet, :internal, project: project1) + @snippet3 = create(:project_snippet, :public, project: project1) end it "returns public snippets for unauthorized user" do @@ -93,7 +92,7 @@ end it "returns all snippets for project members" do - project1.team << [user, :developer] + project1.team << [user, :developer] snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) expect(snippets).to include(@snippet1, @snippet2, @snippet3) end diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb index aafc24397a9a..cd7596a763d9 100644 --- a/spec/helpers/visibility_level_helper_spec.rb +++ b/spec/helpers/visibility_level_helper_spec.rb @@ -58,7 +58,7 @@ describe "skip_level?" do describe "forks" do - let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } + let(:project) { create(:project, :internal) } let(:fork_project) { create(:forked_project_with_submodules) } before do @@ -74,7 +74,7 @@ end describe "non-forked project" do - let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } + let(:project) { create(:project, :internal) } it "skips levels" do expect(skip_level?(project, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey @@ -84,7 +84,7 @@ end describe "Snippet" do - let(:snippet) { create(:snippet, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } + let(:snippet) { create(:snippet, :internal) } it "skips levels" do expect(skip_level?(snippet, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f9842d23afa6..c458d9c9b1be 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -561,7 +561,7 @@ end describe '#visibility_level_allowed?' do - let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL } + let(:project) { create(:project, :internal) } context 'when checking on non-forked project' do it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy } diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 3c06a8901634..e8b9e6b92384 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -102,8 +102,8 @@ describe :visibility_level do let(:user) { create :user, admin: true } - let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL } - let(:forked_project) { create :forked_project_with_submodules, visibility_level: Gitlab::VisibilityLevel::INTERNAL } + let(:project) { create(:project, :internal) } + let(:forked_project) { create(:forked_project_with_submodules, :internal) } let(:opts) { {} } before do -- GitLab From 22d3997d7a907b31349658c1c2fca868173f3879 Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Mon, 15 Feb 2016 21:17:20 -0500 Subject: [PATCH 062/223] Use a custom Devise failure app to handle unauthenticated .zip requests Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/12944 --- config/initializers/devise.rb | 10 ++--- lib/gitlab/devise_failure.rb | 23 ++++++++++++ .../projects/repositories_controller_spec.rb | 37 ++++++++++++------- 3 files changed, 52 insertions(+), 18 deletions(-) create mode 100644 lib/gitlab/devise_failure.rb diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index d82cfb3ec0c7..31dceaebcadb 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -203,11 +203,11 @@ # If you want to use other strategies, that are not supported by Devise, or # change the failure app, you can configure them inside the config.warden block. # - # config.warden do |manager| - # manager.failure_app = AnotherApp - # manager.intercept_401 = false - # manager.default_strategies(scope: :user).unshift :some_external_strategy - # end + config.warden do |manager| + manager.failure_app = Gitlab::DeviseFailure + # manager.intercept_401 = false + # manager.default_strategies(scope: :user).unshift :some_external_strategy + end if Gitlab::LDAP::Config.enabled? Gitlab.config.ldap.servers.values.each do |server| diff --git a/lib/gitlab/devise_failure.rb b/lib/gitlab/devise_failure.rb new file mode 100644 index 000000000000..a78fde9d7829 --- /dev/null +++ b/lib/gitlab/devise_failure.rb @@ -0,0 +1,23 @@ +module Gitlab + class DeviseFailure < Devise::FailureApp + protected + + # Override `Devise::FailureApp#request_format` to handle a special case + # + # This tells Devise to handle an unauthenticated `.zip` request as an HTML + # request (i.e., redirect to sign in). + # + # Otherwise, Devise would respond with a 401 Unauthorized with + # `Content-Type: application/zip` and a response body in plaintext, and the + # browser would freak out. + # + # See https://gitlab.com/gitlab-org/gitlab-ce/issues/12944 + def request_format + if request.format == :zip + Mime::Type.lookup_by_extension(:html).ref + else + super + end + end + end +end diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb index 09ec4f18f9d2..0ddbec9eac21 100644 --- a/spec/controllers/projects/repositories_controller_spec.rb +++ b/spec/controllers/projects/repositories_controller_spec.rb @@ -2,30 +2,41 @@ describe Projects::RepositoriesController do let(:project) { create(:project) } - let(:user) { create(:user) } describe "GET archive" do - before do - sign_in(user) - project.team << [user, :developer] - end - - it "uses Gitlab::Workhorse" do - expect(Gitlab::Workhorse).to receive(:send_git_archive).with(project, "master", "zip") + context 'as a guest' do + it 'responds with redirect in correct format' do + get :archive, namespace_id: project.namespace.path, project_id: project.path, format: "zip" - get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" + expect(response.content_type).to start_with 'text/html' + expect(response).to be_redirect + end end - context "when the service raises an error" do + context 'as a user' do + let(:user) { create(:user) } before do - allow(Gitlab::Workhorse).to receive(:send_git_archive).and_raise("Archive failed") + project.team << [user, :developer] + sign_in(user) end + it "uses Gitlab::Workhorse" do + expect(Gitlab::Workhorse).to receive(:send_git_archive).with(project, "master", "zip") - it "renders Not Found" do get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" + end + + context "when the service raises an error" do + + before do + allow(Gitlab::Workhorse).to receive(:send_git_archive).and_raise("Archive failed") + end + + it "renders Not Found" do + get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" - expect(response.status).to eq(404) + expect(response.status).to eq(404) + end end end end -- GitLab From cf3a6a020a0a9e0d979515f467f38fa5cd1414fc Mon Sep 17 00:00:00 2001 From: Jonas Friedmann <j@frd.mn> Date: Wed, 9 Mar 2016 08:26:39 +0100 Subject: [PATCH 063/223] Fix URL to upgrader guide --- doc/update/upgrader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/update/upgrader.md b/doc/update/upgrader.md index fd0327686b17..5fa39ef1b0a7 100644 --- a/doc/update/upgrader.md +++ b/doc/update/upgrader.md @@ -4,7 +4,7 @@ Although deprecated, if someone wants to make this script into a gem or otherwise improve it merge requests are welcome. -*Make sure you view this [upgrade guide from the 'master' branch](../../../master/doc/update/upgrader.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide from the 'master' branch](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/upgrader.md) for the most up to date instructions.* GitLab Upgrader - a ruby script that allows you easily upgrade GitLab to latest minor version. -- GitLab From b7ed4a320b5ce202fdad2651d7d885631c5205cd Mon Sep 17 00:00:00 2001 From: Florent Baldino <fbaldino@onisep.fr> Date: Wed, 9 Mar 2016 09:30:50 +0100 Subject: [PATCH 064/223] Cache only last response in markdown preview --- app/assets/javascripts/markdown_preview.js.coffee | 4 ++-- app/views/help/_shortcuts.html.haml | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/markdown_preview.js.coffee b/app/assets/javascripts/markdown_preview.js.coffee index 3aa61de2f5a5..2a0b94794450 100644 --- a/app/assets/javascripts/markdown_preview.js.coffee +++ b/app/assets/javascripts/markdown_preview.js.coffee @@ -25,7 +25,7 @@ class @MarkdownPreview renderMarkdown: (text, success) -> return unless window.markdown_preview_path - return success(@ajaxCache[text]) if @ajaxCache[text] + return success(@ajaxCache.response) if text == @ajaxCache.text $.ajax type: 'POST' @@ -33,7 +33,7 @@ class @MarkdownPreview data: { text: text } dataType: 'json' success: (response) => - @ajaxCache[text] = response + @ajaxCache = text: text, response: response success(response) hideReferencedUsers: (form) -> diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index e6e3bfbea717..9fc2372762b3 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -24,7 +24,11 @@ %td Show this dialog %tr %td.shortcut - .key ctrl shift p + - if browser.mac? + .key ctrl ⌘ p + - else + .key ctrl shift p + %td Toggle Markdown preview %tbody %tr -- GitLab From 37a07d768ca972427123a977b1032a5cdf9efa70 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Wed, 9 Mar 2016 12:00:17 +0100 Subject: [PATCH 065/223] Prevent performance issues --- app/views/projects/commits/_commit_list.html.haml | 2 +- app/views/projects/commits/_commits.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml index c0988eca6de2..bac9e244d363 100644 --- a/app/views/projects/commits/_commit_list.html.haml +++ b/app/views/projects/commits/_commit_list.html.haml @@ -9,6 +9,6 @@ - commits.each do |commit| = render "projects/commits/inline_commit", commit: commit, project: @project %li.warning-row.unstyled - #{number_with_delimiter(hidden)} additional commits have been omitted. + #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues. - else %ul.well-list= render commits, project: @project diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 787fe443ac8a..a7e3c2478c24 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -18,4 +18,4 @@ - if hidden > 0 .alert.alert-warning - #{number_with_delimiter(hidden)} additional commits have been omitted. + #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues. -- GitLab From dfe09d57aa99530f8f82ec992ba938089c250032 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Wed, 9 Mar 2016 10:41:41 +0100 Subject: [PATCH 066/223] Added main_language to the schema This wasn't included in gitlab-org/gitlab-ce!3000. --- db/schema.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/db/schema.rb b/db/schema.rb index 2d6b9b5a4cc6..4f56f3970db3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -697,6 +697,7 @@ t.integer "build_timeout", default: 3600, null: false t.boolean "pending_delete", default: false t.boolean "public_builds", default: true, null: false + t.string "main_language" end add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree -- GitLab From 082c04b3ca3a4c5218874065a1f0943922f6a3dd Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 12 Nov 2015 10:52:20 +0100 Subject: [PATCH 067/223] Retry failed tests --- .gitlab-ci.yml | 2 ++ Gemfile | 1 + Gemfile.lock | 3 +++ spec/spec_helper.rb | 4 ++++ 4 files changed, 10 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c477721f9da6..f77b5bdc955d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,6 +12,8 @@ cache: variables: MYSQL_ALLOW_EMPTY_PASSWORD: "1" + # retry tests only in CI environment + RSPEC_RETRY_RETRY_COUNT: "3" before_script: - source ./scripts/prepare_build.sh diff --git a/Gemfile b/Gemfile index 62b2e55b5374..064778105eba 100644 --- a/Gemfile +++ b/Gemfile @@ -263,6 +263,7 @@ group :development, :test do gem 'database_cleaner', '~> 1.4.0' gem 'factory_girl_rails', '~> 4.6.0' gem 'rspec-rails', '~> 3.3.0' + gem 'rspec-retry' gem 'spinach-rails', '~> 0.2.1' # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) diff --git a/Gemfile.lock b/Gemfile.lock index 4f6eea577fdf..156aba562daa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -679,6 +679,8 @@ GEM rspec-expectations (~> 3.3.0) rspec-mocks (~> 3.3.0) rspec-support (~> 3.3.0) + rspec-retry (0.4.5) + rspec-core rspec-support (3.3.0) rubocop (0.35.1) astrolabe (~> 1.3) @@ -999,6 +1001,7 @@ DEPENDENCIES rouge (~> 1.10.1) rqrcode-rails3 (~> 0.1.7) rspec-rails (~> 3.3.0) + rspec-retry rubocop (~> 0.35.0) ruby-fogbugz (~> 0.2.1) sanitize (~> 2.0) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8f381f46e577..159fb9641718 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -15,6 +15,7 @@ require 'shoulda/matchers' require 'sidekiq/testing/inline' require 'benchmark/ips' +require 'rspec/retry' # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. @@ -25,6 +26,9 @@ config.use_instantiated_fixtures = false config.mock_with :rspec + config.verbose_retry = true + config.display_try_failure_messages = true + config.include Devise::TestHelpers, type: :controller config.include LoginHelpers, type: :feature config.include LoginHelpers, type: :request -- GitLab From 25822dfd7bab6fe07b0b308edc139d00d9ba7bac Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 9 Mar 2016 14:12:08 +0100 Subject: [PATCH 068/223] Retry spinach tests --- Gemfile | 1 + Gemfile.lock | 3 ++ features/support/env.rb | 1 + features/support/rerun.rb | 14 +++++++++ lib/tasks/spinach.rake | 62 +++++++++++++++++++++------------------ 5 files changed, 53 insertions(+), 28 deletions(-) create mode 100644 features/support/rerun.rb diff --git a/Gemfile b/Gemfile index 064778105eba..7e70761a77a1 100644 --- a/Gemfile +++ b/Gemfile @@ -265,6 +265,7 @@ group :development, :test do gem 'rspec-rails', '~> 3.3.0' gem 'rspec-retry' gem 'spinach-rails', '~> 0.2.1' + gem 'spinach-rerun-reporter', '~> 0.0.2' # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) gem 'minitest', '~> 5.7.0' diff --git a/Gemfile.lock b/Gemfile.lock index 156aba562daa..3777528ea4b6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -766,6 +766,8 @@ GEM capybara (>= 2.0.0) railties (>= 3) spinach (>= 0.4) + spinach-rerun-reporter (0.0.2) + spinach (~> 0.8) spring (1.6.4) spring-commands-rspec (1.0.4) spring (>= 0.9.1) @@ -1020,6 +1022,7 @@ DEPENDENCIES six (~> 0.2.0) slack-notifier (~> 1.2.0) spinach-rails (~> 0.2.1) + spinach-rerun-reporter (~> 0.0.2) spring (~> 1.6.4) spring-commands-rspec (~> 1.0.4) spring-commands-spinach (~> 1.0.0) diff --git a/features/support/env.rb b/features/support/env.rb index 62c80b9c9488..357d164d87f7 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -14,6 +14,7 @@ require_relative 'capybara' require_relative 'db_cleaner' +require_relative 'rerun' %w(select2_helper test_env repo_helpers).each do |f| require Rails.root.join('spec', 'support', f) diff --git a/features/support/rerun.rb b/features/support/rerun.rb new file mode 100644 index 000000000000..8b176c5be895 --- /dev/null +++ b/features/support/rerun.rb @@ -0,0 +1,14 @@ +# The spinach-rerun-reporter doesn't define the on_undefined_step +# See it here: https://github.com/javierav/spinach-rerun-reporter/blob/master/lib/spinach/reporter/rerun.rb +module Spinach + class Reporter + class Rerun + def on_undefined_step(step_data, failure, step_definitions = nil) + super step_data, failure, step_definitions + + # save feature file and scenario line + @rerun << "#{current_feature.filename}:#{current_scenario.line}" + end + end + end +end diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake index 3acfc6e20750..01d23b89bb7e 100644 --- a/lib/tasks/spinach.rake +++ b/lib/tasks/spinach.rake @@ -4,53 +4,59 @@ namespace :spinach do namespace :project do desc "GitLab | Spinach | Run project commits, issues and merge requests spinach features" task :half do - cmds = [ - %W(rake gitlab:setup), - %W(spinach --tags @project_commits,@project_issues,@project_merge_requests), - ] - run_commands(cmds) + run_spinach_tests('@project_commits,@project_issues,@project_merge_requests') end desc "GitLab | Spinach | Run remaining project spinach features" task :rest do - cmds = [ - %W(rake gitlab:setup), - %W(spinach --tags ~@admin,~@dashboard,~@profile,~@public,~@snippets,~@project_commits,~@project_issues,~@project_merge_requests), - ] - run_commands(cmds) + run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets,~@project_commits,~@project_issues,~@project_merge_requests') end end desc "GitLab | Spinach | Run project spinach features" task :project do - cmds = [ - %W(rake gitlab:setup), - %W(spinach --tags ~@admin,~@dashboard,~@profile,~@public,~@snippets), - ] - run_commands(cmds) + run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets') end desc "GitLab | Spinach | Run other spinach features" task :other do - cmds = [ - %W(rake gitlab:setup), - %W(spinach --tags @admin,@dashboard,@profile,@public,@snippets), - ] - run_commands(cmds) + run_spinach_tests('@admin,@dashboard,@profile,@public,@snippets') + end + + desc "GitLab | Spinach | Run other spinach features" + task :builds do + run_spinach_tests('@builds') end end desc "GitLab | Run spinach" task :spinach do - cmds = [ - %W(rake gitlab:setup), - %W(spinach), - ] - run_commands(cmds) + run_spinach_tests(nil) +end + +def run_command(cmd) + system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) end -def run_commands(cmds) - cmds.each do |cmd| - system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!") +def run_spinach_command(args) + run_command(%w(spinach -r rerun) + args) +end + +def run_spinach_tests(tags) + #run_command(%w(rake gitlab:setup)) or raise('gitlab:setup failed!') + + success = run_spinach_command(%W(--tags #{tags})) + 3.times do |_| + break if success + break unless File.exists?('tmp/spinach-rerun.txt') + + tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp) + puts '' + puts "Spinach tests for #{tags}: Retrying tests... #{tests}".red + puts '' + sleep(3) + success = run_spinach_command(tests) end + + raise("spinach tests for #{tags} failed!") unless success end -- GitLab From 37ec743387825f5187d35ce3db229f6925c2a52a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 9 Mar 2016 14:12:29 +0100 Subject: [PATCH 069/223] Retry apt-get installation and bundle install --- .gitlab-ci.yml | 2 +- scripts/prepare_build.sh | 26 +++++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f77b5bdc955d..ffefeb6dfd83 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -23,7 +23,7 @@ before_script: - cp config/gitlab.yml.example config/gitlab.yml - touch log/application.log - touch log/test.log - - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" + - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" - RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate stages: diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index b6f076a90c3b..bce9bce99c38 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -2,15 +2,27 @@ if [ -f /.dockerinit ]; then mkdir -p vendor - if [ ! -e vendor/phantomjs_1.9.8-0jessie_amd64.deb ]; then + + # Install phantomjs package + pushd vendor + if [ ! -e phantomjs_1.9.8-0jessie_amd64.deb ]; then wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb - mv phantomjs_1.9.8-0jessie_amd64.deb vendor/ fi - dpkg -i vendor/phantomjs_1.9.8-0jessie_amd64.deb + dpkg -i phantomjs_1.9.8-0jessie_amd64.deb + popd + + # Try to install packages + for i in $(seq 1 3); do + apt-get update -yqqq || true - apt-get update -qq - apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \ - libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip + if apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \ + libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip; then + break + fi + + sleep 3s + echo "Retrying package installation..." + fi cp config/database.yml.mysql config/database.yml sed -i 's/username:.*/username: root/g' config/database.yml @@ -20,7 +32,7 @@ if [ -f /.dockerinit ]; then cp config/resque.yml.example config/resque.yml sed -i 's/localhost/redis/g' config/resque.yml - export FLAGS=(--path vendor) + export FLAGS=(--path vendor --retry 3) else export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin cp config/database.yml.mysql config/database.yml -- GitLab From 0491b7806bb06df63a6cf31519ac5bfe83c87fc8 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 9 Mar 2016 14:26:49 +0100 Subject: [PATCH 070/223] Fix script error --- scripts/prepare_build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index bce9bce99c38..82de51a9a2e9 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -22,7 +22,7 @@ if [ -f /.dockerinit ]; then sleep 3s echo "Retrying package installation..." - fi + done cp config/database.yml.mysql config/database.yml sed -i 's/username:.*/username: root/g' config/database.yml -- GitLab From 027d829efa78cb1871388c5fc289ac68e4db7dff Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Wed, 9 Mar 2016 11:06:58 -0300 Subject: [PATCH 071/223] Destroy all related todos when removing a project --- app/models/project.rb | 1 + spec/models/project_spec.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/app/models/project.rb b/app/models/project.rb index 3451779e18d4..43e4c574b779 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -151,6 +151,7 @@ def update_forks_visibility_level has_many :releases, dependent: :destroy has_many :lfs_objects_projects, dependent: :destroy has_many :lfs_objects, through: :lfs_objects_projects + has_many :todos, dependent: :destroy has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index c458d9c9b1be..2fa38a5d3d38 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -68,6 +68,7 @@ it { is_expected.to have_many(:runners) } it { is_expected.to have_many(:variables) } it { is_expected.to have_many(:triggers) } + it { is_expected.to have_many(:todos).dependent(:destroy) } end describe 'modules' do -- GitLab From f6b513fe90700188f8d4985bf6dbd1aff1eb1680 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Wed, 9 Mar 2016 15:16:46 +0100 Subject: [PATCH 072/223] Parse config/resque.yml in one place only --- config/application.rb | 18 ++--------------- config/initializers/session_store.rb | 5 ++++- config/initializers/sidekiq.rb | 17 +++++----------- config/mail_room.yml | 9 ++------- lib/gitlab/redis_config.rb | 30 ++++++++++++++++++++++++++++ lib/tasks/cache.rake | 6 +++--- 6 files changed, 46 insertions(+), 39 deletions(-) create mode 100644 lib/gitlab/redis_config.rb diff --git a/config/application.rb b/config/application.rb index 7fd75ebe69e8..d8d1e7b46795 100644 --- a/config/application.rb +++ b/config/application.rb @@ -4,6 +4,7 @@ require 'devise' I18n.config.enforce_available_locales = false Bundler.require(:default, Rails.env) +require_relative '../lib/gitlab/redis_config' module Gitlab REDIS_CACHE_NAMESPACE = 'cache:gitlab' @@ -67,22 +68,7 @@ class Application < Rails::Application end end - # Use Redis caching across all environments - redis_config_file = Rails.root.join('config', 'resque.yml') - - redis_url_string = if File.exists?(redis_config_file) - YAML.load_file(redis_config_file)[Rails.env] - else - "redis://localhost:6379" - end - - # Redis::Store does not handle Unix sockets well, so let's do it for them - redis_config_hash = Redis::Store::Factory.extract_host_options_from_uri(redis_url_string) - redis_uri = URI.parse(redis_url_string) - if redis_uri.scheme == 'unix' - redis_config_hash[:path] = redis_uri.path - end - + redis_config_hash = Gitlab::RedisConfig.redis_store_options redis_config_hash[:namespace] = REDIS_CACHE_NAMESPACE redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever config.cache_store = :redis_store, redis_config_hash diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 0fc725842ba1..3da5d46be92a 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -13,9 +13,12 @@ if Rails.env.test? Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session" else + redis_config = Gitlab::RedisConfig.redis_store_options + redis_config[:namespace] = 'session:gitlab' + Gitlab::Application.config.session_store( :redis_store, # Using the cookie_store would enable session replay attacks. - servers: Rails.application.config.cache_store[1].merge(namespace: 'session:gitlab'), # re-use the Redis config from the Rails cache store + servers: redis_config, key: '_gitlab_session', secure: Gitlab.config.gitlab.https, httponly: true, diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index dcf6ce74d96b..cc83137745ad 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -1,16 +1,9 @@ -# Custom Redis configuration -config_file = Rails.root.join('config', 'resque.yml') - -resque_url = if File.exists?(config_file) - YAML.load_file(config_file)[Rails.env] - else - "redis://localhost:6379" - end +SIDEKIQ_REDIS_NAMESPACE = 'resque:gitlab' Sidekiq.configure_server do |config| config.redis = { - url: resque_url, - namespace: 'resque:gitlab' + url: Gitlab::RedisConfig.url, + namespace: SIDEKIQ_REDIS_NAMESPACE } config.server_middleware do |chain| @@ -36,7 +29,7 @@ Sidekiq.configure_client do |config| config.redis = { - url: resque_url, - namespace: 'resque:gitlab' + url: Gitlab::RedisConfig.url, + namespace: SIDEKIQ_REDIS_NAMESPACE } end diff --git a/config/mail_room.yml b/config/mail_room.yml index f266a70ee0df..aed55f74eab5 100644 --- a/config/mail_room.yml +++ b/config/mail_room.yml @@ -2,6 +2,7 @@ <% require "yaml" require "json" +require_relative "lib/gitlab/redis_config" rails_env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" @@ -17,13 +18,7 @@ if File.exists?(config_file) config['mailbox'] = "inbox" if config['mailbox'].nil? if config['enabled'] && config['address'] && config['address'].include?('%{key}') - redis_config_file = "config/resque.yml" - redis_url = - if File.exists?(redis_config_file) - YAML.load_file(redis_config_file)[rails_env] - else - "redis://localhost:6379" - end + redis_url = Gitlab::RedisConfig.new(rails_env).url %> - :host: <%= config['host'].to_json %> diff --git a/lib/gitlab/redis_config.rb b/lib/gitlab/redis_config.rb new file mode 100644 index 000000000000..eea51d5207f0 --- /dev/null +++ b/lib/gitlab/redis_config.rb @@ -0,0 +1,30 @@ +module Gitlab + class RedisConfig + attr_reader :url + + def self.url + new.url + end + + def self.redis_store_options + url = new.url + redis_config_hash = Redis::Store::Factory.extract_host_options_from_uri(url) + # Redis::Store does not handle Unix sockets well, so let's do it for them + redis_uri = URI.parse(url) + if redis_uri.scheme == 'unix' + redis_config_hash[:path] = redis_uri.path + end + redis_config_hash + end + + def initialize(rails_env=nil) + rails_env ||= Rails.env + config_file = File.expand_path('../../../config/resque.yml', __FILE__) + + @url = "redis://localhost:6379" + if File.exists?(config_file) + @url =YAML.load_file(config_file)[rails_env] + end + end + end +end \ No newline at end of file diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake index f221afcf73a2..51e746ef9236 100644 --- a/lib/tasks/cache.rake +++ b/lib/tasks/cache.rake @@ -4,16 +4,16 @@ namespace :cache do desc "GitLab | Clear redis cache" task :clear => :environment do - redis_store = Rails.cache.instance_variable_get(:@data) + redis = Redis.new(url: Gitlab::RedisConfig.url) cursor = REDIS_SCAN_START_STOP loop do - cursor, keys = redis_store.scan( + cursor, keys = redis.scan( cursor, match: "#{Gitlab::REDIS_CACHE_NAMESPACE}*", count: CLEAR_BATCH_SIZE ) - redis_store.del(*keys) if keys.any? + redis.del(*keys) if keys.any? break if cursor == REDIS_SCAN_START_STOP end -- GitLab From d5ccdae31a547aa2dc45bf8f8c256b2fcefd82a2 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Wed, 9 Mar 2016 11:18:13 -0300 Subject: [PATCH 073/223] Remove invalid todos from database --- db/migrate/20160309140734_fix_todos.rb | 16 ++++++++++++++++ db/schema.rb | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20160309140734_fix_todos.rb diff --git a/db/migrate/20160309140734_fix_todos.rb b/db/migrate/20160309140734_fix_todos.rb new file mode 100644 index 000000000000..ebe0fc82305d --- /dev/null +++ b/db/migrate/20160309140734_fix_todos.rb @@ -0,0 +1,16 @@ +class FixTodos < ActiveRecord::Migration + def up + execute <<-SQL + DELETE FROM todos + WHERE todos.target_type IN ('Commit', 'ProjectSnippet') + OR NOT EXISTS ( + SELECT * + FROM projects + WHERE projects.id = todos.project_id + ) + SQL + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index 4f56f3970db3..a74b86d8e2fd 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: 20160305220806) do +ActiveRecord::Schema.define(version: 20160309140734) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" -- GitLab From 952914b6ae90bcc938578e54920fb57420dbb6eb Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Wed, 9 Mar 2016 11:18:37 -0300 Subject: [PATCH 074/223] Update CHANGELOG --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index ba95785199ed..7adf993cdc22 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -25,6 +25,9 @@ v 8.6.0 (unreleased) - Add ability to show archived projects on dashboard, explore and group pages - Add shortcut to toggle markdown preview (Florent Baldino) +v 8.5.5 + - Fix error 500 in Todos + v 8.5.4 - Do not cache requests for badges (including builds badge) -- GitLab From 3f90b11e4ab3fad554518e3c1e397afedf764c20 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Wed, 9 Mar 2016 15:56:44 +0100 Subject: [PATCH 075/223] Always with the newlines --- lib/gitlab/redis_config.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/redis_config.rb b/lib/gitlab/redis_config.rb index eea51d5207f0..4949c6db5392 100644 --- a/lib/gitlab/redis_config.rb +++ b/lib/gitlab/redis_config.rb @@ -27,4 +27,4 @@ def initialize(rails_env=nil) end end end -end \ No newline at end of file +end -- GitLab From f695ee2cfbeda33430680ef05a077aa8d0534341 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Wed, 9 Mar 2016 13:49:05 +0100 Subject: [PATCH 076/223] Fixed part of the GitPushService specs These were broken by commit 21a05328ffd5cb9130ae516faa7dd672cacba90c. Two JIRA tests remain broken but I can't quite figure out how to fix them. --- spec/services/git_push_service_spec.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index f5c51e46e8bc..7fee16a0586d 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -271,22 +271,24 @@ allow(project.repository).to receive(:commits_between). and_return([closing_commit]) + + project.team << [commit_author, :master] end context "to default branches" do it "closes issues" do - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, commit_author, @oldrev, @newrev, @ref ) expect(Issue.find(issue.id)).to be_closed end it "adds a note indicating that the issue is now closed" do expect(SystemNoteService).to receive(:change_status).with(issue, project, commit_author, "closed", closing_commit) - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, commit_author, @oldrev, @newrev, @ref ) end it "doesn't create additional cross-reference notes" do expect(SystemNoteService).not_to receive(:cross_reference) - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, commit_author, @oldrev, @newrev, @ref ) end it "doesn't close issues when external issue tracker is in use" do @@ -294,7 +296,7 @@ # The push still shouldn't create cross-reference notes. expect do - execute_service(project, user, @oldrev, @newrev, 'refs/heads/hurf' ) + execute_service(project, commit_author, @oldrev, @newrev, 'refs/heads/hurf' ) end.not_to change { Note.where(project_id: project.id, system: true).count } end end @@ -316,7 +318,6 @@ end end - # EE-only tests context "for jira issue tracker" do include JiraServiceHelper -- GitLab From 3dd8776e0a79f4054e37c6032658a5412a5926cf Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Wed, 9 Mar 2016 15:38:07 +0100 Subject: [PATCH 077/223] Handle permissions for ExternalIssue instances This fixes the remainder of the GitPushService specs. --- app/models/ability.rb | 5 +++++ spec/services/git_push_service_spec.rb | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index f34554d557c0..fe9e0aab7173 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -9,6 +9,7 @@ def allowed(user, subject) when CommitStatus then commit_status_abilities(user, subject) when Project then project_abilities(user, subject) when Issue then issue_abilities(user, subject) + when ExternalIssue then external_issue_abilities(user, subject) when Note then note_abilities(user, subject) when ProjectSnippet then project_snippet_abilities(user, subject) when PersonalSnippet then personal_snippet_abilities(user, subject) @@ -424,6 +425,10 @@ def abilities end end + def external_issue_abilities(user, subject) + project_abilities(user, subject.project) + end + private def named_abilities(name) diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 7fee16a0586d..82813cee2272 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -367,7 +367,7 @@ } }.to_json - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, commit_author, @oldrev, @newrev, @ref ) expect(WebMock).to have_requested(:post, jira_api_transition_url).with( body: transition_body ).once @@ -378,7 +378,7 @@ body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, commit_author, @oldrev, @newrev, @ref ) expect(WebMock).to have_requested(:post, jira_api_comment_url).with( body: comment_body ).once -- GitLab From 5d7319d4614cfca0c7a958ce1804c93103201f29 Mon Sep 17 00:00:00 2001 From: Baldinof <baldinof@gmail.com> Date: Wed, 9 Mar 2016 17:13:21 +0000 Subject: [PATCH 078/223] Fix typo in shortcuts help --- app/views/help/_shortcuts.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 9fc2372762b3..da3c3711cdd0 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -25,7 +25,7 @@ %tr %td.shortcut - if browser.mac? - .key ctrl ⌘ p + .key ⌘ shift p - else .key ctrl shift p -- GitLab From b2d50c84677e4d27693b0841aec2aa8098013156 Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Tue, 8 Mar 2016 16:13:51 -0500 Subject: [PATCH 079/223] Add a `gemojione:sprite` Rake task This task will generate a standard and Retina sprite of all of the current Gemojione Emojis, with the accompanying SCSS map. It will not appear in `rake -T` output, and the dependent gems are not included in the Gemfile by default, because this task will only be needed occasionally. [ci skip] --- lib/tasks/gemojione.rake | 121 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 lib/tasks/gemojione.rake diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake new file mode 100644 index 000000000000..ebe301c1fc7c --- /dev/null +++ b/lib/tasks/gemojione.rake @@ -0,0 +1,121 @@ +# This task will generate a standard and Retina sprite of all of the current +# Gemojione Emojis, with the accompanying SCSS map. +# +# It will not appear in `rake -T` output, and the dependent gems are not +# included in the Gemfile by default, because this task will only be needed +# occasionally, such as when new Emojis are added to Gemojione. + +begin + require 'sprite_factory' + require 'rmagick' +rescue LoadError + # noop +end + +namespace :gemojione do + task sprite: :environment do + check_requirements! + + SIZE = 20 + RETINA = SIZE * 2 + + Dir.mktmpdir do |tmpdir| + # Copy the Gemojione assets to the temporary folder for resizing + FileUtils.cp_r(Gemojione.index.images_path, tmpdir) + + Dir.chdir(tmpdir) do + Dir["**/*.png"].each do |png| + resize!(File.join(tmpdir, png), SIZE) + end + end + + style_path = Rails.root.join(*%w(app assets stylesheets pages emojis.scss)) + + # Combine the resized assets into a packed sprite and re-generate the SCSS + SpriteFactory.cssurl = "image-url('$IMAGE')" + SpriteFactory.run!(File.join(tmpdir, 'images'), { + output_style: style_path, + output_image: "app/assets/images/emoji.png", + selector: '.emoji-', + style: :scss, + nocomments: true, + pngcrush: true, + layout: :packed + }) + + # SpriteFactory's SCSS is a bit too verbose for our purposes here, so + # let's simplify it + system(%Q(sed -i '' "s/width: #{SIZE}px; height: #{SIZE}px; background: image-url('emoji.png')/background-position:/" #{style_path})) + system(%Q(sed -i '' "s/ no-repeat//" #{style_path})) + + # Append a generic rule that applies to all Emojis + File.open(style_path, 'a') do |f| + f.puts + f.puts <<-CSS.strip_heredoc + .emoji-icon { + background-image: image-url('emoji.png'); + background-repeat: no-repeat; + height: #{SIZE}px; + width: #{SIZE}px; + + @media only screen and (-webkit-min-device-pixel-ratio: 2), + only screen and (min--moz-device-pixel-ratio: 2), + only screen and (-o-min-device-pixel-ratio: 2/1), + only screen and (min-device-pixel-ratio: 2), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 2dppx) { + background-image: image-url('emoji@2x.png'); + background-size: 840px 820px; + } + } + CSS + end + end + + # Now do it again but for Retina + Dir.mktmpdir do |tmpdir| + # Copy the Gemojione assets to the temporary folder for resizing + FileUtils.cp_r(Gemojione.index.images_path, tmpdir) + + Dir.chdir(tmpdir) do + Dir["**/*.png"].each do |png| + resize!(File.join(tmpdir, png), RETINA) + end + end + + # Combine the resized assets into a packed sprite and re-generate the SCSS + SpriteFactory.run!(File.join(tmpdir, 'images'), { + output_image: "app/assets/images/emoji@2x.png", + style: false, + nocomments: true, + pngcrush: true, + layout: :packed + }) + end + end + + def check_requirements! + return if defined?(SpriteFactory) && defined?(Magick) + + puts <<-MSG.strip_heredoc + This task is disabled by default and should only be run when the Gemojione + gem is updated with new Emojis. + + To enable this task, *temporarily* add the following lines to Gemfile and + re-bundle: + + gem 'sprite-factory' + gem 'rmagick' + MSG + + exit 1 + end + + def resize!(image_path, size) + # Resize the image in-place, save it, and free the object + image = Magick::Image.read(image_path).first + image.resize!(size, size) + image.write(image_path) { self.quality = 100 } + image.destroy! + end +end -- GitLab From 4f2f0c82000aebd5fee26c893a9c6abaaf71ed51 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Wed, 9 Mar 2016 14:47:04 -0300 Subject: [PATCH 080/223] Avoid error 500 when todo author was removed --- app/helpers/application_helper.rb | 2 +- app/views/dashboard/todos/_todo.html.haml | 5 ++++- app/views/projects/builds/index.html.haml | 2 +- app/views/projects/commit_statuses/_commit_status.html.haml | 6 ++++++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 40a8b512e4ce..368969c64721 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -72,7 +72,7 @@ def avatar_icon(user_or_email = nil, size = nil, scale = 2) if user_or_email.is_a?(User) user = user_or_email else - user = User.find_by_any_email(user_or_email.downcase) + user = User.find_by(email: user_or_email.try(:downcase)) end if user diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index f878d36e7394..45cfe3da188f 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -4,7 +4,10 @@ .todo-title %span.author-name - = link_to_author todo + - if todo.author + = link_to_author(todo) + - else + (removed) %span.todo-label = todo_action_name(todo) = todo_target_link(todo) diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 14f1d3226bbb..10ad52c5b182 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -56,6 +56,6 @@ %th - @builds.each do |build| - = render 'projects/commit_statuses/commit_status', commit_status: build, commit_sha: true, stage: true, coverage: @project.build_coverage_enabled?, allow_retry: true + = render 'projects/commit_statuses/commit_status', commit_status: build, commit_sha: true, stage: true, ref: false, coverage: @project.build_coverage_enabled?, allow_retry: true = paginate @builds, theme: 'gitlab' diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index a3449d1ae057..205122e5d938 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -20,6 +20,12 @@ - if defined?(commit_sha) && commit_sha %td = link_to commit_status.short_sha, namespace_project_commit_path(commit_status.project.namespace, commit_status.project, commit_status.sha), class: "monospace" + + - if defined?(ref) && ref + = link_to commit_status.ref, namespace_project_commits_path(commit_status.project.namespace, commit_status.project, commit_status.ref) + + - else + .light none %td - if commit_status.ref -- GitLab From b45976e24dc9be4a1a0dc565ac072a1b027caa03 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Wed, 9 Mar 2016 19:11:24 +0100 Subject: [PATCH 081/223] Acquire lock before LDAP sync --- app/controllers/application_controller.rb | 2 + lib/gitlab/expiring_lock.rb | 52 +++++++++++++++++++++++ lib/gitlab/ldap/access.rb | 6 +++ lib/gitlab/user_access.rb | 5 ++- 4 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 lib/gitlab/expiring_lock.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index fb74919ea23b..bc8019193ee7 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -246,6 +246,8 @@ def check_2fa_requirement def ldap_security_check if current_user && current_user.requires_ldap_check? + return unless Gitlab::LDAP::Access.try_lock_user(user) + unless Gitlab::LDAP::Access.allowed?(current_user) sign_out current_user flash[:alert] = "Access denied for your LDAP account." diff --git a/lib/gitlab/expiring_lock.rb b/lib/gitlab/expiring_lock.rb new file mode 100644 index 000000000000..ef77a24cf7d8 --- /dev/null +++ b/lib/gitlab/expiring_lock.rb @@ -0,0 +1,52 @@ +module Gitlab + # This class implements a distributed self-expiring lock. + # + # [2] pry(main)> l = Gitlab::ExpiringLock.new('foobar', 5) + # => #<Gitlab::ExpiringLock:0x007ffb9d7cb7f8 @key="foobar", @timeout=5> + # [3] pry(main)> l.try_lock + # => true + # [4] pry(main)> l.try_lock # Only the first try_lock succeeds + # => false + # [5] pry(main)> l.locked? + # => true + # [6] pry(main)> sleep 5 + # => 5 + # [7] pry(main)> l.locked? # After the timeout the lock is released + # => false + # + class ExpiringLock + def initialize(key, timeout) + @key, @timeout = key, timeout + end + + # Try to obtain the lock. Return true on succes, + # false if the lock is already taken. + def try_lock + # INCR does not change the key TTL + if redis.incr(redis_key) == 1 + # We won the race to insert the key into Redis + redis.expire(redis_key, @timeout) + true + else + # Somebody else won the race + false + end + end + + # Check if somebody somewhere locked this key + def locked? + !!redis.get(redis_key) + end + + private + + def redis + # Maybe someday we want to use a connection pool... + @redis ||= Redis.new(url: Gitlab::RedisConfig.url) + end + + def redis_key + "gitlab:expiring_lock:#{@key}" + end + end +end diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index da4435c7308d..29347c05b7db 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -7,6 +7,12 @@ module LDAP class Access attr_reader :provider, :user + LOCK_TIMEOUT = 600 + + def self.try_lock_user(user) + Gitlab::ExpiringLock.new("user_ldap_check:#{user.id}", LOCK_TIMEOUT).try_lock + end + def self.open(user, &block) Gitlab::LDAP::Adapter.open(user.ldap_identity.provider) do |adapter| block.call(self.new(user, adapter)) diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 4885baf95265..46ac5825fd1a 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -3,8 +3,9 @@ module UserAccess def self.allowed?(user) return false if user.blocked? - if user.requires_ldap_check? - return false unless Gitlab::LDAP::Access.allowed?(user) + if user.requires_ldap_check? && Gitlab::LDAP::Access.try_lock_user(user) + return Gitlab::LDAP::Access.allowed?(user) + end end true -- GitLab From 07f319710a56381ce407ed078d3b5a0b8549d38c Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Wed, 9 Mar 2016 16:21:53 -0300 Subject: [PATCH 082/223] Update CHANGELOG --- CHANGELOG | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 7adf993cdc22..49e841784f6f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,7 +26,8 @@ v 8.6.0 (unreleased) - Add shortcut to toggle markdown preview (Florent Baldino) v 8.5.5 - - Fix error 500 in Todos + - Ensure removing a project removes associated Todo entries. + - Prevent a 500 error in Todos when author was removed. v 8.5.4 - Do not cache requests for badges (including builds badge) -- GitLab From 4edbe5874f85f5c8edb75a46623572594ca97385 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg <zegerjan@gitlab.com> Date: Wed, 9 Mar 2016 20:43:54 +0100 Subject: [PATCH 083/223] Ignore .byebug_history --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1eb785451f49..8f861d76a373 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ .sass-cache/ .secret .vagrant +.byebug_history Vagrantfile backups/* config/aws.yml -- GitLab From 225bdab31c7a6c02a3a13e39e7d947b1174e7731 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 9 Mar 2016 21:22:25 +0100 Subject: [PATCH 084/223] Retry bundler and apt-get --- .gitlab-ci.yml | 4 ++-- scripts/prepare_build.sh | 24 +++++++++++++----------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ffefeb6dfd83..d21785f7af21 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -19,11 +19,11 @@ before_script: - source ./scripts/prepare_build.sh - ruby -v - which ruby - - gem install bundler --no-ri --no-rdoc + - retry gem install bundler --no-ri --no-rdoc - cp config/gitlab.yml.example config/gitlab.yml - touch log/application.log - touch log/test.log - - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" + - retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" - RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate stages: diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index 82de51a9a2e9..4a7ee7dbb64d 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -1,5 +1,16 @@ #!/bin/bash +retry() { + for i in $(seq 1 3); do + if eval "$@"; then + return 0 + fi + sleep 3s + echo "Retrying..." + done + return 1 +} + if [ -f /.dockerinit ]; then mkdir -p vendor @@ -12,17 +23,8 @@ if [ -f /.dockerinit ]; then popd # Try to install packages - for i in $(seq 1 3); do - apt-get update -yqqq || true - - if apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \ - libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip; then - break - fi - - sleep 3s - echo "Retrying package installation..." - done + retry 'apt-get update -yqqq; apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \ + libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip' cp config/database.yml.mysql config/database.yml sed -i 's/username:.*/username: root/g' config/database.yml -- GitLab From 22d7b8a94ca39c736dee5d57f442411a4976f07d Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran <alfredo@gitlab.com> Date: Wed, 9 Mar 2016 18:54:15 -0500 Subject: [PATCH 085/223] Set blue color for link inside .gray-content-block --- app/assets/stylesheets/framework/blocks.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index d7e4153ddc0d..e6609ac71085 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -28,6 +28,10 @@ border-bottom: 1px solid $border-color; color: $gl-gray; + a { + color: $md-link-color; + } + &.oneline-block { line-height: 42px; } -- GitLab From ff3625f81ffdafdd47786d34954dc75bfc54615d Mon Sep 17 00:00:00 2001 From: Josh Frye <joshfng@gmail.com> Date: Wed, 9 Mar 2016 20:58:41 -0500 Subject: [PATCH 086/223] Allow filtered dashboard/explore results to be paged. Fixes #14104 --- CHANGELOG | 1 + app/controllers/dashboard/projects_controller.rb | 4 ++-- app/controllers/explore/projects_controller.rb | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 49e841784f6f..6b2ee516b4dd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -24,6 +24,7 @@ v 8.6.0 (unreleased) - Add main language of a project in the list of projects (Tiago Botelho) - Add ability to show archived projects on dashboard, explore and group pages - Add shortcut to toggle markdown preview (Florent Baldino) + - Fix pagination for filtered dashboard and explore pages v 8.5.5 - Ensure removing a project removes associated Todo entries. diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index fc51c3241af6..0e8b63872ca1 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -8,7 +8,7 @@ def index @projects = filter_projects(@projects) @projects = @projects.includes(:namespace) @projects = @projects.sort(@sort = params[:sort]) - @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? + @projects = @projects.page(params[:page]).per(PER_PAGE) @last_push = current_user.recent_push @@ -32,7 +32,7 @@ def starred @projects = filter_projects(@projects) @projects = @projects.includes(:namespace, :forked_from_project, :tags) @projects = @projects.sort(@sort = params[:sort]) - @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? + @projects = @projects.page(params[:page]).per(PER_PAGE) @last_push = current_user.recent_push @groups = [] diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 5b811db3068e..8271ca87436a 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -8,7 +8,7 @@ def index @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? @projects = filter_projects(@projects) @projects = @projects.sort(@sort = params[:sort]) - @projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? + @projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) respond_to do |format| format.html @@ -23,7 +23,7 @@ def index def trending @projects = TrendingProjectsFinder.new.execute(current_user) @projects = filter_projects(@projects) - @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? + @projects = @projects.page(params[:page]).per(PER_PAGE) respond_to do |format| format.html @@ -39,7 +39,7 @@ def starred @projects = ProjectsFinder.new.execute(current_user) @projects = filter_projects(@projects) @projects = @projects.reorder('star_count DESC') - @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? + @projects = @projects.page(params[:page]).per(PER_PAGE) respond_to do |format| format.html -- GitLab From 862286a479776aa1aa6e449335a92485b24b3923 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 8 Mar 2016 15:56:19 +0100 Subject: [PATCH 087/223] Added specs for Issue#referenced_merge_requests --- spec/models/issue_spec.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 52271c7c8c6d..7f44ca2f7dbe 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -105,6 +105,31 @@ end end + describe '#referenced_merge_requests' do + it 'returns the referenced merge requests' do + project = create(:project, :public) + + mr1 = create(:merge_request, + source_project: project, + source_branch: 'master', + target_branch: 'feature') + + mr2 = create(:merge_request, + source_project: project, + source_branch: 'feature', + target_branch: 'master') + + issue = create(:issue, description: mr1.to_reference, project: project) + + create(:note_on_issue, + noteable: issue, + note: mr2.to_reference, + project_id: project.id) + + expect(issue.referenced_merge_requests).to eq([mr1, mr2]) + end + end + it_behaves_like 'an editable mentionable' do subject { create(:issue) } -- GitLab From 6cf824f1829cd3357c5b5ee8b8a41dc955bae07b Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 7 Mar 2016 11:22:13 +0000 Subject: [PATCH 088/223] Added CSS for new dropdowns See #3279 for the designs --- .../stylesheets/framework/dropdowns.scss | 175 +++++++++++++++++ app/views/help/ui.html.haml | 178 ++++++++++++++++++ 2 files changed, 353 insertions(+) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 3dc524ccca44..2e847a345fe6 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -36,6 +36,21 @@ border-radius: $border-radius-base; box-shadow: 0 2px 4px $dropdown-shadow-color; + &.is-loading { + .dropdown-content { + display: none; + } + + .dropdown-loading { + display: block; + } + } + + ul { + margin: 0; + padding: 0; + } + li { text-align: left; list-style: none; @@ -68,6 +83,57 @@ } } +.dropdown-menu-paging { + .dropdown-page-two { + display: none; + } + + &.is-page-two { + .dropdown-page-one { + display: none; + } + + .dropdown-page-two { + display: block; + } + } +} + +.dropdown-menu-user { + .avatar { + float: left; + width: 30px; + height: 30px; + margin: 0 10px 0 0; + } +} + +.dropdown-menu-user-link { + padding-top: 7px; + padding-bottom: 7px; +} + +.dropdown-menu-user-full-name { + display: block; + margin-bottom: 2px; + line-height: 1; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.dropdown-menu-user-username { + display: block; + line-height: 1; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.dropdown-select { + width: 280px; +} + .dropdown-menu-align-right { left: auto; right: 0; @@ -101,3 +167,112 @@ font-size: 13px; line-height: 22px; } + +.dropdown-title { + position: relative; + margin-bottom: 10px; + padding-left: 30px; + padding-right: 30px; + padding-bottom: 10px; + font-weight: 600; + line-height: 1; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; + border-bottom: 1px solid $dropdown-divider-color; + overflow: hidden; +} + +.dropdown-title-button { + position: absolute; + top: -1px; + padding: 0; + color: #BFBFBF; + font-size: 14px; + border: 0; + background: none; + + &:hover { + color: darken(#BFBFBF, 15%); + } +} + +.dropdown-menu-close { + right: 0; +} + +.dropdown-menu-back { + left: 0; +} + +.dropdown-input { + position: relative; + margin-bottom: 10px; + + .fa { + position: absolute; + top: 10px; + right: 10px; + color: #C7C7C7; + font-size: 12px; + pointer-events: none; + } +} + +.dropdown-input-field { + width: 100%; + padding: 0 7px; + color: #C7C7C7; + line-height: 30px; + border: 1px solid $dropdown-divider-color; + border-radius: 2px; + outline: 0; + + &:focus { + color: $dropdown-link-color; + border-color: rgb(58, 171, 240); + box-shadow: 0 0 4px rgba(#000, .2); + + + .fa { + color: $dropdown-link-color; + } + } + + &:hover { + + .fa { + color: $dropdown-link-color; + } + } +} + +.dropdown-content { + max-height: 200px; + overflow-y: scroll; +} + +.dropdown-footer { + padding-top: 10px; + margin-top: 10px; + font-size: 13px; + border-top: 1px solid $dropdown-divider-color; +} + +.dropdown-loading { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: none; + z-index: 9; + background-color: rgba(#fff, .6); + font-size: 28px; + + .fa { + position: absolute; + top: 50%; + left: 50%; + margin-top: -14px; + margin-left: -14px; + } +} diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index a2c0a858930b..1ec3b0e48532 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -18,6 +18,8 @@ = link_to 'Nav', '#nav' %li = link_to 'Buttons', '#buttons' + %li + = link_to 'Dropdowns', '#dropdowns' %li = link_to 'Panels', '#panels' %li @@ -212,6 +214,182 @@ %button.btn.btn-danger{:type => "button"} Danger %button.btn.btn-link{:type => "button"} Link + %h2#dropdowns Dropdowns + + .example + .clearfix + .dropdown.inline.pull-left + %button.dropdown-toggle.btn{type: 'button', data: {toggle: 'dropdown'}} + Dropdown + %b.caret + %ul.dropdown-menu + %li + %a{href: "#"} + Dropdown Option + .dropdown.inline.pull-right + %button.dropdown-toggle.btn{type: 'button', data: {toggle: 'dropdown'}} + Dropdown + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + %li + %a{href: "#"} + Dropdown Option + .example + %div + .dropdown.inline + %button.dropdown-toggle.btn{type: 'button', data: {toggle: 'dropdown'}} + Dropdown + %b.caret + %ul.dropdown-menu.dropdown-menu-selectable + %li + %a.is-active{href: "#"} + Dropdown Option + .example + %div + .dropdown.inline + %button.dropdown-toggle.btn{type: 'button', data: {toggle: 'dropdown'}} + Dropdown + %b.caret + .dropdown-menu.dropdown-select.dropdown-menu-selectable + .dropdown-title + %span Dropdown Title + %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + = icon('times') + .dropdown-input + %input.dropdown-input-field{type: "search", placeholder: "Filter results"} + = icon('search') + .dropdown-content + %ul + %li + %a.is-active{href: "#"} + Dropdown Option + %li + %a{href: "#"} + Dropdown Option + %li.divider + %li + %a{href: "#"} + Dropdown Option + %li + %a{href: "#"} + Dropdown Option + %li + %a{href: "#"} + Dropdown Option + %li + %a{href: "#"} + Dropdown Option + %li + %a{href: "#"} + Dropdown Option + .dropdown-footer + %strong Tip: + If an author is not a member of this project, you can still filter by his name while using the search field. + .dropdown.inline + %button.dropdown-toggle.btn{type: 'button', data: {toggle: 'dropdown'}} + Dropdown loading + %b.caret + .dropdown-menu.dropdown-select.dropdown-menu-selectable.is-loading + .dropdown-title + %span Dropdown Title + %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + = icon('times') + .dropdown-input + %input.dropdown-input-field{type: "search", placeholder: "Filter results"} + = icon('search') + .dropdown-content + %ul + %li + %a.is-active{href: "#"} + Dropdown Option + %li + %a{href: "#"} + Dropdown Option + %li.divider + %li + %a{href: "#"} + Dropdown Option + %li + %a{href: "#"} + Dropdown Option + %li + %a{href: "#"} + Dropdown Option + %li + %a{href: "#"} + Dropdown Option + %li + %a{href: "#"} + Dropdown Option + .dropdown-footer + %strong Tip: + If an author is not a member of this project, you can still filter by his name while using the search field. + .dropdown-loading + = icon('spinner spin') + + .example + %div + .dropdown.inline + %button.dropdown-toggle.btn{type: 'button', data: {toggle: 'dropdown'}} + Dropdown user + %b.caret + .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user + .dropdown-title + %span Dropdown Title + %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + = icon('times') + .dropdown-input + %input.dropdown-input-field{type: "search", placeholder: "Filter results"} + = icon('search') + .dropdown-content + %ul + %li + %a.dropdown-menu-user-link.is-active{href: "#"} + = link_to_member_avatar(current_user, size: 30) + %strong.dropdown-menu-user-full-name + = current_user.name + .dropdown-menu-user-username + = current_user.to_reference + + .example + %div + .dropdown.inline.open + %button.dropdown-toggle.btn{type: 'button', data: {toggle: 'dropdown'}} + Dropdown page 2 + %b.caret + .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user.dropdown-menu-paging.is-page-two + .dropdown-page-one + .dropdown-title + %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}} + = icon('arrow-left') + %span Dropdown Title + %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + = icon('times') + .dropdown-input + %input.dropdown-input-field{type: "search", placeholder: "Filter results"} + = icon('search') + .dropdown-content + %ul + %li + %a.dropdown-menu-user-link.is-active{href: "#"} + = link_to_member_avatar(current_user, size: 30) + %strong.dropdown-menu-user-full-name + = current_user.name + .dropdown-menu-user-username + = current_user.to_reference + .dropdown-page-two + .dropdown-title + %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}} + = icon('arrow-left') + %span Create label + %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + = icon('times') + .dropdown-input + %input.dropdown-input-field{type: "search", placeholder: "Name new label"} + .dropdown-content + %button.btn.btn-primary + Create + %h2#panels Panels .row -- GitLab From 18a74cf7b92ff5f0e64899c6be14ac489d2846c9 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 7 Mar 2016 11:26:35 +0000 Subject: [PATCH 089/223] Dropdown variables --- app/assets/stylesheets/framework/dropdowns.scss | 12 ++++++------ app/assets/stylesheets/framework/variables.scss | 5 +++++ app/views/help/ui.html.haml | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 2e847a345fe6..dbf113168118 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -187,13 +187,13 @@ position: absolute; top: -1px; padding: 0; - color: #BFBFBF; + color: $dropdown-title-btn-color; font-size: 14px; border: 0; background: none; &:hover { - color: darken(#BFBFBF, 15%); + color: darken($dropdown-title-btn-color, 15%); } } @@ -222,7 +222,7 @@ .dropdown-input-field { width: 100%; padding: 0 7px; - color: #C7C7C7; + color: $dropdown-input-color; line-height: 30px; border: 1px solid $dropdown-divider-color; border-radius: 2px; @@ -230,8 +230,8 @@ &:focus { color: $dropdown-link-color; - border-color: rgb(58, 171, 240); - box-shadow: 0 0 4px rgba(#000, .2); + border-color: $dropdown-input-focus-border; + box-shadow: 0 0 4px $dropdown-input-focus-shadow; + .fa { color: $dropdown-link-color; @@ -265,7 +265,7 @@ left: 0; display: none; z-index: 9; - background-color: rgba(#fff, .6); + background-color: $dropdown-loading-bg; font-size: 28px; .fa { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index cc84a5ff932c..aad7ca1a3962 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -138,3 +138,8 @@ $dropdown-shadow-color: rgba(#000, .1); $dropdown-divider-color: rgba(#000, .1); $dropdown-header-color: #959494; $dropdown-caret-color: #54565B; +$dropdown-title-btn-color: #BFBFBF; +$dropdown-input-color: #C7C7C7; +$dropdown-input-focus-border: rgb(58, 171, 240); +$dropdown-input-focus-shadow: rgba(#000, .2); +$dropdown-loading-bg: rgba(#fff, .6); diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 1ec3b0e48532..97a27e373edb 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -353,7 +353,7 @@ .example %div - .dropdown.inline.open + .dropdown.inline %button.dropdown-toggle.btn{type: 'button', data: {toggle: 'dropdown'}} Dropdown page 2 %b.caret -- GitLab From 3ea66024bec863a7242c2cc2437e9f4c4379845f Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 7 Mar 2016 11:50:39 +0000 Subject: [PATCH 090/223] Dropdown toggle button styling --- .../stylesheets/framework/dropdowns.scss | 41 +++++++++++++++++++ .../stylesheets/framework/variables.scss | 7 ++++ app/views/help/ui.html.haml | 32 +++++++-------- 3 files changed, 64 insertions(+), 16 deletions(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index dbf113168118..e5e1c8310edb 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -17,6 +17,47 @@ .dropdown-menu { display: block; } + + .dropdown-menu-toggle { + border-color: $dropdown-toggle-hover-border-color; + + .fa { + color: $dropdown-toggle-hover-icon-color; + } + } +} + +.dropdown-menu-toggle { + position: relative; + min-width: 160px; + padding: 5px 20px 5px 10px; + background-color: $dropdown-toggle-bg; + color: $dropdown-toggle-color; + font-size: 15px; + text-align: left; + border: 1px solid $dropdown-toggle-border-color; + border-radius: 2px; + outline: 0; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + + .fa { + position: absolute; + top: 50%; + right: 6px; + margin-top: -4px; + color: $dropdown-toggle-icon-color; + font-size: 10px; + } + + &:hover, { + border-color: $dropdown-toggle-hover-border-color; + + .fa { + color: $dropdown-toggle-hover-icon-color; + } + } } .dropdown-menu { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index aad7ca1a3962..6561b3de7c1d 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -143,3 +143,10 @@ $dropdown-input-color: #C7C7C7; $dropdown-input-focus-border: rgb(58, 171, 240); $dropdown-input-focus-shadow: rgba(#000, .2); $dropdown-loading-bg: rgba(#fff, .6); + +$dropdown-toggle-bg: #fff; +$dropdown-toggle-color: #626262; +$dropdown-toggle-border-color: #EAEAEA; +$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%); +$dropdown-toggle-icon-color: #C4C4C4; +$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color; diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 97a27e373edb..0123834bc313 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -182,9 +182,9 @@ .nav-controls = text_field_tag 'sample', nil, class: 'form-control' .dropdown - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'} %span Sort by name - %b.caret + = icon('chevron-down') %ul.dropdown-menu %li %a Sort by date @@ -219,17 +219,17 @@ .example .clearfix .dropdown.inline.pull-left - %button.dropdown-toggle.btn{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} Dropdown - %b.caret + = icon('chevron-down') %ul.dropdown-menu %li %a{href: "#"} Dropdown Option .dropdown.inline.pull-right - %button.dropdown-toggle.btn{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} Dropdown - %b.caret + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-align-right %li %a{href: "#"} @@ -237,9 +237,9 @@ .example %div .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} Dropdown - %b.caret + = icon('chevron-down') %ul.dropdown-menu.dropdown-menu-selectable %li %a.is-active{href: "#"} @@ -247,9 +247,9 @@ .example %div .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} Dropdown - %b.caret + = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-title %span Dropdown Title @@ -286,9 +286,9 @@ %strong Tip: If an author is not a member of this project, you can still filter by his name while using the search field. .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} Dropdown loading - %b.caret + = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-selectable.is-loading .dropdown-title %span Dropdown Title @@ -330,9 +330,9 @@ .example %div .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} Dropdown user - %b.caret + = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user .dropdown-title %span Dropdown Title @@ -354,9 +354,9 @@ .example %div .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', data: {toggle: 'dropdown'}} + %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} Dropdown page 2 - %b.caret + = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user.dropdown-menu-paging.is-page-two .dropdown-page-one .dropdown-title -- GitLab From a67ee0e540a1d6d580cc9915f869f74997749919 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Thu, 10 Mar 2016 10:41:16 +0100 Subject: [PATCH 091/223] Rename ExpiringLock to ExclusiveLease --- app/controllers/application_controller.rb | 2 +- lib/gitlab/exclusive_lease.rb | 37 ++++++++++++++++ lib/gitlab/expiring_lock.rb | 52 ----------------------- lib/gitlab/ldap/access.rb | 2 +- 4 files changed, 39 insertions(+), 54 deletions(-) create mode 100644 lib/gitlab/exclusive_lease.rb delete mode 100644 lib/gitlab/expiring_lock.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index bc8019193ee7..15fee9948ec1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -246,7 +246,7 @@ def check_2fa_requirement def ldap_security_check if current_user && current_user.requires_ldap_check? - return unless Gitlab::LDAP::Access.try_lock_user(user) + return unless Gitlab::LDAP::Access.try_lock_user(current_user) unless Gitlab::LDAP::Access.allowed?(current_user) sign_out current_user diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb new file mode 100644 index 000000000000..f801e8b60b38 --- /dev/null +++ b/lib/gitlab/exclusive_lease.rb @@ -0,0 +1,37 @@ +require 'securerandom' + +module Gitlab + # This class implements an 'exclusive lease'. We call it a 'lease' + # because it has a set expiry time. We call it 'exclusive' because only + # one caller may obtain a lease for a given key at a time. The + # implementation is intended to work across GitLab processes and across + # servers. It is a 'cheap' alternative to using SQL queries and updates: + # you do not need to change the SQL schema to start using + # ExclusiveLease. + class ExclusiveLease + def initialize(key, timeout) + @key, @timeout = key, timeout + end + + # Try to obtain the lease. Return true on succes, + # false if the lease is already taken. + def try_obtain + !!redis.set(redis_key, redis_value, nx: true, ex: @timeout) + end + + private + + def redis + # Maybe someday we want to use a connection pool... + @redis ||= Redis.new(url: Gitlab::RedisConfig.url) + end + + def redis_key + "gitlab:exclusive_lease:#{@key}" + end + + def redis_value + @redis_value ||= SecureRandom.hex(10) + end + end +end diff --git a/lib/gitlab/expiring_lock.rb b/lib/gitlab/expiring_lock.rb deleted file mode 100644 index ef77a24cf7d8..000000000000 --- a/lib/gitlab/expiring_lock.rb +++ /dev/null @@ -1,52 +0,0 @@ -module Gitlab - # This class implements a distributed self-expiring lock. - # - # [2] pry(main)> l = Gitlab::ExpiringLock.new('foobar', 5) - # => #<Gitlab::ExpiringLock:0x007ffb9d7cb7f8 @key="foobar", @timeout=5> - # [3] pry(main)> l.try_lock - # => true - # [4] pry(main)> l.try_lock # Only the first try_lock succeeds - # => false - # [5] pry(main)> l.locked? - # => true - # [6] pry(main)> sleep 5 - # => 5 - # [7] pry(main)> l.locked? # After the timeout the lock is released - # => false - # - class ExpiringLock - def initialize(key, timeout) - @key, @timeout = key, timeout - end - - # Try to obtain the lock. Return true on succes, - # false if the lock is already taken. - def try_lock - # INCR does not change the key TTL - if redis.incr(redis_key) == 1 - # We won the race to insert the key into Redis - redis.expire(redis_key, @timeout) - true - else - # Somebody else won the race - false - end - end - - # Check if somebody somewhere locked this key - def locked? - !!redis.get(redis_key) - end - - private - - def redis - # Maybe someday we want to use a connection pool... - @redis ||= Redis.new(url: Gitlab::RedisConfig.url) - end - - def redis_key - "gitlab:expiring_lock:#{@key}" - end - end -end diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index 29347c05b7db..76786169a493 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -10,7 +10,7 @@ class Access LOCK_TIMEOUT = 600 def self.try_lock_user(user) - Gitlab::ExpiringLock.new("user_ldap_check:#{user.id}", LOCK_TIMEOUT).try_lock + Gitlab::ExclusiveLease.new("user_ldap_check:#{user.id}", LOCK_TIMEOUT).try_obtain end def self.open(user, &block) -- GitLab From 17630d83a22f3c1940c0391699f7fd4884ce142d Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Thu, 10 Mar 2016 11:05:59 +0100 Subject: [PATCH 092/223] Explain LDAP "lock" behavior --- lib/gitlab/ldap/access.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index 76786169a493..90d5996757f6 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -7,10 +7,14 @@ module LDAP class Access attr_reader :provider, :user - LOCK_TIMEOUT = 600 + # This timeout acts as a throttle on LDAP user checks. Its value of 600 + # seconds (10 minutes) means that after calling try_lock_user for user + # janedoe, no new LDAP checks can start for that user for the next 10 + # minutes. + LEASE_TIMEOUT = 600 def self.try_lock_user(user) - Gitlab::ExclusiveLease.new("user_ldap_check:#{user.id}", LOCK_TIMEOUT).try_obtain + Gitlab::ExclusiveLease.new("user_ldap_check:#{user.id}", LEASE_TIMEOUT).try_obtain end def self.open(user, &block) -- GitLab From 99ed35e97aef407239420d77d6455ac7991cd733 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Wed, 9 Mar 2016 11:39:48 +0100 Subject: [PATCH 093/223] Removed benchmark suite and its documentation The rationale for this can be found in https://gitlab.com/gitlab-org/gitlab-ce/issues/13718 but in short the benchmark suite no longer serves a good purpose now that we have proper production monitoring in place. Fixes gitlab-org/gitlab-ce#13718 --- .gitlab-ci.yml | 27 +------ doc/development/README.md | 1 - doc/development/benchmarking.md | 69 ---------------- lib/tasks/spec.rake | 13 +--- spec/benchmarks/finders/issues_finder_spec.rb | 55 ------------- .../finders/trending_projects_finder_spec.rb | 14 ---- .../gitlab/markdown/reference_filter_spec.rb | 41 ---------- spec/benchmarks/models/milestone_spec.rb | 17 ---- spec/benchmarks/models/project_spec.rb | 50 ------------ spec/benchmarks/models/project_team_spec.rb | 23 ------ spec/benchmarks/models/user_spec.rb | 78 ------------------- .../services/projects/create_service_spec.rb | 28 ------- spec/spec_helper.rb | 2 - spec/support/matchers/benchmark_matchers.rb | 61 --------------- 14 files changed, 3 insertions(+), 476 deletions(-) delete mode 100644 doc/development/benchmarking.md delete mode 100644 spec/benchmarks/finders/issues_finder_spec.rb delete mode 100644 spec/benchmarks/finders/trending_projects_finder_spec.rb delete mode 100644 spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb delete mode 100644 spec/benchmarks/models/milestone_spec.rb delete mode 100644 spec/benchmarks/models/project_spec.rb delete mode 100644 spec/benchmarks/models/project_team_spec.rb delete mode 100644 spec/benchmarks/models/user_spec.rb delete mode 100644 spec/benchmarks/services/projects/create_service_spec.rb delete mode 100644 spec/support/matchers/benchmark_matchers.rb diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d21785f7af21..bd013d50faaf 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -71,15 +71,6 @@ spec:services: - ruby - mysql -spec:benchmark: - stage: test - script: - - RAILS_ENV=test bundle exec rake spec:benchmark - tags: - - ruby - - mysql - allow_failure: true - spec:other: stage: test script: @@ -243,22 +234,6 @@ spec:services:ruby22: - ruby - mysql -spec:benchmark:ruby22: - stage: test - image: ruby:2.2 - only: - - master - script: - - RAILS_ENV=test bundle exec rake spec:benchmark - cache: - key: "ruby22" - paths: - - vendor - tags: - - ruby - - mysql - allow_failure: true - spec:other:ruby22: stage: test image: ruby:2.2 @@ -332,4 +307,4 @@ notify:slack: - master@gitlab-org/gitlab-ce - tags@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ee - - tags@gitlab-org/gitlab-ee \ No newline at end of file + - tags@gitlab-org/gitlab-ee diff --git a/doc/development/README.md b/doc/development/README.md index f5c3107ff441..1b281809afcb 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -1,7 +1,6 @@ # Development - [Architecture](architecture.md) of GitLab -- [Benchmarking](benchmarking.md) - [CI setup](ci_setup.md) for testing GitLab - [Gotchas](gotchas.md) to avoid - [How to dump production data to staging](db_dump.md) diff --git a/doc/development/benchmarking.md b/doc/development/benchmarking.md deleted file mode 100644 index 88e18ee95f9b..000000000000 --- a/doc/development/benchmarking.md +++ /dev/null @@ -1,69 +0,0 @@ -# Benchmarking - -GitLab CE comes with a set of benchmarks that are executed for every build. This -makes it easier to measure performance of certain components over time. - -Benchmarks are written as RSpec tests using a few extra helpers. To write a -benchmark, first tag the top-level `describe`: - -```ruby -describe MaruTheCat, benchmark: true do - -end -``` - -This ensures the benchmark is executed separately from other test collections. -It also exposes the various RSpec matchers used for writing benchmarks to the -test group. - -Next, lets write the actual benchmark: - -```ruby -describe MaruTheCat, benchmark: true do - let(:maru) { MaruTheChat.new } - - describe '#jump_in_box' do - benchmark_subject { maru.jump_in_box } - - it { is_expected.to iterate_per_second(9000) } - end -end -``` - -Here `benchmark_subject` is a small wrapper around RSpec's `subject` method that -makes it easier to specify the subject of a benchmark. Using RSpec's regular -`subject` would require us to write the following instead: - -```ruby -subject { -> { maru.jump_in_box } } -``` - -The `iterate_per_second` matcher defines the amount of times per second a -subject should be executed. The higher the amount of iterations the better. - -By default the allowed standard deviation is a maximum of 30%. This can be -adjusted by chaining the `with_maximum_stddev` on the `iterate_per_second` -matcher: - -```ruby -it { is_expected.to iterate_per_second(9000).with_maximum_stddev(50) } -``` - -This can be useful if the code in question depends on external resources of -which the performance can vary a lot (e.g. physical HDDs, network calls, etc). -However, in most cases 30% should be enough so only change this when really -needed. - -## Benchmarks Location - -Benchmarks should be stored in `spec/benchmarks` and should follow the regular -Rails specs structure. That is, model benchmarks go in `spec/benchmark/models`, -benchmarks for code in the `lib` directory go in `spec/benchmarks/lib`, etc. - -## Underlying Technology - -The benchmark setup uses [benchmark-ips][benchmark-ips] which takes care of the -heavy lifting such as warming up code, calculating iterations, standard -deviation, etc. - -[benchmark-ips]: https://github.com/evanphx/benchmark-ips diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake index 0985ef3a669e..2cf7a25a0fde 100644 --- a/lib/tasks/spec.rake +++ b/lib/tasks/spec.rake @@ -46,20 +46,11 @@ namespace :spec do run_commands(cmds) end - desc 'GitLab | Rspec | Run benchmark specs' - task :benchmark do - cmds = [ - %W(rake gitlab:setup), - %W(rspec spec --tag @benchmark) - ] - run_commands(cmds) - end - desc 'GitLab | Rspec | Run other specs' task :other do cmds = [ %W(rake gitlab:setup), - %W(rspec spec --tag ~@api --tag ~@feature --tag ~@models --tag ~@lib --tag ~@services --tag ~@benchmark) + %W(rspec spec --tag ~@api --tag ~@feature --tag ~@models --tag ~@lib --tag ~@services) ] run_commands(cmds) end @@ -69,7 +60,7 @@ desc "GitLab | Run specs" task :spec do cmds = [ %W(rake gitlab:setup), - %W(rspec spec --tag ~@benchmark), + %W(rspec spec), ] run_commands(cmds) end diff --git a/spec/benchmarks/finders/issues_finder_spec.rb b/spec/benchmarks/finders/issues_finder_spec.rb deleted file mode 100644 index b57a33004a48..000000000000 --- a/spec/benchmarks/finders/issues_finder_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -require 'spec_helper' - -describe IssuesFinder, benchmark: true do - describe '#execute' do - let(:user) { create(:user) } - let(:project) { create(:project, :public) } - - let(:label1) { create(:label, project: project, title: 'A') } - let(:label2) { create(:label, project: project, title: 'B') } - - before do - 10.times do |n| - issue = create(:issue, author: user, project: project) - - if n > 4 - create(:label_link, label: label1, target: issue) - create(:label_link, label: label2, target: issue) - end - end - end - - describe 'retrieving issues without labels' do - let(:finder) do - IssuesFinder.new(user, scope: 'all', label_name: Label::None.title, - state: 'opened') - end - - benchmark_subject { finder.execute } - - it { is_expected.to iterate_per_second(2000) } - end - - describe 'retrieving issues with labels' do - let(:finder) do - IssuesFinder.new(user, scope: 'all', label_name: label1.title, - state: 'opened') - end - - benchmark_subject { finder.execute } - - it { is_expected.to iterate_per_second(1000) } - end - - describe 'retrieving issues for a single project' do - let(:finder) do - IssuesFinder.new(user, scope: 'all', label_name: Label::None.title, - state: 'opened', project_id: project.id) - end - - benchmark_subject { finder.execute } - - it { is_expected.to iterate_per_second(2000) } - end - end -end diff --git a/spec/benchmarks/finders/trending_projects_finder_spec.rb b/spec/benchmarks/finders/trending_projects_finder_spec.rb deleted file mode 100644 index 551ce21840d6..000000000000 --- a/spec/benchmarks/finders/trending_projects_finder_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'spec_helper' - -describe TrendingProjectsFinder, benchmark: true do - describe '#execute' do - let(:finder) { described_class.new } - let(:user) { create(:user) } - - # to_a is used to force actually running the query (instead of just building - # it). - benchmark_subject { finder.execute(user).non_archived.to_a } - - it { is_expected.to iterate_per_second(500) } - end -end diff --git a/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb b/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb deleted file mode 100644 index 3855763b200b..000000000000 --- a/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -require 'spec_helper' - -describe Banzai::Filter::ReferenceFilter, benchmark: true do - let(:input) do - html = <<-EOF -<p>Hello @alice and @bob, how are you doing today?</p> -<p>This is simple @dummy text to see how the @ReferenceFilter class performs -when @processing HTML.</p> - EOF - - Nokogiri::HTML.fragment(html) - end - - let(:project) { create(:empty_project) } - - let(:filter) { described_class.new(input, project: project) } - - describe '#replace_text_nodes_matching' do - let(:iterations) { 6000 } - - describe 'with identical input and output HTML' do - benchmark_subject do - filter.replace_text_nodes_matching(User.reference_pattern) do |content| - content - end - end - - it { is_expected.to iterate_per_second(iterations) } - end - - describe 'with different input and output HTML' do - benchmark_subject do - filter.replace_text_nodes_matching(User.reference_pattern) do |content| - '@eve' - end - end - - it { is_expected.to iterate_per_second(iterations) } - end - end -end diff --git a/spec/benchmarks/models/milestone_spec.rb b/spec/benchmarks/models/milestone_spec.rb deleted file mode 100644 index a94afc4c40d8..000000000000 --- a/spec/benchmarks/models/milestone_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'spec_helper' - -describe Milestone, benchmark: true do - describe '#sort_issues' do - let(:milestone) { create(:milestone) } - - let(:issue1) { create(:issue, milestone: milestone) } - let(:issue2) { create(:issue, milestone: milestone) } - let(:issue3) { create(:issue, milestone: milestone) } - - let(:issue_ids) { [issue3.id, issue2.id, issue1.id] } - - benchmark_subject { milestone.sort_issues(issue_ids) } - - it { is_expected.to iterate_per_second(500) } - end -end diff --git a/spec/benchmarks/models/project_spec.rb b/spec/benchmarks/models/project_spec.rb deleted file mode 100644 index cee0949edc53..000000000000 --- a/spec/benchmarks/models/project_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'spec_helper' - -describe Project, benchmark: true do - describe '.trending' do - let(:group) { create(:group) } - let(:project1) { create(:empty_project, :public, group: group) } - let(:project2) { create(:empty_project, :public, group: group) } - - let(:iterations) { 500 } - - before do - 2.times do - create(:note_on_commit, project: project1) - end - - create(:note_on_commit, project: project2) - end - - describe 'without an explicit start date' do - benchmark_subject { described_class.trending.to_a } - - it { is_expected.to iterate_per_second(iterations) } - end - - describe 'with an explicit start date' do - let(:date) { 1.month.ago } - - benchmark_subject { described_class.trending(date).to_a } - - it { is_expected.to iterate_per_second(iterations) } - end - end - - describe '.find_with_namespace' do - let(:group) { create(:group, name: 'sisinmaru') } - let(:project) { create(:project, name: 'maru', namespace: group) } - - describe 'using a capitalized namespace' do - benchmark_subject { described_class.find_with_namespace('sisinmaru/MARU') } - - it { is_expected.to iterate_per_second(600) } - end - - describe 'using a lowercased namespace' do - benchmark_subject { described_class.find_with_namespace('sisinmaru/maru') } - - it { is_expected.to iterate_per_second(600) } - end - end -end diff --git a/spec/benchmarks/models/project_team_spec.rb b/spec/benchmarks/models/project_team_spec.rb deleted file mode 100644 index 8b039ef73173..000000000000 --- a/spec/benchmarks/models/project_team_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'spec_helper' - -describe ProjectTeam, benchmark: true do - describe '#max_member_access' do - let(:group) { create(:group) } - let(:project) { create(:empty_project, group: group) } - let(:user) { create(:user) } - - before do - project.team << [user, :master] - - 5.times do - project.team << [create(:user), :reporter] - - project.group.add_user(create(:user), :reporter) - end - end - - benchmark_subject { project.team.max_member_access(user.id) } - - it { is_expected.to iterate_per_second(35000) } - end -end diff --git a/spec/benchmarks/models/user_spec.rb b/spec/benchmarks/models/user_spec.rb deleted file mode 100644 index 1be7a8d3ed9d..000000000000 --- a/spec/benchmarks/models/user_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -require 'spec_helper' - -describe User, benchmark: true do - describe '.all' do - before do - 10.times { create(:user) } - end - - benchmark_subject { User.all.to_a } - - it { is_expected.to iterate_per_second(500) } - end - - describe '.by_login' do - before do - %w{Alice Bob Eve}.each do |name| - create(:user, - email: "#{name}@gitlab.com", - username: name, - name: name) - end - end - - # The iteration count is based on the query taking little over 1 ms when - # using PostgreSQL. - let(:iterations) { 900 } - - describe 'using a capitalized username' do - benchmark_subject { User.by_login('Alice') } - - it { is_expected.to iterate_per_second(iterations) } - end - - describe 'using a lowercase username' do - benchmark_subject { User.by_login('alice') } - - it { is_expected.to iterate_per_second(iterations) } - end - - describe 'using a capitalized Email address' do - benchmark_subject { User.by_login('Alice@gitlab.com') } - - it { is_expected.to iterate_per_second(iterations) } - end - - describe 'using a lowercase Email address' do - benchmark_subject { User.by_login('alice@gitlab.com') } - - it { is_expected.to iterate_per_second(iterations) } - end - end - - describe '.find_by_any_email' do - let(:user) { create(:user) } - - describe 'using a user with only a single Email address' do - let(:email) { user.email } - - benchmark_subject { User.find_by_any_email(email) } - - it { is_expected.to iterate_per_second(1000) } - end - - describe 'using a user with multiple Email addresses' do - let(:email) { user.emails.first.email } - - benchmark_subject { User.find_by_any_email(email) } - - before do - 10.times do - user.emails.create(email: FFaker::Internet.email) - end - end - - it { is_expected.to iterate_per_second(1000) } - end - end -end diff --git a/spec/benchmarks/services/projects/create_service_spec.rb b/spec/benchmarks/services/projects/create_service_spec.rb deleted file mode 100644 index 25ed48c34fdb..000000000000 --- a/spec/benchmarks/services/projects/create_service_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'spec_helper' - -describe Projects::CreateService, benchmark: true do - describe '#execute' do - let(:user) { create(:user, :admin) } - - let(:group) do - group = create(:group) - - create(:group_member, group: group, user: user) - - group - end - - benchmark_subject do - name = SecureRandom.hex - service = described_class.new(user, - name: name, - path: name, - namespace_id: group.id, - visibility_level: Gitlab::VisibilityLevel::PUBLIC) - - service.execute - end - - it { is_expected.to iterate_per_second(0.5) } - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 159fb9641718..7d939ca7509f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,7 +14,6 @@ require 'rspec/rails' require 'shoulda/matchers' require 'sidekiq/testing/inline' -require 'benchmark/ips' require 'rspec/retry' # Requires supporting ruby files with custom matchers and macros, etc, @@ -38,7 +37,6 @@ config.include ActiveJob::TestHelper config.include StubGitlabCalls config.include StubGitlabData - config.include BenchmarkMatchers, benchmark: true config.infer_spec_type_from_file_location! config.raise_errors_for_deprecations! diff --git a/spec/support/matchers/benchmark_matchers.rb b/spec/support/matchers/benchmark_matchers.rb deleted file mode 100644 index 84f655c21196..000000000000 --- a/spec/support/matchers/benchmark_matchers.rb +++ /dev/null @@ -1,61 +0,0 @@ -module BenchmarkMatchers - extend RSpec::Matchers::DSL - - def self.included(into) - into.extend(ClassMethods) - end - - matcher :iterate_per_second do |min_iterations| - supports_block_expectations - - match do |block| - @max_stddev ||= 30 - - @entry = benchmark(&block) - - expect(@entry.ips).to be >= min_iterations - expect(@entry.stddev_percentage).to be <= @max_stddev - end - - chain :with_maximum_stddev do |value| - @max_stddev = value - end - - description do - "run at least #{min_iterations} iterations per second" - end - - failure_message do - ips = @entry.ips.round(2) - stddev = @entry.stddev_percentage.round(2) - - "expected at least #{min_iterations} iterations per second " \ - "with a maximum stddev of #{@max_stddev}%, instead of " \ - "#{ips} iterations per second with a stddev of #{stddev}%" - end - end - - # Benchmarks the given block and returns a Benchmark::IPS::Report::Entry. - def benchmark(&block) - report = Benchmark.ips(quiet: true) do |bench| - bench.report do - instance_eval(&block) - end - end - - report.entries[0] - end - - module ClassMethods - # Wraps around rspec's subject method so you can write: - # - # benchmark_subject { SomeClass.some_method } - # - # instead of: - # - # subject { -> { SomeClass.some_method } } - def benchmark_subject(&block) - subject { block } - end - end -end -- GitLab From 7c3ff6d53327d8757e0a420fe1712cc4a3806153 Mon Sep 17 00:00:00 2001 From: Roberto Dip <dip.jesusr@gmail.com> Date: Mon, 15 Feb 2016 14:41:02 -0300 Subject: [PATCH 094/223] Decrease the font size and the padding of the `.anchor` icons This commit fixes #13334, removing an overlap between the anchor icon and the sidebar --- CHANGELOG | 1 + app/assets/stylesheets/framework/typography.scss | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6b2ee516b4dd..091ee247e681 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,7 @@ v 8.6.0 (unreleased) - Strip leading and trailing spaces in URL validator (evuez) - Add "last_sign_in_at" and "confirmed_at" to GET /users/* API endpoints for admins (evuez) - Return empty array instead of 404 when commit has no statuses in commit status API + - Decrease the font size and the padding of the `.anchor` icons used in the README (Roberto Dip) - Add support for cross-project label references - Update documentation to reflect Guest role not being enforced on internal projects - Allow search for logged out users diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 48570abff490..9381cb3281ca 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -149,13 +149,13 @@ } &:hover > a.anchor { - $size: 16px; + $size: 14px; position: absolute; right: 100%; top: 50%; - margin-top: -$size/2; - margin-right: 0px; - padding-right: 20px; + margin-top: -11px; + margin-right: 0; + padding-right: 15px; display: inline-block; width: $size; height: $size; -- GitLab From 5f54c47731e1696ef852eb69deeb906623b0772d Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 10 Mar 2016 11:02:22 +0000 Subject: [PATCH 095/223] Fixed quick submit on note edit Also removed the cloning of forms for note edits. Instead it just hides/shows the form seeing as it is already there Closes #14094 --- app/assets/javascripts/notes.js.coffee | 31 ++++++++++++------- app/views/projects/notes/_edit_form.html.haml | 2 +- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index eff8beb97e96..daa504b7782e 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -31,7 +31,7 @@ class @Notes $(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote # change note in UI after update - $(document).on "ajax:success", "form.edit_note", @updateNote + $(document).on "ajax:success", "form.edit-note", @updateNote # Edit note link $(document).on "click", ".js-note-edit", @showEditForm @@ -75,7 +75,7 @@ class @Notes cleanBinding: -> $(document).off "ajax:success", ".js-main-target-form" $(document).off "ajax:success", ".js-discussion-note-form" - $(document).off "ajax:success", "form.edit_note" + $(document).off "ajax:success", "form.edit-note" $(document).off "click", ".js-note-edit" $(document).off "click", ".note-edit-cancel" $(document).off "click", ".js-note-delete" @@ -355,22 +355,26 @@ class @Notes note = $(this).closest(".note") note.find(".note-body > .note-text").hide() note.find(".note-header").hide() - base_form = note.find(".note-edit-form") - form = base_form.clone().insertAfter(base_form) - form.addClass('current-note-edit-form gfm-form') - form.find('.div-dropzone').remove() + form = note.find(".note-edit-form") + isNewForm = form.is(':not(.gfm-form)') + if isNewForm + form.addClass('gfm-form') + form.addClass('current-note-edit-form') + form.show() # Show the attachment delete link note.find(".js-note-attachment-delete").show() # Setup markdown form - GitLab.GfmAutoComplete.setup() - new DropzoneInput(form) + if isNewForm + GitLab.GfmAutoComplete.setup() + new DropzoneInput(form) - form.show() textarea = form.find("textarea") textarea.focus() - autosize(textarea) + + if isNewForm + autosize(textarea) # HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?). # The textarea has the correct value, Chrome just won't show it unless we @@ -379,7 +383,8 @@ class @Notes textarea.val "" textarea.val value - disableButtonIfEmptyField textarea, form.find(".js-comment-button") + if isNewForm + disableButtonIfEmptyField textarea, form.find(".js-comment-button") ### Called in response to clicking the edit note link @@ -391,7 +396,9 @@ class @Notes note = $(this).closest(".note") note.find(".note-body > .note-text").show() note.find(".note-header").show() - note.find(".current-note-edit-form").remove() + note.find(".current-note-edit-form") + .removeClass("current-note-edit-form") + .hide() ### Called in response to deleting a note of any kind. diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index b5f076088c7b..13e624764d9b 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -1,5 +1,5 @@ .note-edit-form - = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, class: 'js-quick-submit' do |f| + = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note js-quick-submit' } do |f| = note_target_fields(note) = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field' -- GitLab From 65a349e2b2d54bb82a280df6c76398c16f17406f Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Thu, 10 Mar 2016 12:37:14 +0100 Subject: [PATCH 096/223] Move method to User --- app/controllers/application_controller.rb | 2 +- app/models/user.rb | 7 +++++++ lib/gitlab/exclusive_lease.rb | 13 ++++++++++++- lib/gitlab/ldap/access.rb | 10 ---------- lib/gitlab/user_access.rb | 5 ++--- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 15fee9948ec1..1f55b18e0b16 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -246,7 +246,7 @@ def check_2fa_requirement def ldap_security_check if current_user && current_user.requires_ldap_check? - return unless Gitlab::LDAP::Access.try_lock_user(current_user) + return unless current_user.try_obtain_ldap_lease unless Gitlab::LDAP::Access.allowed?(current_user) sign_out current_user diff --git a/app/models/user.rb b/app/models/user.rb index 3098d49d58a0..505a547d8ec6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -612,6 +612,13 @@ def requires_ldap_check? end end + def try_obtain_ldap_lease + # After obtaining this lease LDAP checks will be blocked for 600 seconds + # (10 minutes) for this user. + lease = Gitlab::ExclusiveLease.new("user_ldap_check:#{id}", timeout: 600) + lease.try_obtain + end + def solo_owned_groups @solo_owned_groups ||= owned_groups.select do |group| group.owners == [self] diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb index f801e8b60b38..0ed4c3579261 100644 --- a/lib/gitlab/exclusive_lease.rb +++ b/lib/gitlab/exclusive_lease.rb @@ -8,14 +8,25 @@ module Gitlab # servers. It is a 'cheap' alternative to using SQL queries and updates: # you do not need to change the SQL schema to start using # ExclusiveLease. + # + # It is important to choose the timeout wisely. If the timeout is very + # high (1 hour) then the throughput of your operation gets very low (at + # most once an hour). If the timeout is lower than how long your + # operation may take then you cannot count on exclusivity. For example, + # if the timeout is 10 seconds and you do an operation which may take 20 + # seconds then two overlapping operations may hold a lease at the + # same time. + # class ExclusiveLease - def initialize(key, timeout) + def initialize(key, timeout:) @key, @timeout = key, timeout end # Try to obtain the lease. Return true on succes, # false if the lease is already taken. def try_obtain + # This is expected to be atomic because we are talking to a + # single-threaded Redis server. !!redis.set(redis_key, redis_value, nx: true, ex: @timeout) end diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index 90d5996757f6..da4435c7308d 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -7,16 +7,6 @@ module LDAP class Access attr_reader :provider, :user - # This timeout acts as a throttle on LDAP user checks. Its value of 600 - # seconds (10 minutes) means that after calling try_lock_user for user - # janedoe, no new LDAP checks can start for that user for the next 10 - # minutes. - LEASE_TIMEOUT = 600 - - def self.try_lock_user(user) - Gitlab::ExclusiveLease.new("user_ldap_check:#{user.id}", LEASE_TIMEOUT).try_obtain - end - def self.open(user, &block) Gitlab::LDAP::Adapter.open(user.ldap_identity.provider) do |adapter| block.call(self.new(user, adapter)) diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 46ac5825fd1a..d1b42c1f9b97 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -3,9 +3,8 @@ module UserAccess def self.allowed?(user) return false if user.blocked? - if user.requires_ldap_check? && Gitlab::LDAP::Access.try_lock_user(user) - return Gitlab::LDAP::Access.allowed?(user) - end + if user.requires_ldap_check? && user.try_obtain_ldap_lease + return false unless Gitlab::LDAP::Access.allowed?(user) end true -- GitLab From f912dd4bc68a281f4f26634bc24b12df3e9d851d Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Thu, 10 Mar 2016 12:52:19 +0100 Subject: [PATCH 097/223] Add tests for ExclusiveLease --- spec/lib/gitlab/exclusive_lease_spec.rb | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 spec/lib/gitlab/exclusive_lease_spec.rb diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb new file mode 100644 index 000000000000..aacd1707165a --- /dev/null +++ b/spec/lib/gitlab/exclusive_lease_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::ExclusiveLease do + it 'is exclusive' do + lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600) + expect(lease.try_obtain).to eq(true) + expect(lease.try_obtain).to eq(false) + end + + it 'expires' do + timeout = 1 + lease = Gitlab::ExclusiveLease.new(unique_key, timeout: timeout) + lease.try_obtain + sleep(2 * timeout) + expect(lease.try_obtain).to eq(true) + end + + def unique_key + SecureRandom.hex(10) + end +end -- GitLab From ab504d9749f69cd44ac49670a17d42b42fd7a723 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 10 Mar 2016 11:52:31 +0000 Subject: [PATCH 098/223] Fixed flicker of download project button Closes #14177 --- app/views/projects/buttons/_download.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 14ee2263b7d8..6a60cfeff76f 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,4 +1,4 @@ - unless @project.empty_repo? - if can? current_user, :download_code, @project - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has_tooltip', rel: 'nofollow', title: "Download ZIP" do + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has_tooltip', data: {container: "body"}, rel: 'nofollow', title: "Download ZIP" do = icon('download') -- GitLab From 5661439dea576d178e71850770b9507c8cde50ba Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Thu, 10 Mar 2016 12:55:06 +0100 Subject: [PATCH 099/223] Improve test descriptions --- spec/lib/gitlab/exclusive_lease_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb index aacd1707165a..fbdb7ea34ac5 100644 --- a/spec/lib/gitlab/exclusive_lease_spec.rb +++ b/spec/lib/gitlab/exclusive_lease_spec.rb @@ -1,17 +1,17 @@ require 'spec_helper' describe Gitlab::ExclusiveLease do - it 'is exclusive' do + it 'cannot obtain twice before the lease has expired' do lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600) expect(lease.try_obtain).to eq(true) expect(lease.try_obtain).to eq(false) end - it 'expires' do + it 'can obtain after the lease has expired' do timeout = 1 lease = Gitlab::ExclusiveLease.new(unique_key, timeout: timeout) - lease.try_obtain - sleep(2 * timeout) + lease.try_obtain # start the lease + sleep(2 * timeout) # lease should have expired now expect(lease.try_obtain).to eq(true) end -- GitLab From f9ce66ec0bca1d0fdc5e98a115e082c6e7bdbab6 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Thu, 10 Mar 2016 12:58:51 +0100 Subject: [PATCH 100/223] Improve comment --- lib/gitlab/exclusive_lease.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb index 0ed4c3579261..8bf6f13b99d3 100644 --- a/lib/gitlab/exclusive_lease.rb +++ b/lib/gitlab/exclusive_lease.rb @@ -14,8 +14,8 @@ module Gitlab # most once an hour). If the timeout is lower than how long your # operation may take then you cannot count on exclusivity. For example, # if the timeout is 10 seconds and you do an operation which may take 20 - # seconds then two overlapping operations may hold a lease at the - # same time. + # seconds then two overlapping operations may hold a lease for the same + # key at the same time. # class ExclusiveLease def initialize(key, timeout:) -- GitLab From ab35dc7313e87a64bc1da8b52fe8e74d932c81b9 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 10 Mar 2016 12:15:00 +0000 Subject: [PATCH 101/223] Fixes issue with markdown snippet not being copyable On markdown snippets this adds a hidden div with the raw markdown content so that it can be copied Closes #13882 --- app/views/shared/snippets/_blob.html.haml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml index e0e41fc4bea4..c84614c08796 100644 --- a/app/views/shared/snippets/_blob.html.haml +++ b/app/views/shared/snippets/_blob.html.haml @@ -1,5 +1,7 @@ - unless @snippet.content.empty? - if markup?(@snippet.file_name) + .hidden.blob-content{data: {blob_id: @snippet.id}} + = @snippet.data .file-content.wiki = render_markup(@snippet.file_name, @snippet.data) - else -- GitLab From fb59a663afd281d913253ac2aee7318bfa5e0825 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Thu, 10 Mar 2016 13:41:23 +0100 Subject: [PATCH 102/223] Fix projects pagination via ajax Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- app/assets/javascripts/projects_list.js.coffee | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee index ed5206368ce7..a0d7dc80afce 100644 --- a/app/assets/javascripts/projects_list.js.coffee +++ b/app/assets/javascripts/projects_list.js.coffee @@ -2,6 +2,7 @@ init: -> $(".projects-list-filter").off('keyup') this.initSearch() + this.initPagination() initSearch: -> @timer = null @@ -29,3 +30,8 @@ # Change url so if user reload a page - search results are saved history.replaceState {page: project_filter_url}, document.title, project_filter_url dataType: "json" + + initPagination: -> + $('.projects-list-holder .pagination').bind('ajax:success', (e, data) -> + $('.projects-list-holder').replaceWith(data.html) + ) -- GitLab From 684eee98b778c2d2e0a314259d9c28e072780a9d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Thu, 10 Mar 2016 13:55:01 +0100 Subject: [PATCH 103/223] Use modern syntax Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- app/assets/javascripts/projects_list.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee index a0d7dc80afce..e4c4bf3b2730 100644 --- a/app/assets/javascripts/projects_list.js.coffee +++ b/app/assets/javascripts/projects_list.js.coffee @@ -32,6 +32,6 @@ dataType: "json" initPagination: -> - $('.projects-list-holder .pagination').bind('ajax:success', (e, data) -> + $('.projects-list-holder .pagination').on('ajax:success', (e, data) -> $('.projects-list-holder').replaceWith(data.html) ) -- GitLab From 035071896fa03e82ed633695c12f3ae1c4cbcefa Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 10 Mar 2016 13:17:35 +0000 Subject: [PATCH 104/223] Moves build status tooltip on issues --- app/helpers/ci_status_helper.rb | 4 ++-- app/views/projects/issues/_merge_requests.html.haml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index d8bee21c82e9..10456189e214 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -42,12 +42,12 @@ def ci_icon_for_status(status) icon(icon_name + ' fw') end - def render_ci_status(ci_commit) + def render_ci_status(ci_commit, placement: 'left') link_to ci_status_icon(ci_commit), ci_status_path(ci_commit), class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}", title: "Build #{ci_status_label(ci_commit)}", - data: { toggle: 'tooltip', placement: 'left' } + data: { toggle: 'tooltip', placement: placement } end def no_runners_for_project?(project) diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index 640a1962ffc6..c6da6f7997e1 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -7,11 +7,11 @@ %li %span.merge-request-ci-status - if merge_request.ci_commit - = render_ci_status(merge_request.ci_commit) + = render_ci_status(merge_request.ci_commit, placement: 'bottom') - elsif has_any_ci = icon('blank fw') %span.merge-request-id - \!#{merge_request.iid} + #{merge_request.to_reference} %span.merge-request-info %strong = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title" -- GitLab From 7a21b0707cd5b51dbbc90b34007756e84abcdc84 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Thu, 10 Mar 2016 14:29:38 +0100 Subject: [PATCH 105/223] Move group activity feed to separate page for consistency with dashboard and project pages Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- CHANGELOG | 1 + app/assets/javascripts/dispatcher.js.coffee | 3 ++- app/controllers/groups_controller.rb | 6 ++++-- app/views/groups/_activities.html.haml | 12 ++++++++++++ app/views/groups/activity.html.haml | 9 +++++++++ app/views/groups/show.html.haml | 15 +-------------- app/views/layouts/nav/_group.html.haml | 7 ++++++- config/routes.rb | 2 +- features/groups.feature | 4 ++++ features/steps/shared/paths.rb | 4 ++++ 10 files changed, 44 insertions(+), 19 deletions(-) create mode 100644 app/views/groups/_activities.html.haml create mode 100644 app/views/groups/activity.html.haml diff --git a/CHANGELOG b/CHANGELOG index 091ee247e681..c3ef06c45811 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,7 @@ v 8.6.0 (unreleased) - Add ability to show archived projects on dashboard, explore and group pages - Add shortcut to toggle markdown preview (Florent Baldino) - Fix pagination for filtered dashboard and explore pages + - Move group activity to separate page v 8.5.5 - Ensure removing a project removes associated Todo entries. diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 77056bba8d27..8a95e3532350 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -74,8 +74,9 @@ class Dispatcher shortcut_handler = new ShortcutsNavigation() new TreeView() if $('#tree-slider').length - when 'groups:show' + when 'groups:activity' new Activities() + when 'groups:show' shortcut_handler = new ShortcutsNavigation() when 'groups:group_members:index' new GroupMembers() diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index f05c29e9974f..360930f95a80 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -15,7 +15,7 @@ class GroupsController < Groups::ApplicationController # Load group projects before_action :load_projects, except: [:index, :new, :create, :projects, :edit, :update, :autocomplete] - before_action :event_filter, only: [:show, :events] + before_action :event_filter, only: [:activity] layout :determine_layout @@ -62,8 +62,10 @@ def show end end - def events + def activity respond_to do |format| + format.html + format.json do load_events pager_json("events/_events", @events.count) diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml new file mode 100644 index 000000000000..dc76599b7767 --- /dev/null +++ b/app/views/groups/_activities.html.haml @@ -0,0 +1,12 @@ +.hidden-xs + = render "events/event_last_push", event: @last_push + +.nav-block + - if current_user + .controls + = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do + %i.fa.fa-rss + = render 'shared/event_filter' + +.content_list += spinner diff --git a/app/views/groups/activity.html.haml b/app/views/groups/activity.html.haml new file mode 100644 index 000000000000..10b432ef03ed --- /dev/null +++ b/app/views/groups/activity.html.haml @@ -0,0 +1,9 @@ += content_for :meta_tags do + - if current_user + = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") + +- page_title "Activity" +- header_title "Activity", activity_dashboard_path + +%section.activities + = render 'activities' diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 6148d8cb3d2f..3cf0a4baacd2 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -30,26 +30,13 @@ %ul.nav-links %li.active - = link_to "#activity", 'data-toggle' => 'tab' do - Activity - %li = link_to "#projects", 'data-toggle' => 'tab' do Projects - if can?(current_user, :read_group, @group) %div{ class: container_class } .tab-content - .tab-pane.active#activity - .activity-filter-block - - if current_user - = render "events/event_last_push", event: @last_push - - = render 'shared/event_filter' - - .content_list{data: {href: events_group_path}} - = spinner - - .tab-pane#projects + .tab-pane.active#projects = render "projects", projects: @projects - else diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index e5e2a59eaedb..59411ae1da10 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -9,10 +9,15 @@ = nav_link(path: 'groups#show', html_options: {class: 'home'}) do = link_to group_path(@group), title: 'Home' do - = icon('dashboard fw') + = icon('group fw') %span Group - if can?(current_user, :read_group, @group) + = nav_link(path: 'groups#activity') do + = link_to activity_group_path(@group), title: 'Activity' do + = icon('dashboard fw') + %span + Activity - if current_user = nav_link(controller: [:group, :milestones]) do = link_to group_milestones_path(@group), title: 'Milestones' do diff --git a/config/routes.rb b/config/routes.rb index a918b5bd3f0b..869fca03ec42 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -382,7 +382,7 @@ get :issues get :merge_requests get :projects - get :events + get :activity end scope module: :groups do diff --git a/features/groups.feature b/features/groups.feature index a60c3860b834..419a5d3963d6 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -15,6 +15,10 @@ Feature: Groups Scenario: I should see group "Owned" dashboard list When I visit group "Owned" page Then I should see group "Owned" projects list + + @javascript + Scenario: I should see group "Owned" activity feed + When I visit group "Owned" activity page And I should see projects activity feed Scenario: I should see group "Owned" issues list diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index da9d1503ebcf..2bd8ea745e47 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -27,6 +27,10 @@ module SharedPaths 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 -- GitLab From 93c24d77d712dec4c5aab580ac321bacc8333ddd Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 10 Mar 2016 13:34:31 +0000 Subject: [PATCH 106/223] Replaced hidden div for textarea off-the screen Element with the content being copied cant be hidden so instead i\'ve moved it off the screen --- app/assets/stylesheets/pages/snippets.scss | 8 ++++++++ app/views/shared/snippets/_blob.html.haml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index 7d414ae003d2..639d639d5b07 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -28,3 +28,11 @@ border: 1px solid; line-height: 32px; } + +.markdown-snippet-copy { + position: fixed; + top: -10px; + left: -10px; + max-height: 0; + max-width: 0; +} diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml index c84614c08796..773ce8ac240f 100644 --- a/app/views/shared/snippets/_blob.html.haml +++ b/app/views/shared/snippets/_blob.html.haml @@ -1,6 +1,6 @@ - unless @snippet.content.empty? - if markup?(@snippet.file_name) - .hidden.blob-content{data: {blob_id: @snippet.id}} + %textarea.markdown-snippet-copy.blob-content{data: {blob_id: @snippet.id}} = @snippet.data .file-content.wiki = render_markup(@snippet.file_name, @snippet.data) -- GitLab From f87cac12bfbbe531a1d408004810b95c45912170 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Date: Thu, 10 Mar 2016 14:35:23 +0100 Subject: [PATCH 107/223] Fix header title Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> --- app/views/groups/activity.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/groups/activity.html.haml b/app/views/groups/activity.html.haml index 10b432ef03ed..f73e1d9e8652 100644 --- a/app/views/groups/activity.html.haml +++ b/app/views/groups/activity.html.haml @@ -3,7 +3,7 @@ = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") - page_title "Activity" -- header_title "Activity", activity_dashboard_path +- header_title group_title(@group, "Activity", activity_group_path(@group)) %section.activities = render 'activities' -- GitLab From 1978523f74751ad656fe229ee5f859e00114f95e Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 7 Mar 2016 15:37:35 +0000 Subject: [PATCH 108/223] GitLab dropdown JS --- app/assets/javascripts/gl_dropdown.js.coffee | 194 ++++++++++++++++++ app/assets/javascripts/lib/fuse.js | 20 ++ .../stylesheets/framework/dropdowns.scss | 5 +- app/helpers/dropdowns_helper.rb | 47 +++++ app/views/help/ui.html.haml | 45 ++++ 5 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/gl_dropdown.js.coffee create mode 100644 app/assets/javascripts/lib/fuse.js create mode 100644 app/helpers/dropdowns_helper.rb diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee new file mode 100644 index 000000000000..f15d65473c62 --- /dev/null +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -0,0 +1,194 @@ +class GitLabDropdownFilter + BLUR_KEYCODES = [27, 40] + + constructor: (@dropdown, @remote, @data, @callback) -> + @input = @dropdown.find(".dropdown-input-field") + + # Key events + @input.on "keyup", (e) => + blur_field = @shouldBlur e.keyCode + search_text = @input.val() + + if blur_field + @input.blur() + + if @remote + @remote search_text, (data) => + @callback(data) + else + @filter search_text + + shouldBlur: (keyCode) -> + return BLUR_KEYCODES.indexOf(keyCode) >= 0 + + filter: (search_text) -> + data = @data() + results = if search_text isnt "" then data.search(search_text) else data.list + + @callback results + +class GitLabDropdownRemote + constructor: (@dataEndpoint, @options) -> + + execute: -> + if typeof @dataEndpoint is "string" + @fetchData() + else if typeof @dataEndpoint is "function" + if @options.beforeSend + @options.beforeSend() + + # Fetch the data by calling the data funcfion + @dataEndpoint (data) => + if @options.success + @options.success(data) + + if @options.beforeSend + @options.beforeSend() + + # Fetch the data through ajax if the data is a string + fetchData: -> + $.ajax( + url: @dataEndpoint, + dataType: @options.dataType, + beforeSend: => + if @options.beforeSend + @options.beforeSend() + success: (data) => + if @options.success + @options.success(data) + ) + +class GitLabDropdown + LOADING_CLASS = "is-loading" + + constructor: (@el, @options) -> + self = @ + @dropdown = $(@el).parent() + search_fields = if @options.search then @options.search.fields else []; + + if @options.data + # Remote data + @remote = new GitLabDropdownRemote @options.data, { + dataType: @options.dataType, + beforeSend: @toggleLoading.bind(@) + success: (data) => + @fullData = data + dataToPrase = @fullData + + if @options.filterable + @fullData = new Fuse data, { + keys: search_fields + } + dataToPrase = @fullData.list + + @parseData dataToPrase + } + + # Init filiterable + if @options.filterable + @filter = new GitLabDropdownFilter @dropdown, @options.query, => + return @fullData + , (data) => + @parseData data + + # Event listeners + $(@el).parent().on "shown.bs.dropdown", @opened + + if @options.selectable + @dropdown.on "click", "a", (e) -> + self.rowClicked $(@) + + if self.options.clicked + self.options.clicked() + + toggleLoading: -> + $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS + + parseData: (data) -> + @renderedData = data + + # Render each row + html = $.map data, (obj) => + return @renderItem(obj) + + if @options.filterable and data.length is 0 + # render no matching results + html = [@noResults()] + + # Render the full menu + full_html = @renderMenu(html.join("")) + + @appendMenu(full_html) + + opened: => + if @remote + @remote.execute() + + # Render the full menu + renderMenu: (html) -> + menu_html = "" + + if @options.renderMenu + menu_html = @options.renderMenu(html) + else + menu_html = "<ul>#{html}</ul>" + + return menu_html + + # Append the menu into the dropdown + appendMenu: (html) -> + $('.dropdown-content', @dropdown).html html + + # Render the row + renderItem: (data) -> + html = "" + + if @options.renderRow + # Call the render function + html = @options.renderRow(data) + else + selected = if @options.isSelected then @options.isSelected(data) else false + url = if @options.url then @options.url(data) else "" + text = if @options.text then @options.text(data) else "" + cssClass = ""; + + if selected + cssClass = "is-active" + + html = "<li>" + html += "<a href='#{url}' class='#{cssClass}'>" + html += text + html += "</a>" + html += "</li>" + + return html + + noResults: -> + html = "<li>" + html += "<a href='#' class='is-focused'>" + html += "No matching results." + html += "</a>" + html += "</li>" + + rowClicked: (el) -> + fieldName = @options.fieldName + selectedIndex = el.parent().index() + selectedObject = @renderedData[selectedIndex] + value = if @options.id then @options.id(selectedObject) else selectedObject.id + + if @options.multiSelect + fieldName = "[#{fieldName}]" + else + @dropdown.find('.is-active').removeClass 'is-active' + @dropdown.parent().find("input[name='#{fieldName}']").remove() + + # Toggle active class for the tick mark + el.toggleClass "is-active" + + # Create hidden input for form + input = "<input type='hidden' name='#{fieldName}' value='#{value}' />" + @dropdown.before input + +$.fn.glDropdown = (opts) -> + return @.each -> + new GitLabDropdown @, opts diff --git a/app/assets/javascripts/lib/fuse.js b/app/assets/javascripts/lib/fuse.js new file mode 100644 index 000000000000..09f4ad0aacce --- /dev/null +++ b/app/assets/javascripts/lib/fuse.js @@ -0,0 +1,20 @@ +/** + * @license + * Fuse - Lightweight fuzzy-search + * + * Copyright (c) 2012-2016 Kirollos Risk <kirollos@gmail.com>. + * All Rights Reserved. Apache Software License 2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +!function(t){"use strict";function e(){console.log.apply(console,arguments)}function s(t,e){var s,n,i,o;for(this.list=t,this.options=e=e||{},s=0,o=["sort","shouldSort","verbose","tokenize"],n=o.length;n>s;s++)i=o[s],this.options[i]=i in e?e[i]:h[i];for(s=0,o=["searchFn","sortFn","keys","getFn","include"],n=o.length;n>s;s++)i=o[s],this.options[i]=e[i]||h[i]}function n(t,e,s){var o,r,h,a,c,p;if(e){if(h=e.indexOf("."),-1!==h?(o=e.slice(0,h),r=e.slice(h+1)):o=e,a=t[o],null!==a&&void 0!==a)if(r||"string"!=typeof a&&"number"!=typeof a)if(i(a))for(c=0,p=a.length;p>c;c++)n(a[c],r,s);else r&&n(a,r,s);else s.push(a)}else s.push(t);return s}function i(t){return"[object Array]"===Object.prototype.toString.call(t)}function o(t,e){e=e||{},this.options=e,this.options.location=e.location||o.defaultOptions.location,this.options.distance="distance"in e?e.distance:o.defaultOptions.distance,this.options.threshold="threshold"in e?e.threshold:o.defaultOptions.threshold,this.options.maxPatternLength=e.maxPatternLength||o.defaultOptions.maxPatternLength,this.pattern=e.caseSensitive?t:t.toLowerCase(),this.patternLen=t.length,this.patternLen<=this.options.maxPatternLength&&(this.matchmask=1<<this.patternLen-1,this.patternAlphabet=this._calculatePatternAlphabet())}var r=/ +/g,h={id:null,caseSensitive:!1,include:[],shouldSort:!0,searchFn:o,sortFn:function(t,e){return t.score-e.score},getFn:n,keys:[],verbose:!1,tokenize:!1};s.VERSION="2.2.0-beta",s.prototype.set=function(t){return this.list=t,t},s.prototype.search=function(t){this.options.verbose&&e("\nSearch term:",t,"\n"),this.pattern=t,this.results=[],this.resultMap={},this._keyMap=null,this._prepareSearchers(),this._startSearch(),this._computeScore(),this._sort();var s=this._format();return s},s.prototype._prepareSearchers=function(){var t=this.options,e=this.pattern,s=t.searchFn,n=e.split(r),i=0,o=n.length;if(this.options.tokenize)for(this.tokenSearchers=[];o>i;i++)this.tokenSearchers.push(new s(n[i],t));this.fullSeacher=new s(e,t)},s.prototype._startSearch=function(){var t,e,s,n,i=this.options,o=i.getFn,r=this.list,h=r.length,a=this.options.keys,c=a.length,p=null;if("string"==typeof r[0])for(s=0;h>s;s++)this._analyze("",r[s],s,s);else for(this._keyMap={},s=0;h>s;s++)for(p=r[s],n=0;c>n;n++){if(t=a[n],"string"!=typeof t){if(e=1-t.weight||1,this._keyMap[t.name]={weight:e},t.weight<=0||t.weight>1)throw new Error("Key weight has to be > 0 and <= 1");t=t.name}else this._keyMap[t]={weight:1};this._analyze(t,o(p,t,[]),p,s)}},s.prototype._analyze=function(t,s,n,o){var h,a,c,p,l,u,f,d,g,m,y,v,b,S,k,_=this.options,M=!1;if(void 0!==s&&null!==s)if(a=[],"string"==typeof s){if(h=s.split(r),_.verbose&&e("---------\nKey:",t),_.verbose&&e("Record:",h),this.options.tokenize){for(c=this.tokenSearchers,p=c.length,S=0;S<this.tokenSearchers.length;S++){for(m=this.tokenSearchers[S],y=[],k=0;k<h.length;k++)v=h[k],b=m.search(v),b.isMatch?(M=!0,y.push(b.score),a.push(b.score)):(y.push(1),a.push(1));_.verbose&&e("Token scores:",y)}for(u=a[0],d=a.length,S=1;d>S;S++)u+=a[S];u/=d,_.verbose&&e("Token score average:",u)}g=this.fullSeacher.search(s),_.verbose&&e("Full text score:",g.score),f=g.score,void 0!==u&&(f=(f+u)/2),_.verbose&&e("Score average:",f),(M||g.isMatch)&&(l=this.resultMap[o],l?l.output.push({key:t,score:f,matchedIndices:g.matchedIndices}):(this.resultMap[o]={item:n,output:[{key:t,score:f,matchedIndices:g.matchedIndices}]},this.results.push(this.resultMap[o])))}else if(i(s))for(S=0;S<s.length;S++)this._analyze(t,s[S],n,o)},s.prototype._computeScore=function(){var t,s,n,i,o,r,h,a,c,p=this._keyMap,l=this.results;for(this.options.verbose&&e("\n\nComputing score:\n"),t=0;t<l.length;t++){for(n=0,i=l[t].output,o=i.length,a=1,s=0;o>s;s++)r=i[s].score,h=p?p[i[s].key].weight:1,c=r*h,1!==h?a=Math.min(a,c):(n+=c,i[s].nScore=c);1===a?l[t].score=n/o:l[t].score=a,this.options.verbose&&e(l[t])}},s.prototype._sort=function(){var t=this.options;t.shouldSort&&(t.verbose&&e("\n\nSorting...."),this.results.sort(t.sortFn))},s.prototype._format=function(){var t,s,n,i,o,r=this.options,h=r.getFn,a=[],c=this.results,p=r.include;for(r.verbose&&e("\n\nOutput:\n\n",c),i=r.id?function(t){c[t].item=h(c[t].item,r.id,[])[0]}:function(){},o=function(t){var e,s,n,i,o,r=c[t];if(p.length>0){if(e={item:r.item},-1!==p.indexOf("matches"))for(n=r.output,e.matches=[],s=0;s<n.length;s++)i=n[s],o={indices:i.matchedIndices},i.key&&(o.key=i.key),e.matches.push(o);-1!==p.indexOf("score")&&(e.score=c[t].score)}else e=r.item;return e},s=0,n=c.length;n>s;s++)i(s),t=o(s),a.push(t);return a},o.defaultOptions={location:0,distance:100,threshold:.6,maxPatternLength:32},o.prototype._calculatePatternAlphabet=function(){var t={},e=0;for(e=0;e<this.patternLen;e++)t[this.pattern.charAt(e)]=0;for(e=0;e<this.patternLen;e++)t[this.pattern.charAt(e)]|=1<<this.pattern.length-e-1;return t},o.prototype._bitapScore=function(t,e){var s=t/this.patternLen,n=Math.abs(this.options.location-e);return this.options.distance?s+n/this.options.distance:n?1:s},o.prototype.search=function(t){var e,s,n,i,o,h,a,c,p,l,u,f,d,g,m,y,v,b,S,k,_,M,L=this.options;if(t=L.caseSensitive?t:t.toLowerCase(),this.pattern===t)return{isMatch:!0,score:0,matchedIndices:[[0,t.length-1]]};if(this.patternLen>L.maxPatternLength){if(v=t.match(new RegExp(this.pattern.replace(r,"|"))),b=!!v)for(k=[],e=0,_=v.length;_>e;e++)M=v[e],k.push([t.indexOf(M),M.length-1]);return{isMatch:b,score:b?.5:1,matchedIndices:k}}for(i=L.location,n=t.length,o=L.threshold,h=t.indexOf(this.pattern,i),S=[],e=0;n>e;e++)S[e]=0;for(-1!=h&&(o=Math.min(this._bitapScore(0,h),o),h=t.lastIndexOf(this.pattern,i+this.patternLen),-1!=h&&(o=Math.min(this._bitapScore(0,h),o))),h=-1,m=1,y=[],p=this.patternLen+n,e=0;e<this.patternLen;e++){for(a=0,c=p;c>a;)this._bitapScore(e,i+c)<=o?a=c:p=c,c=Math.floor((p-a)/2+a);for(p=c,l=Math.max(1,i-c+1),u=Math.min(i+c,n)+this.patternLen,f=Array(u+2),f[u+1]=(1<<e)-1,s=u;s>=l;s--)if(g=this.patternAlphabet[t.charAt(s-1)],g&&(S[s-1]=1),0===e?f[s]=(f[s+1]<<1|1)&g:f[s]=(f[s+1]<<1|1)&g|((d[s+1]|d[s])<<1|1)|d[s+1],f[s]&this.matchmask&&(m=this._bitapScore(e,s-1),o>=m)){if(o=m,h=s-1,y.push(h),!(h>i))break;l=Math.max(1,2*i-h)}if(this._bitapScore(e+1,i)>o)break;d=f}return k=this._getMatchedIndices(S),{isMatch:h>=0,score:0===m?.001:m,matchedIndices:k}},o.prototype._getMatchedIndices=function(t){for(var e,s=[],n=-1,i=-1,o=0,r=r=t.length;r>o;o++)e=t[o],e&&-1===n?n=o:e||-1===n||(i=o-1,s.push([n,i]),n=-1);return t[o-1]&&s.push([n,o-1]),s},"object"==typeof exports?module.exports=s:"function"==typeof define&&define.amd?define(function(){return s}):t.Fuse=s}(this); diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index e5e1c8310edb..1ef9d001ed43 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -117,9 +117,12 @@ white-space: nowrap; overflow: hidden; - &:hover { + &:hover, + &:focus, + &.is-focused { background-color: $dropdown-link-hover-bg; text-decoration: none; + outline: 0; } } } diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb new file mode 100644 index 000000000000..c96b6eeebd5b --- /dev/null +++ b/app/helpers/dropdowns_helper.rb @@ -0,0 +1,47 @@ +module DropdownsHelper + def dropdown_tag(toggle_text, title: false, filter: false, placeholder: "", &block) + content_tag :div, class: "dropdown" do + dropdown_output = "" + dropdown_output += content_tag :button, class: "dropdown-menu-toggle", type: "button", data: {toggle: "dropdown"} do + output = toggle_text + output << icon('chevron-down') + output.html_safe + end + + dropdown_output += content_tag :div, class: "dropdown-menu dropdown-select dropdown-menu-selectable" do + output = "" + + if title + output += content_tag :div, class: "dropdown-title" do + title_output = content_tag(:span, title) + + title_output += content_tag :button, class: "dropdown-title-button dropdown-menu-close", aria: {label: "close"} do + icon('times') + end.html_safe + end + end + + if filter + output += content_tag :div, class: "dropdown-input" do + filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder + filter_output += icon('search') + + filter_output.html_safe + end + end + + output += content_tag :div, class: "dropdown-content" do + capture(&block) if block + end + + output += content_tag :div, class: "dropdown-loading" do + icon('spinner spin') + end + + output.html_safe + end + + dropdown_output.html_safe + end + end +end diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 0123834bc313..db46e51f8d72 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -390,6 +390,51 @@ %button.btn.btn-primary Create + .example + %div + .dropdown.inline + %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} + Projects + = icon('chevron-down') + .dropdown-menu.dropdown-select.dropdown-menu-selectable + .dropdown-title + %span Go to project + %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + = icon('times') + .dropdown-input + %input.dropdown-input-field{type: "search", placeholder: "Filter results"} + = icon('search') + .dropdown-content + .dropdown-loading + = icon('spinner spin') + :javascript + $('#js-project-dropdown').glDropdown({ + data: function (callback) { + Api.projects("", "last_activity_at", function (data) { + callback(data); + }); + }, + text: function (project) { + return project.name_with_namespace || project.name; + }, + selectable: true, + fieldName: "author_id", + filterable: true, + search: { + fields: ['name_with_namespace'] + }, + id: function (data) { + return data.id; + }, + isSelected: function (data) { + return data.id === 2; + } + }) + + .example + %div + = dropdown_tag("Projects", title: "Go to project", filter: true, placeholder: "Filter projects") + %h2#panels Panels .row -- GitLab From 7b1581eb451705802a8d1ec57ed461643c9b584a Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 7 Mar 2016 15:41:05 +0000 Subject: [PATCH 109/223] Custom ID & toggle class on dropdown --- app/helpers/dropdowns_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index c96b6eeebd5b..a5728968ae10 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -1,8 +1,8 @@ module DropdownsHelper - def dropdown_tag(toggle_text, title: false, filter: false, placeholder: "", &block) + def dropdown_tag(toggle_text, id: nil, toggle_class: nil, title: false, filter: false, placeholder: "", &block) content_tag :div, class: "dropdown" do dropdown_output = "" - dropdown_output += content_tag :button, class: "dropdown-menu-toggle", type: "button", data: {toggle: "dropdown"} do + dropdown_output += content_tag :button, class: "dropdown-menu-toggle #{toggle_class}", id: id, type: "button", data: {toggle: "dropdown"} do output = toggle_text output << icon('chevron-down') output.html_safe -- GitLab From 66785cc8a3ca3b79115455ee866cf1b895ac3d6b Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 7 Mar 2016 17:17:11 +0000 Subject: [PATCH 110/223] Applied new dropdowns to issue filters --- app/assets/javascripts/api.js.coffee | 13 ++++++ app/assets/javascripts/gl_dropdown.js.coffee | 7 ++-- .../javascripts/labels_select.js.coffee | 25 +++++++++++ app/assets/javascripts/users_select.js.coffee | 41 +++++++++++++++++++ .../stylesheets/framework/dropdowns.scss | 11 +++++ app/helpers/dropdowns_helper.rb | 8 ++-- app/views/shared/issuable/_filter.html.haml | 24 ++++++----- 7 files changed, 113 insertions(+), 16 deletions(-) create mode 100644 app/assets/javascripts/labels_select.js.coffee diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index 3e0fdb3f795a..466b086259a8 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -4,6 +4,7 @@ namespaces_path: "/api/:version/namespaces.json" group_projects_path: "/api/:version/groups/:id/projects.json" projects_path: "/api/:version/projects.json" + labels_path: "/api/:version/projects/:id/labels" group: (group_id, callback) -> url = Api.buildUrl(Api.group_path) @@ -61,6 +62,18 @@ ).done (projects) -> callback(projects) + projectLabels: (project_id, callback) -> + url = Api.buildUrl(Api.labels_path) + url = url.replace(':id', project_id) + + $.ajax( + url: url + data: + private_token: gon.api_token + dataType: "json" + ).done (labels) -> + callback(labels) + # Return group projects list. Filtered by query groupProjects: (group_id, query, callback) -> url = Api.buildUrl(Api.group_projects_path) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index f15d65473c62..10b8fd07a0c2 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -185,9 +185,10 @@ class GitLabDropdown # Toggle active class for the tick mark el.toggleClass "is-active" - # Create hidden input for form - input = "<input type='hidden' name='#{fieldName}' value='#{value}' />" - @dropdown.before input + if value isnt 0 + # Create hidden input for form + input = "<input type='hidden' name='#{fieldName}' value='#{value}' />" + @dropdown.before input $.fn.glDropdown = (opts) -> return @.each -> diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee new file mode 100644 index 000000000000..c240af0b0481 --- /dev/null +++ b/app/assets/javascripts/labels_select.js.coffee @@ -0,0 +1,25 @@ +class @LabelsSelect + constructor: -> + $('.js-label-select').each (i, dropdown) -> + projectId = $(dropdown).data('project-id') + + $(dropdown).glDropdown( + data: (callback) -> + Api.projectLabels 8, callback + renderRow: (label) -> + "<li> + <a href='#'> + <span class='label' style='background-color: #{label.color}'>#{label.name}</span> + #{label.name} + </a> + </li>" + filterable: true + search: + fields: ['name'] + selectable: true + fieldName: $(dropdown).data('field-name') + id: (label) -> + label.name + clicked: -> + $(dropdown).parents('form').submit() + ) diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 9467011799fd..2b5c2f43818f 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -3,6 +3,47 @@ class @UsersSelect @usersPath = "/autocomplete/users.json" @userPath = "/autocomplete/users/:id.json" + $('.js-user-search').each (i, dropdown) => + projectId = $(dropdown).data('project-id') + showNullUser = $(dropdown).data('null-user') + selectedId = $(dropdown).data('selected') + + $(dropdown).glDropdown( + data: (callback) => + @users "", (users) => + if showNullUser + users.unshift( + name: 'Unassigned', + id: 0 + ) + + # Send the data back + callback users + filterable: true + search: + fields: ['name', 'username'] + selectable: true + fieldName: $(dropdown).data('field-name') + clicked: -> + $(dropdown).parents('form').submit() + renderRow: (user) -> + username = if user.username then "@#{user.username}" else "" + avatar = if user.avatar_url then user.avatar_url else gon.default_avatar_url + selected = if user.id is selectedId then "is-active" else "" + + "<li> + <a href='#' class='dropdown-menu-user-link #{selected}'> + <img src='#{avatar}' class='avatar avatar-inline' width='30' /> + <strong class='dropdown-menu-user-full-name'> + #{user.name} + </strong> + <span class='dropdown-menu-user-username'> + #{username} + </span> + </a> + </li>" + ) + $('.ajax-users-select').each (i, select) => @projectId = $(select).data('project-id') @groupId = $(select).data('group-id') diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 1ef9d001ed43..770534061b29 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -320,3 +320,14 @@ margin-left: -14px; } } + +.dropdown-menu-labels { + .label { + position: relative; + top: 5px; + width: 30px; + margin-right: 5px; + text-indent: -99999px; + overflow: hidden; + } +} diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index a5728968ae10..49c40e311763 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -1,14 +1,16 @@ module DropdownsHelper - def dropdown_tag(toggle_text, id: nil, toggle_class: nil, title: false, filter: false, placeholder: "", &block) + def dropdown_tag(toggle_text, id: nil, toggle_class: nil, dropdown_class: nil, title: false, filter: false, placeholder: "", data: {}, &block) content_tag :div, class: "dropdown" do + toggle_hash = data.merge({toggle: "dropdown"}) + dropdown_output = "" - dropdown_output += content_tag :button, class: "dropdown-menu-toggle #{toggle_class}", id: id, type: "button", data: {toggle: "dropdown"} do + dropdown_output += content_tag :button, class: "dropdown-menu-toggle #{toggle_class}", id: id, type: "button", data: toggle_hash do output = toggle_text output << icon('chevron-down') output.html_safe end - dropdown_output += content_tag :div, class: "dropdown-menu dropdown-select dropdown-menu-selectable" do + dropdown_output += content_tag :div, class: "dropdown-menu dropdown-select #{dropdown_class}" do output = "" if title diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index e55159d996b0..2be1ee1f7274 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -7,22 +7,25 @@ class: "check_all_issues left" .issues-other-filters .filter-item.inline - = users_select_tag(:author_id, selected: params[:author_id], - placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true) + = dropdown_tag("Author", toggle_class: "js-user-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", + placeholder: "Search authors", data: {any_user: "Any Author", first_user: true, current_user: true, project_id: @project.id, selected: params[:author_id], field_name: "author_id"}) .filter-item.inline - = users_select_tag(:assignee_id, selected: params[:assignee_id], - placeholder: 'Assignee', class: 'trigger-submit', any_user: "Any Assignee", null_user: true, first_user: true, current_user: true) + = dropdown_tag("Assignee", toggle_class: "js-user-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", + placeholder: "Search assignee", data: {any_user: "Any Author", first_user: true, null_user: true, current_user: true, project_id: @project.id, selected: params[:assignee_id], field_name: "assignee_id"}) .filter-item.inline.milestone-filter - = select_tag('milestone_title', projects_milestones_options, - class: 'select2 trigger-submit', include_blank: true, - data: {placeholder: 'Milestone'}) + = dropdown_tag("Milestone", title: "Filter by milestone", filter: true, dropdown_class: "dropdown-menu-selectable", + placeholder: "Search milestones", data: {field_name: "milestone_title"}) do + %ul + - @project.milestones.each do |milestone| + %li + %a{href: "#", data: {id: milestone.id}} + = milestone.name .filter-item.inline.labels-filter - = select_tag('label_name', projects_labels_options, - class: 'select2 trigger-submit', include_blank: true, - data: {placeholder: 'Label'}) + = dropdown_tag("Label", title: "Filter by label", toggle_class: "js-label-select", filter: true, dropdown_class: "dropdown-menu-labels dropdown-menu-selectable", + placeholder: "Search labels", data: {field_name: "label_name", selected: params[:label_name], project_id: @project.id}) .pull-right = render 'shared/sort_dropdown' @@ -47,6 +50,7 @@ :javascript new UsersSelect(); + new LabelsSelect(); $('form.filter-form').on('submit', function (event) { event.preventDefault(); Turbolinks.visit(this.action + '&' + $(this).serialize()); -- GitLab From c137a871a9712848e881e41c7b9d0ba92da4612d Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 7 Mar 2016 17:18:53 +0000 Subject: [PATCH 111/223] Fixed alignment of labels in dropdowns --- app/assets/stylesheets/framework/dropdowns.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 770534061b29..357423709bf3 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -324,10 +324,8 @@ .dropdown-menu-labels { .label { position: relative; - top: 5px; width: 30px; margin-right: 5px; text-indent: -99999px; - overflow: hidden; } } -- GitLab From f8e2518ef3936c6f5a13d822ac2113ed93575b7f Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 7 Mar 2016 17:19:36 +0000 Subject: [PATCH 112/223] Removed label colour from dropdown --- app/assets/javascripts/labels_select.js.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index c240af0b0481..817367136405 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -9,7 +9,6 @@ class @LabelsSelect renderRow: (label) -> "<li> <a href='#'> - <span class='label' style='background-color: #{label.color}'>#{label.name}</span> #{label.name} </a> </li>" -- GitLab From f4694bbf4047b0dcf49d6520562b0bb7142e28c9 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 7 Mar 2016 17:28:37 +0000 Subject: [PATCH 113/223] Extra links in label filter dropdown fotoer --- app/assets/stylesheets/framework/dropdowns.scss | 8 ++++++++ app/helpers/dropdowns_helper.rb | 10 ++++++++-- app/views/shared/issuable/_filter.html.haml | 6 +++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 357423709bf3..ba65a8b17c68 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -301,6 +301,14 @@ border-top: 1px solid $dropdown-divider-color; } +.dropdown-footer-list { + font-size: 14px; + + a { + padding-left: 10px; + } +} + .dropdown-loading { position: absolute; top: 0; diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 49c40e311763..5e3ee734314d 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -1,5 +1,5 @@ module DropdownsHelper - def dropdown_tag(toggle_text, id: nil, toggle_class: nil, dropdown_class: nil, title: false, filter: false, placeholder: "", data: {}, &block) + def dropdown_tag(toggle_text, id: nil, toggle_class: nil, dropdown_class: nil, title: false, filter: false, placeholder: "", footer_content: false, data: {}, &block) content_tag :div, class: "dropdown" do toggle_hash = data.merge({toggle: "dropdown"}) @@ -33,7 +33,13 @@ def dropdown_tag(toggle_text, id: nil, toggle_class: nil, dropdown_class: nil, t end output += content_tag :div, class: "dropdown-content" do - capture(&block) if block + capture(&block) if block && !footer_content + end + + if block && footer_content + output += content_tag :div, class: "dropdown-footer" do + capture(&block) + end end output += content_tag :div, class: "dropdown-loading" do diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 2be1ee1f7274..18d3f0e50659 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -25,7 +25,11 @@ .filter-item.inline.labels-filter = dropdown_tag("Label", title: "Filter by label", toggle_class: "js-label-select", filter: true, dropdown_class: "dropdown-menu-labels dropdown-menu-selectable", - placeholder: "Search labels", data: {field_name: "label_name", selected: params[:label_name], project_id: @project.id}) + placeholder: "Search labels", footer_content: true, data: {field_name: "label_name", selected: params[:label_name], project_id: @project.id}) do + %ul.dropdown-footer-list + %li + = link_to namespace_project_labels_path(@project.namespace, @project) do + Manage labels .pull-right = render 'shared/sort_dropdown' -- GitLab From 179a05450632d1d15853f2b061aedfd04722c7f8 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 8 Mar 2016 08:38:16 +0000 Subject: [PATCH 114/223] milestone dropdown --- app/assets/javascripts/api.js.coffee | 13 +++++++++++ app/assets/javascripts/gl_dropdown.js.coffee | 2 +- .../javascripts/labels_select.js.coffee | 5 +++- .../javascripts/milestone_select.js.coffee | 23 +++++++++++++++++++ app/views/shared/issuable/_filter.html.haml | 10 +++----- 5 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 app/assets/javascripts/milestone_select.js.coffee diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index 466b086259a8..987815b6bda6 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -5,6 +5,7 @@ group_projects_path: "/api/:version/groups/:id/projects.json" projects_path: "/api/:version/projects.json" labels_path: "/api/:version/projects/:id/labels" + milestones_path: "/api/:version/projects/:id/milestones" group: (group_id, callback) -> url = Api.buildUrl(Api.group_path) @@ -74,6 +75,18 @@ ).done (labels) -> callback(labels) + milestones: (project_id, callback) -> + url = Api.buildUrl(Api.milestones_path) + url = url.replace(':id', project_id) + + $.ajax( + url: url + data: + private_token: gon.api_token + dataType: "json" + ).done (milestones) -> + callback(milestones) + # Return group projects list. Filtered by query groupProjects: (group_id, query, callback) -> url = Api.buildUrl(Api.group_projects_path) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 10b8fd07a0c2..8ab82dc25200 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -148,7 +148,7 @@ class GitLabDropdown html = @options.renderRow(data) else selected = if @options.isSelected then @options.isSelected(data) else false - url = if @options.url then @options.url(data) else "" + url = if @options.url then @options.url(data) else "#" text = if @options.text then @options.text(data) else "" cssClass = ""; diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 817367136405..f79a8773533c 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -2,13 +2,16 @@ class @LabelsSelect constructor: -> $('.js-label-select').each (i, dropdown) -> projectId = $(dropdown).data('project-id') + selectedLabel = $(dropdown).data('selected') $(dropdown).glDropdown( data: (callback) -> Api.projectLabels 8, callback renderRow: (label) -> + selected = if label.name is selectedLabel then "is-active" else "" + "<li> - <a href='#'> + <a href='#' class='#{selected}'> #{label.name} </a> </li>" diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee new file mode 100644 index 000000000000..184479e209c3 --- /dev/null +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -0,0 +1,23 @@ +class @MilestoneSelect + constructor: -> + $('.js-milestone-select').each (i, dropdown) -> + projectId = $(dropdown).data('project-id') + selectedMilestone = $(dropdown).data('selected') + + $(dropdown).glDropdown( + data: (callback) -> + Api.milestones projectId, callback + filterable: true + search: + fields: ['name'] + selectable: true + fieldName: $(dropdown).data('field-name') + text: (milestone) -> + milestone.title + id: (milestone) -> + milestone.title + isSelected: (milestone) -> + milestone.title is selectedMilestone + clicked: -> + $(dropdown).parents('form').submit() + ) diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 18d3f0e50659..402fc7535ba7 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -15,13 +15,8 @@ placeholder: "Search assignee", data: {any_user: "Any Author", first_user: true, null_user: true, current_user: true, project_id: @project.id, selected: params[:assignee_id], field_name: "assignee_id"}) .filter-item.inline.milestone-filter - = dropdown_tag("Milestone", title: "Filter by milestone", filter: true, dropdown_class: "dropdown-menu-selectable", - placeholder: "Search milestones", data: {field_name: "milestone_title"}) do - %ul - - @project.milestones.each do |milestone| - %li - %a{href: "#", data: {id: milestone.id}} - = milestone.name + = dropdown_tag("Milestone", title: "Filter by milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable", + placeholder: "Search milestones", data: {field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.id}) .filter-item.inline.labels-filter = dropdown_tag("Label", title: "Filter by label", toggle_class: "js-label-select", filter: true, dropdown_class: "dropdown-menu-labels dropdown-menu-selectable", @@ -55,6 +50,7 @@ :javascript new UsersSelect(); new LabelsSelect(); + new MilestoneSelect(); $('form.filter-form').on('submit', function (event) { event.preventDefault(); Turbolinks.visit(this.action + '&' + $(this).serialize()); -- GitLab From 3e815dab56a7c9108960547602ede619a9d2fa5b Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 8 Mar 2016 09:09:39 +0000 Subject: [PATCH 115/223] Correctly fitlers remotely --- app/assets/javascripts/gl_dropdown.js.coffee | 36 ++++++++++++------- .../javascripts/labels_select.js.coffee | 2 +- .../javascripts/milestone_select.js.coffee | 4 +-- app/assets/javascripts/users_select.js.coffee | 35 ++++++++++++++---- app/helpers/dropdowns_helper.rb | 2 +- app/views/shared/issuable/_filter.html.haml | 8 +++++ 6 files changed, 64 insertions(+), 23 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 8ab82dc25200..c00facd921d7 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -1,22 +1,26 @@ class GitLabDropdownFilter BLUR_KEYCODES = [27, 40] - constructor: (@dropdown, @remote, @data, @callback) -> + constructor: (@dropdown, @remote, @query, @data, @callback) -> @input = @dropdown.find(".dropdown-input-field") # Key events + timeout = "" @input.on "keyup", (e) => - blur_field = @shouldBlur e.keyCode - search_text = @input.val() + clearTimeout timeout + timeout = setTimeout => + blur_field = @shouldBlur e.keyCode + search_text = @input.val() - if blur_field - @input.blur() + if blur_field + @input.blur() - if @remote - @remote search_text, (data) => - @callback(data) - else - @filter search_text + if @remote + @query search_text, (data) => + @callback(data) + else + @filter search_text + , 250 shouldBlur: (keyCode) -> return BLUR_KEYCODES.indexOf(keyCode) >= 0 @@ -38,7 +42,7 @@ class GitLabDropdownRemote @options.beforeSend() # Fetch the data by calling the data funcfion - @dataEndpoint (data) => + @dataEndpoint "", (data) => if @options.success @options.success(data) @@ -86,13 +90,14 @@ class GitLabDropdown # Init filiterable if @options.filterable - @filter = new GitLabDropdownFilter @dropdown, @options.query, => + @filter = new GitLabDropdownFilter @dropdown, @options.filterRemote, @options.data, => return @fullData , (data) => @parseData data # Event listeners $(@el).parent().on "shown.bs.dropdown", @opened + $(@el).parent().on "hidden.bs.dropdown", @hidden if @options.selectable @dropdown.on "click", "a", (e) -> @@ -124,6 +129,13 @@ class GitLabDropdown if @remote @remote.execute() + if @options.filterable + @dropdown.find(".dropdown-input-field").focus() + + hidden: => + if @options.filterable + @dropdown.find(".dropdown-input-field").blur().val("") + # Render the full menu renderMenu: (html) -> menu_html = "" diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index f79a8773533c..56fe6dc9a7af 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -5,7 +5,7 @@ class @LabelsSelect selectedLabel = $(dropdown).data('selected') $(dropdown).glDropdown( - data: (callback) -> + data: (term, callback) -> Api.projectLabels 8, callback renderRow: (label) -> selected = if label.name is selectedLabel then "is-active" else "" diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 184479e209c3..4256158660a4 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -5,11 +5,11 @@ class @MilestoneSelect selectedMilestone = $(dropdown).data('selected') $(dropdown).glDropdown( - data: (callback) -> + data: (term, callback) -> Api.milestones projectId, callback filterable: true search: - fields: ['name'] + fields: ['title'] selectable: true fieldName: $(dropdown).data('field-name') text: (milestone) -> diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 2b5c2f43818f..77db99a617f5 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -6,20 +6,41 @@ class @UsersSelect $('.js-user-search').each (i, dropdown) => projectId = $(dropdown).data('project-id') showNullUser = $(dropdown).data('null-user') + showAnyUser = $(dropdown).data('any-user') + firstUser = $(dropdown).data('first-user') selectedId = $(dropdown).data('selected') $(dropdown).glDropdown( - data: (callback) => - @users "", (users) => - if showNullUser - users.unshift( - name: 'Unassigned', - id: 0 - ) + data: (term, callback) => + @users term, (users) => + if term.length is 0 + if firstUser + # Move current user to the front of the list + for obj, index in users + if obj.username == firstUser + users.splice(index, 1) + users.unshift(obj) + break + + if showNullUser + users.unshift( + name: 'Unassigned', + id: 0 + ) + + if showAnyUser + name = showAnyUser + name = 'Any User' if name == true + anyUser = { + name: name, + id: null + } + users.unshift(anyUser) # Send the data back callback users filterable: true + filterRemote: true search: fields: ['name', 'username'] selectable: true diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 5e3ee734314d..73b3e70f3ce1 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -17,7 +17,7 @@ def dropdown_tag(toggle_text, id: nil, toggle_class: nil, dropdown_class: nil, t output += content_tag :div, class: "dropdown-title" do title_output = content_tag(:span, title) - title_output += content_tag :button, class: "dropdown-title-button dropdown-menu-close", aria: {label: "close"} do + title_output += content_tag :button, class: "dropdown-title-button dropdown-menu-close", aria: {label: "close"}, type: "button" do icon('times') end.html_safe end diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 402fc7535ba7..0a8ec7cde5bc 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -7,18 +7,26 @@ class: "check_all_issues left" .issues-other-filters .filter-item.inline + - if params[:author_id] + = hidden_field_tag(:author_id, params[:author_id]) = dropdown_tag("Author", toggle_class: "js-user-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", placeholder: "Search authors", data: {any_user: "Any Author", first_user: true, current_user: true, project_id: @project.id, selected: params[:author_id], field_name: "author_id"}) .filter-item.inline + - if params[:assignee_id] + = hidden_field_tag(:assignee_id, params[:assignee_id]) = dropdown_tag("Assignee", toggle_class: "js-user-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", placeholder: "Search assignee", data: {any_user: "Any Author", first_user: true, null_user: true, current_user: true, project_id: @project.id, selected: params[:assignee_id], field_name: "assignee_id"}) .filter-item.inline.milestone-filter + - if params[:milestone_title] + = hidden_field_tag(:milestone_title, params[:milestone_title]) = dropdown_tag("Milestone", title: "Filter by milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search milestones", data: {field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.id}) .filter-item.inline.labels-filter + - if params[:label_name] + = hidden_field_tag(:label_name, params[:label_name]) = dropdown_tag("Label", title: "Filter by label", toggle_class: "js-label-select", filter: true, dropdown_class: "dropdown-menu-labels dropdown-menu-selectable", placeholder: "Search labels", footer_content: true, data: {field_name: "label_name", selected: params[:label_name], project_id: @project.id}) do %ul.dropdown-footer-list -- GitLab From 0434b825423178d648b2001952a57ef7ac47c911 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 8 Mar 2016 10:24:03 +0000 Subject: [PATCH 116/223] Started page two for labels dropdown to allow creating new labels --- app/assets/javascripts/gl_dropdown.js.coffee | 42 ++++++++++-- .../javascripts/milestone_select.js.coffee | 26 +++++++- app/assets/javascripts/users_select.js.coffee | 15 ++++- .../stylesheets/framework/dropdowns.scss | 9 ++- app/assets/stylesheets/pages/labels.scss | 11 ++++ app/helpers/dropdowns_helper.rb | 2 +- app/views/shared/issuable/_filter.html.haml | 64 +++++++++++++++++-- 7 files changed, 149 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index c00facd921d7..18b90d32c96b 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -64,6 +64,7 @@ class GitLabDropdownRemote class GitLabDropdown LOADING_CLASS = "is-loading" + PAGE_TWO_CLASS = "is-page-two" constructor: (@el, @options) -> self = @ @@ -96,11 +97,23 @@ class GitLabDropdown @parseData data # Event listeners - $(@el).parent().on "shown.bs.dropdown", @opened - $(@el).parent().on "hidden.bs.dropdown", @hidden + @dropdown.on "shown.bs.dropdown", @opened + @dropdown.on "hidden.bs.dropdown", @hidden + + if @dropdown.find(".dropdown-toggle-page").length + @dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) => + e.preventDefault() + e.stopPropagation() + + @togglePage() if @options.selectable - @dropdown.on "click", "a", (e) -> + selector = "a" + + if @dropdown.find(".dropdown-toggle-page").length + selector = ".dropdown-page-one a" + + @dropdown.on "click", selector, (e) -> self.rowClicked $(@) if self.options.clicked @@ -109,6 +122,15 @@ class GitLabDropdown toggleLoading: -> $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS + togglePage: -> + menu = $('.dropdown-menu', @dropdown) + + if menu.hasClass(PAGE_TWO_CLASS) + if @remote + @remote.execute() + + menu.toggleClass PAGE_TWO_CLASS + parseData: (data) -> @renderedData = data @@ -136,6 +158,10 @@ class GitLabDropdown if @options.filterable @dropdown.find(".dropdown-input-field").blur().val("") + if @dropdown.find(".dropdown-toggle-page").length + $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS + + # Render the full menu renderMenu: (html) -> menu_html = "" @@ -149,12 +175,18 @@ class GitLabDropdown # Append the menu into the dropdown appendMenu: (html) -> - $('.dropdown-content', @dropdown).html html + selector = '.dropdown-content' + if @dropdown.find(".dropdown-toggle-page").length + selector = ".dropdown-page-one .dropdown-content" + + $(selector, @dropdown).html html # Render the row renderItem: (data) -> html = "" + return "<li class='divider'></li>" if data is "divider" + if @options.renderRow # Call the render function html = @options.renderRow(data) @@ -189,7 +221,7 @@ class GitLabDropdown value = if @options.id then @options.id(selectedObject) else selectedObject.id if @options.multiSelect - fieldName = "[#{fieldName}]" + fieldName = "#{fieldName}[]" else @dropdown.find('.is-active').removeClass 'is-active' @dropdown.parent().find("input[name='#{fieldName}']").remove() diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 4256158660a4..aa0d632561f0 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -3,10 +3,29 @@ class @MilestoneSelect $('.js-milestone-select').each (i, dropdown) -> projectId = $(dropdown).data('project-id') selectedMilestone = $(dropdown).data('selected') + showNo = $(dropdown).data('show-no') + showAny = $(dropdown).data('show-any') $(dropdown).glDropdown( data: (term, callback) -> - Api.milestones projectId, callback + Api.milestones projectId, (data) -> + data = $.map data, (milestone) -> + return milestone if milestone.state isnt "closed" + + if showNo + data.unshift( + title: 'No milestone' + ) + + if showAny + data.unshift( + title: 'Any milestone' + ) + + if data.length > 2 + data.splice 2, 0, "divider" + + callback(data) filterable: true search: fields: ['title'] @@ -15,7 +34,10 @@ class @MilestoneSelect text: (milestone) -> milestone.title id: (milestone) -> - milestone.title + if milestone.title isnt "Any milestone" + milestone.title + else + "" isSelected: (milestone) -> milestone.title is selectedMilestone clicked: -> diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 77db99a617f5..d4e41ffa5e04 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -14,6 +14,8 @@ class @UsersSelect data: (term, callback) => @users term, (users) => if term.length is 0 + showDivider = 0 + if firstUser # Move current user to the front of the list for obj, index in users @@ -23,12 +25,14 @@ class @UsersSelect break if showNullUser + showDivider += 1 users.unshift( name: 'Unassigned', id: 0 ) if showAnyUser + showDivider += 1 name = showAnyUser name = 'Any User' if name == true anyUser = { @@ -37,6 +41,9 @@ class @UsersSelect } users.unshift(anyUser) + if showDivider + users.splice(showDivider, 0, "divider") + # Send the data back callback users filterable: true @@ -49,12 +56,16 @@ class @UsersSelect $(dropdown).parents('form').submit() renderRow: (user) -> username = if user.username then "@#{user.username}" else "" - avatar = if user.avatar_url then user.avatar_url else gon.default_avatar_url + avatar = if user.avatar_url then user.avatar_url else false selected = if user.id is selectedId then "is-active" else "" + img = "" + + if avatar + img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />" "<li> <a href='#' class='dropdown-menu-user-link #{selected}'> - <img src='#{avatar}' class='avatar avatar-inline' width='30' /> + #{img} <strong class='dropdown-menu-user-full-name'> #{user.name} </strong> diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index ba65a8b17c68..27db075e25df 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -29,7 +29,7 @@ .dropdown-menu-toggle { position: relative; - min-width: 160px; + width: 160px; padding: 5px 20px 5px 10px; background-color: $dropdown-toggle-bg; color: $dropdown-toggle-color; @@ -128,7 +128,8 @@ } .dropdown-menu-paging { - .dropdown-page-two { + .dropdown-page-two, + .dropdown-menu-back { display: none; } @@ -137,7 +138,8 @@ display: none; } - .dropdown-page-two { + .dropdown-page-two, + .dropdown-menu-back { display: block; } } @@ -160,6 +162,7 @@ .dropdown-menu-user-full-name { display: block; margin-bottom: 2px; + font-weight: 600; line-height: 1; text-overflow: ellipsis; overflow: hidden; diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 1c78aafdb873..8c49f3a92384 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -7,6 +7,17 @@ display: inline-block; margin-right: 10px; } + + &.suggest-colors-dropdown { + margin-bottom: 5px; + + a { + @include border-radius(0); + width: 36.7px; + margin-right: 0; + margin-bottom: -5px; + } + } } .label-row { diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 73b3e70f3ce1..73a8794c25f5 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -5,7 +5,7 @@ def dropdown_tag(toggle_text, id: nil, toggle_class: nil, dropdown_class: nil, t dropdown_output = "" dropdown_output += content_tag :button, class: "dropdown-menu-toggle #{toggle_class}", id: id, type: "button", data: toggle_hash do - output = toggle_text + output = content_tag(:span, toggle_text, class: "dropdown-toggle-text") output << icon('chevron-down') output.html_safe end diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 0a8ec7cde5bc..a336a540eb09 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -22,17 +22,67 @@ - if params[:milestone_title] = hidden_field_tag(:milestone_title, params[:milestone_title]) = dropdown_tag("Milestone", title: "Filter by milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable", - placeholder: "Search milestones", data: {field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.id}) + placeholder: "Search milestones", footer_content: true, data: {show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.id}) do + %ul.dropdown-footer-list + - if can? current_user, :admin_milestone, @project + %li + = link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do + Create new + %li + = link_to namespace_project_milestones_path(@project.namespace, @project) do + - if can? current_user, :admin_milestone, @project + Manage milestones + - else + View milestones .filter-item.inline.labels-filter - if params[:label_name] = hidden_field_tag(:label_name, params[:label_name]) - = dropdown_tag("Label", title: "Filter by label", toggle_class: "js-label-select", filter: true, dropdown_class: "dropdown-menu-labels dropdown-menu-selectable", - placeholder: "Search labels", footer_content: true, data: {field_name: "label_name", selected: params[:label_name], project_id: @project.id}) do - %ul.dropdown-footer-list - %li - = link_to namespace_project_labels_path(@project.namespace, @project) do - Manage labels + .dropdown + %button.dropdown-menu-toggle.js-label-select{type: "button", data: {toggle: "dropdown", field_name: "label_name", selected: params[:label_name], project_id: @project.id}} + %span.dropdown-toggle-text + Label + = icon('chevron-down') + .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable + .dropdown-page-one + .dropdown-title + %span + Filter by label + %button.dropdown-title-button.dropdown-menu-close{type: "button", aria: {label: "close"}} + = icon('times') + .dropdown-input + = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: "Search labels" + = icon('search') + .dropdown-content + .dropdown-footer + %ul.dropdown-footer-list + - if can? current_user, :admin_label, @project + %li + %a.dropdown-toggle-page{href: "#"} + Create new + %li + = link_to namespace_project_labels_path(@project.namespace, @project) do + - if can? current_user, :admin_label, @project + Manage labels + - else + View labels + - if can? current_user, :admin_label, @project + .dropdown-page-two + .dropdown-title + %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}} + = icon('arrow-left') + %span + Create new label + .dropdown-content + = text_field_tag :label_name, nil, class: "dropdown-input-field", placeholder: "Name new label" + .suggest-colors.suggest-colors-dropdown + - suggested_colors.each do |color| + = link_to '#', style: "background-color: #{color}", data: { color: color } do +   + %button.btn.btn-primary{type: "button"} + Create + .dropdown-loading + = icon('spinner spin') .pull-right = render 'shared/sort_dropdown' -- GitLab From 98a4e6aea717145a787609c5e483d094c4d037e2 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 8 Mar 2016 11:23:54 +0000 Subject: [PATCH 117/223] Can create new label from dropdown --- app/assets/javascripts/api.js.coffee | 14 ++++++++++ app/assets/javascripts/gl_dropdown.js.coffee | 6 ++--- .../javascripts/labels_select.js.coffee | 26 +++++++++++++++++++ .../stylesheets/framework/dropdowns.scss | 1 + app/assets/stylesheets/pages/labels.scss | 13 +++++++++- app/views/shared/issuable/_filter.html.haml | 8 ++++-- 6 files changed, 62 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index 987815b6bda6..36c6a76e09c2 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -75,6 +75,20 @@ ).done (labels) -> callback(labels) + newLabel: (project_id, data, callback) -> + url = Api.buildUrl(Api.labels_path) + url = url.replace(':id', project_id) + + data.private_token = gon.api_token + $.ajax( + url: url + type: "POST" + data: data + dataType: "json" + ).done (label) -> + callback(label) + + milestones: (project_id, callback) -> url = Api.buildUrl(Api.milestones_path) url = url.replace(':id', project_id) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 18b90d32c96b..2d69280c9e58 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -2,7 +2,7 @@ class GitLabDropdownFilter BLUR_KEYCODES = [27, 40] constructor: (@dropdown, @remote, @query, @data, @callback) -> - @input = @dropdown.find(".dropdown-input-field") + @input = @dropdown.find(".dropdown-input .dropdown-input-field") # Key events timeout = "" @@ -108,10 +108,10 @@ class GitLabDropdown @togglePage() if @options.selectable - selector = "a" + selector = ".dropdown-content a" if @dropdown.find(".dropdown-toggle-page").length - selector = ".dropdown-page-one a" + selector = ".dropdown-page-one .dropdown-content a" @dropdown.on "click", selector, (e) -> self.rowClicked $(@) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 56fe6dc9a7af..5582bf457e38 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -3,6 +3,32 @@ class @LabelsSelect $('.js-label-select').each (i, dropdown) -> projectId = $(dropdown).data('project-id') selectedLabel = $(dropdown).data('selected') + newLabelField = $('#new_label_name') + newColorField = $('#new_label_color') + + if newLabelField.length + $('.suggest-colors-dropdown a').on "click", (e) -> + e.preventDefault() + e.stopPropagation() + newColorField.val $(this).data("color") + $('.js-dropdown-label-color-preview') + .css 'background-color', $(this).data("color") + .addClass 'is-active' + + $('.js-new-label-btn').on "click", (e) -> + e.preventDefault() + e.stopPropagation() + + if newLabelField.val() isnt "" && newColorField.val() isnt "" + $('.js-new-label-btn').disable() + + # Create new label with API + Api.newLabel projectId, { + name: newLabelField.val() + color: newColorField.val() + }, (label) -> + $('.js-new-label-btn').enable() + $('.dropdown-menu-back', $(dropdown).parent()).trigger "click" $(dropdown).glDropdown( data: (term, callback) -> diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 27db075e25df..dce2b3b99cbd 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -238,6 +238,7 @@ font-size: 14px; border: 0; background: none; + outline: 0; &:hover { color: darken($dropdown-title-btn-color, 15%); diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 8c49f3a92384..5ec0966194cf 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -10,7 +10,7 @@ &.suggest-colors-dropdown { margin-bottom: 5px; - + a { @include border-radius(0); width: 36.7px; @@ -20,6 +20,17 @@ } } +.dropdown-label-color-preview { + display: none; + margin-top: 5px; + width: 100%; + height: 25px; + + &.is-active { + display: block; + } +} + .label-row { .label { padding: 9px; diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index a336a540eb09..b9f9fb49233d 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -73,13 +73,17 @@ = icon('arrow-left') %span Create new label + %button.dropdown-title-button.dropdown-menu-close{type: "button", aria: {label: "close"}} + = icon('times') .dropdown-content - = text_field_tag :label_name, nil, class: "dropdown-input-field", placeholder: "Name new label" + = hidden_field_tag :new_label_color, nil + = text_field_tag :new_label_name, nil, class: "dropdown-input-field", placeholder: "Name new label" + .dropdown-label-color-preview.js-dropdown-label-color-preview .suggest-colors.suggest-colors-dropdown - suggested_colors.each do |color| = link_to '#', style: "background-color: #{color}", data: { color: color } do   - %button.btn.btn-primary{type: "button"} + %button.btn.btn-primary.js-new-label-btn{type: "button"} Create .dropdown-loading = icon('spinner spin') -- GitLab From df513eee24abbbedf3fa937062018e9ce25e51ce Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 8 Mar 2016 11:58:14 +0000 Subject: [PATCH 118/223] Status dropdown --- app/assets/javascripts/gl_dropdown.js.coffee | 8 +++--- .../javascripts/issue_status_select.js.coffee | 11 ++++++++ .../javascripts/labels_select.js.coffee | 3 ++- .../javascripts/milestone_select.js.coffee | 5 ++-- app/assets/javascripts/users_select.js.coffee | 3 ++- app/views/shared/issuable/_filter.html.haml | 26 ++++++++++++------- 6 files changed, 40 insertions(+), 16 deletions(-) create mode 100644 app/assets/javascripts/issue_status_select.js.coffee diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 2d69280c9e58..c65b3e31450e 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -217,8 +217,10 @@ class GitLabDropdown rowClicked: (el) -> fieldName = @options.fieldName selectedIndex = el.parent().index() - selectedObject = @renderedData[selectedIndex] - value = if @options.id then @options.id(selectedObject) else selectedObject.id + if @renderedData + selectedObject = @renderedData[selectedIndex] + value = if @options.id then @options.id(selectedObject, el) else selectedObject.id + console.log value if @options.multiSelect fieldName = "#{fieldName}[]" @@ -229,7 +231,7 @@ class GitLabDropdown # Toggle active class for the tick mark el.toggleClass "is-active" - if value isnt 0 + if value # Create hidden input for form input = "<input type='hidden' name='#{fieldName}' value='#{value}' />" @dropdown.before input diff --git a/app/assets/javascripts/issue_status_select.js.coffee b/app/assets/javascripts/issue_status_select.js.coffee new file mode 100644 index 000000000000..c5740f27ddd5 --- /dev/null +++ b/app/assets/javascripts/issue_status_select.js.coffee @@ -0,0 +1,11 @@ +class @IssueStatusSelect + constructor: -> + $('.js-issue-status').each (i, el) -> + fieldName = $(el).data("field-name") + + $(el).glDropdown( + selectable: true + fieldName: fieldName + id: (obj, el) -> + $(el).data("id") + ) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 5582bf457e38..0b4e2ad7aa65 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -49,5 +49,6 @@ class @LabelsSelect id: (label) -> label.name clicked: -> - $(dropdown).parents('form').submit() + if $(dropdown).hasClass "js-filter-submit" + $(dropdown).parents('form').submit() ) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index aa0d632561f0..9c45800af6e5 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -16,7 +16,7 @@ class @MilestoneSelect data.unshift( title: 'No milestone' ) - + if showAny data.unshift( title: 'Any milestone' @@ -41,5 +41,6 @@ class @MilestoneSelect isSelected: (milestone) -> milestone.title is selectedMilestone clicked: -> - $(dropdown).parents('form').submit() + if $(dropdown).hasClass "js-filter-submit" + $(dropdown).parents('form').submit() ) diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index d4e41ffa5e04..2c6faf4bc18c 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -53,7 +53,8 @@ class @UsersSelect selectable: true fieldName: $(dropdown).data('field-name') clicked: -> - $(dropdown).parents('form').submit() + if $(dropdown).hasClass "js-filter-submit" + $(dropdown).parents('form').submit() renderRow: (user) -> username = if user.username then "@#{user.username}" else "" avatar = if user.avatar_url then user.avatar_url else false diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index b9f9fb49233d..b261ca498c6a 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -9,19 +9,19 @@ .filter-item.inline - if params[:author_id] = hidden_field_tag(:author_id, params[:author_id]) - = dropdown_tag("Author", toggle_class: "js-user-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", + = dropdown_tag("Author", toggle_class: "js-user-search js-filter-submit", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", placeholder: "Search authors", data: {any_user: "Any Author", first_user: true, current_user: true, project_id: @project.id, selected: params[:author_id], field_name: "author_id"}) .filter-item.inline - if params[:assignee_id] = hidden_field_tag(:assignee_id, params[:assignee_id]) - = dropdown_tag("Assignee", toggle_class: "js-user-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", + = dropdown_tag("Assignee", toggle_class: "js-user-search js-filter-submit", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", placeholder: "Search assignee", data: {any_user: "Any Author", first_user: true, null_user: true, current_user: true, project_id: @project.id, selected: params[:assignee_id], field_name: "assignee_id"}) .filter-item.inline.milestone-filter - if params[:milestone_title] = hidden_field_tag(:milestone_title, params[:milestone_title]) - = dropdown_tag("Milestone", title: "Filter by milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable", + = dropdown_tag("Milestone", title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search milestones", footer_content: true, data: {show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.id}) do %ul.dropdown-footer-list - if can? current_user, :admin_milestone, @project @@ -39,7 +39,7 @@ - if params[:label_name] = hidden_field_tag(:label_name, params[:label_name]) .dropdown - %button.dropdown-menu-toggle.js-label-select{type: "button", data: {toggle: "dropdown", field_name: "label_name", selected: params[:label_name], project_id: @project.id}} + %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", selected: params[:label_name], project_id: @project.id}} %span.dropdown-toggle-text Label = icon('chevron-down') @@ -76,8 +76,8 @@ %button.dropdown-title-button.dropdown-menu-close{type: "button", aria: {label: "close"}} = icon('times') .dropdown-content - = hidden_field_tag :new_label_color, nil - = text_field_tag :new_label_name, nil, class: "dropdown-input-field", placeholder: "Name new label" + %input#new_label_color{type: "hidden"} + %input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"} .dropdown-label-color-preview.js-dropdown-label-color-preview .suggest-colors.suggest-colors-dropdown - suggested_colors.each do |color| @@ -95,11 +95,18 @@ .issues_bulk_update.hide = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do .filter-item.inline - = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), include_blank: true, data: { placeholder: "Status" }) + = dropdown_tag("Status", toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-selectable", data: {field_name: "update[state_event]"}) do + %ul + %li + %a{href: "#", data: {id: "reopen"}} Open + %li + %a{href: "#", data: {id: "close"}} Closed .filter-item.inline - = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true, first_user: true, current_user: true) + = dropdown_tag("Assignee", toggle_class: "js-user-search", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", + placeholder: "Search authors", data: {first_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]"}) .filter-item.inline - = select_tag('update[milestone_id]', bulk_update_milestone_options, include_blank: true, data: { placeholder: "Milestone" }) + = dropdown_tag("Milestone", title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable", + placeholder: "Search milestones", data: {show_no: true, field_name: "update[milestone_id]", project_id: @project.id}) = hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag :state_event, params[:state_event] .filter-item.inline @@ -113,6 +120,7 @@ new UsersSelect(); new LabelsSelect(); new MilestoneSelect(); + new IssueStatusSelect(); $('form.filter-form').on('submit', function (event) { event.preventDefault(); Turbolinks.visit(this.action + '&' + $(this).serialize()); -- GitLab From 0e5daa735096bccd5b7d3ea79e8c4f338217c91c Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 8 Mar 2016 11:59:52 +0000 Subject: [PATCH 119/223] Filter alignment --- app/assets/stylesheets/framework/dropdowns.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index dce2b3b99cbd..6d3e48437b70 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -30,7 +30,7 @@ .dropdown-menu-toggle { position: relative; width: 160px; - padding: 5px 20px 5px 10px; + padding: 6px 20px 6px 10px; background-color: $dropdown-toggle-bg; color: $dropdown-toggle-color; font-size: 15px; -- GitLab From 567bedd497530118ce7324a7c81576565a108184 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 8 Mar 2016 12:32:04 +0000 Subject: [PATCH 120/223] Uses a publicly facing URL so that it allows labels & milestones to be visible when not logged in --- app/assets/javascripts/api.js.coffee | 26 ------------------- app/assets/javascripts/gl_dropdown.js.coffee | 1 - .../javascripts/labels_select.js.coffee | 21 ++++++++++++--- .../javascripts/milestone_select.js.coffee | 26 ++++++++++++++----- app/views/shared/issuable/_filter.html.haml | 6 ++--- 5 files changed, 40 insertions(+), 40 deletions(-) diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index 36c6a76e09c2..2ddf8612db30 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -5,7 +5,6 @@ group_projects_path: "/api/:version/groups/:id/projects.json" projects_path: "/api/:version/projects.json" labels_path: "/api/:version/projects/:id/labels" - milestones_path: "/api/:version/projects/:id/milestones" group: (group_id, callback) -> url = Api.buildUrl(Api.group_path) @@ -63,18 +62,6 @@ ).done (projects) -> callback(projects) - projectLabels: (project_id, callback) -> - url = Api.buildUrl(Api.labels_path) - url = url.replace(':id', project_id) - - $.ajax( - url: url - data: - private_token: gon.api_token - dataType: "json" - ).done (labels) -> - callback(labels) - newLabel: (project_id, data, callback) -> url = Api.buildUrl(Api.labels_path) url = url.replace(':id', project_id) @@ -88,19 +75,6 @@ ).done (label) -> callback(label) - - milestones: (project_id, callback) -> - url = Api.buildUrl(Api.milestones_path) - url = url.replace(':id', project_id) - - $.ajax( - url: url - data: - private_token: gon.api_token - dataType: "json" - ).done (milestones) -> - callback(milestones) - # Return group projects list. Filtered by query groupProjects: (group_id, query, callback) -> url = Api.buildUrl(Api.group_projects_path) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index c65b3e31450e..a714ba3d675f 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -220,7 +220,6 @@ class GitLabDropdown if @renderedData selectedObject = @renderedData[selectedIndex] value = if @options.id then @options.id(selectedObject, el) else selectedObject.id - console.log value if @options.multiSelect fieldName = "#{fieldName}[]" diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 0b4e2ad7aa65..14ee529ba353 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -2,6 +2,7 @@ class @LabelsSelect constructor: -> $('.js-label-select').each (i, dropdown) -> projectId = $(dropdown).data('project-id') + labelUrl = $(dropdown).data("labels") selectedLabel = $(dropdown).data('selected') newLabelField = $('#new_label_name') newColorField = $('#new_label_color') @@ -32,13 +33,25 @@ class @LabelsSelect $(dropdown).glDropdown( data: (term, callback) -> - Api.projectLabels 8, callback + # We have to fetch the JS version of the labels list because there is no + # public facing JSON url for labels + $.ajax( + url: labelUrl + ).done (data) -> + html = $(data) + data = [] + html.find('.label-row a').each -> + data.push( + title: $(@).text().trim() + ) + + callback data renderRow: (label) -> - selected = if label.name is selectedLabel then "is-active" else "" + selected = if label.title is selectedLabel then "is-active" else "" "<li> <a href='#' class='#{selected}'> - #{label.name} + #{label.title} </a> </li>" filterable: true @@ -47,7 +60,7 @@ class @LabelsSelect selectable: true fieldName: $(dropdown).data('field-name') id: (label) -> - label.name + label.title clicked: -> if $(dropdown).hasClass "js-filter-submit" $(dropdown).parents('form').submit() diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 9c45800af6e5..92b42bfe864a 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -2,18 +2,29 @@ class @MilestoneSelect constructor: -> $('.js-milestone-select').each (i, dropdown) -> projectId = $(dropdown).data('project-id') + milestonesUrl = $(dropdown).data('milestones') selectedMilestone = $(dropdown).data('selected') showNo = $(dropdown).data('show-no') showAny = $(dropdown).data('show-any') + useId = $(dropdown).data('use-id') $(dropdown).glDropdown( data: (term, callback) -> - Api.milestones projectId, (data) -> - data = $.map data, (milestone) -> - return milestone if milestone.state isnt "closed" + $.ajax( + url: milestonesUrl + ).done (data) -> + html = $(data) + data = [] + html.find('.milestone strong a').each -> + link = $(@).attr("href").split("/") + data.push( + id: link[link.length - 1] + title: $(@).text().trim() + ) if showNo data.unshift( + id: "0" title: 'No milestone' ) @@ -34,10 +45,13 @@ class @MilestoneSelect text: (milestone) -> milestone.title id: (milestone) -> - if milestone.title isnt "Any milestone" - milestone.title + if !useId + if milestone.title isnt "Any milestone" + milestone.title + else + "" else - "" + milestone.id isSelected: (milestone) -> milestone.title is selectedMilestone clicked: -> diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index b261ca498c6a..0e023a680959 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -22,7 +22,7 @@ - if params[:milestone_title] = hidden_field_tag(:milestone_title, params[:milestone_title]) = dropdown_tag("Milestone", title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable", - placeholder: "Search milestones", footer_content: true, data: {show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.id}) do + placeholder: "Search milestones", footer_content: true, data: {show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js)}) do %ul.dropdown-footer-list - if can? current_user, :admin_milestone, @project %li @@ -39,7 +39,7 @@ - if params[:label_name] = hidden_field_tag(:label_name, params[:label_name]) .dropdown - %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", selected: params[:label_name], project_id: @project.id}} + %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", selected: params[:label_name], project_id: @project.id, labels: namespace_project_labels_path(@project.namespace, @project, :js)}} %span.dropdown-toggle-text Label = icon('chevron-down') @@ -106,7 +106,7 @@ placeholder: "Search authors", data: {first_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]"}) .filter-item.inline = dropdown_tag("Milestone", title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable", - placeholder: "Search milestones", data: {show_no: true, field_name: "update[milestone_id]", project_id: @project.id}) + placeholder: "Search milestones", data: {show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js), use_id: true}) = hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag :state_event, params[:state_event] .filter-item.inline -- GitLab From a2f68d879cc644551f90a6f0d7015fd195d02fc7 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 8 Mar 2016 15:18:01 +0000 Subject: [PATCH 121/223] Stops excute remote data fetcher when already recieved data --- app/assets/javascripts/gl_dropdown.js.coffee | 3 ++- app/assets/javascripts/labels_select.js.coffee | 18 +++++++++++++++++- app/views/shared/issuable/_filter.html.haml | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index a714ba3d675f..591e0b6f41dd 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -148,7 +148,8 @@ class GitLabDropdown @appendMenu(full_html) opened: => - if @remote + contentHtml = $('.dropdown-content', @dropdown).html() + if @remote && contentHtml is "" @remote.execute() if @options.filterable diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 14ee529ba353..b4708f6537a5 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -6,6 +6,8 @@ class @LabelsSelect selectedLabel = $(dropdown).data('selected') newLabelField = $('#new_label_name') newColorField = $('#new_label_color') + showNo = $(dropdown).data('show-no') + showAny = $(dropdown).data('show-any') if newLabelField.length $('.suggest-colors-dropdown a').on "click", (e) -> @@ -45,6 +47,20 @@ class @LabelsSelect title: $(@).text().trim() ) + if showNo + data.unshift( + id: "0" + title: 'No label' + ) + + if showAny + data.unshift( + title: 'Any label' + ) + + if data.length > 2 + data.splice 2, 0, "divider" + callback data renderRow: (label) -> selected = if label.title is selectedLabel then "is-active" else "" @@ -56,7 +72,7 @@ class @LabelsSelect </li>" filterable: true search: - fields: ['name'] + fields: ['title'] selectable: true fieldName: $(dropdown).data('field-name') id: (label) -> diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 0e023a680959..d7db2ed6e5fb 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -39,7 +39,7 @@ - if params[:label_name] = hidden_field_tag(:label_name, params[:label_name]) .dropdown - %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", selected: params[:label_name], project_id: @project.id, labels: namespace_project_labels_path(@project.namespace, @project, :js)}} + %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.id, labels: namespace_project_labels_path(@project.namespace, @project, :js)}} %span.dropdown-toggle-text Label = icon('chevron-down') -- GitLab From 41e0a0cb8f5db957a62a51e267a97eef70043c3f Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 8 Mar 2016 15:34:35 +0000 Subject: [PATCH 122/223] Fixed issue with user list not fetching current project users --- app/assets/javascripts/users_select.js.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 2c6faf4bc18c..987c6f4b8d21 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -4,7 +4,8 @@ class @UsersSelect @userPath = "/autocomplete/users/:id.json" $('.js-user-search').each (i, dropdown) => - projectId = $(dropdown).data('project-id') + @projectId = $(dropdown).data('project-id') + @showCurrentUser = $(dropdown).data('current-user') showNullUser = $(dropdown).data('null-user') showAnyUser = $(dropdown).data('any-user') firstUser = $(dropdown).data('first-user') -- GitLab From 348f95f603ee9b1e5d0c762ba962c3039baf661b Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 8 Mar 2016 15:45:03 +0000 Subject: [PATCH 123/223] Removed fuse for fuzzaldrin --- app/assets/javascripts/gl_dropdown.js.coffee | 20 +++++++++----------- app/assets/javascripts/lib/fuse.js | 20 -------------------- 2 files changed, 9 insertions(+), 31 deletions(-) delete mode 100644 app/assets/javascripts/lib/fuse.js diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 591e0b6f41dd..a162955d39ae 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -1,7 +1,7 @@ class GitLabDropdownFilter BLUR_KEYCODES = [27, 40] - constructor: (@dropdown, @remote, @query, @data, @callback) -> + constructor: (@dropdown, @remote, @query, @keys, @data, @callback) -> @input = @dropdown.find(".dropdown-input .dropdown-input-field") # Key events @@ -27,7 +27,12 @@ class GitLabDropdownFilter filter: (search_text) -> data = @data() - results = if search_text isnt "" then data.search(search_text) else data.list + results = data + + if search_text isnt "" + results = fuzzaldrinPlus.filter(data, search_text, + key: @keys + ) @callback results @@ -78,20 +83,13 @@ class GitLabDropdown beforeSend: @toggleLoading.bind(@) success: (data) => @fullData = data - dataToPrase = @fullData - - if @options.filterable - @fullData = new Fuse data, { - keys: search_fields - } - dataToPrase = @fullData.list - @parseData dataToPrase + @parseData @fullData } # Init filiterable if @options.filterable - @filter = new GitLabDropdownFilter @dropdown, @options.filterRemote, @options.data, => + @filter = new GitLabDropdownFilter @dropdown, @options.filterRemote, @options.data, @options.search.fields, => return @fullData , (data) => @parseData data diff --git a/app/assets/javascripts/lib/fuse.js b/app/assets/javascripts/lib/fuse.js deleted file mode 100644 index 09f4ad0aacce..000000000000 --- a/app/assets/javascripts/lib/fuse.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @license - * Fuse - Lightweight fuzzy-search - * - * Copyright (c) 2012-2016 Kirollos Risk <kirollos@gmail.com>. - * All Rights Reserved. Apache Software License 2.0 - * - * Licensed under the Apache License, Version 2.0 (the "License") - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -!function(t){"use strict";function e(){console.log.apply(console,arguments)}function s(t,e){var s,n,i,o;for(this.list=t,this.options=e=e||{},s=0,o=["sort","shouldSort","verbose","tokenize"],n=o.length;n>s;s++)i=o[s],this.options[i]=i in e?e[i]:h[i];for(s=0,o=["searchFn","sortFn","keys","getFn","include"],n=o.length;n>s;s++)i=o[s],this.options[i]=e[i]||h[i]}function n(t,e,s){var o,r,h,a,c,p;if(e){if(h=e.indexOf("."),-1!==h?(o=e.slice(0,h),r=e.slice(h+1)):o=e,a=t[o],null!==a&&void 0!==a)if(r||"string"!=typeof a&&"number"!=typeof a)if(i(a))for(c=0,p=a.length;p>c;c++)n(a[c],r,s);else r&&n(a,r,s);else s.push(a)}else s.push(t);return s}function i(t){return"[object Array]"===Object.prototype.toString.call(t)}function o(t,e){e=e||{},this.options=e,this.options.location=e.location||o.defaultOptions.location,this.options.distance="distance"in e?e.distance:o.defaultOptions.distance,this.options.threshold="threshold"in e?e.threshold:o.defaultOptions.threshold,this.options.maxPatternLength=e.maxPatternLength||o.defaultOptions.maxPatternLength,this.pattern=e.caseSensitive?t:t.toLowerCase(),this.patternLen=t.length,this.patternLen<=this.options.maxPatternLength&&(this.matchmask=1<<this.patternLen-1,this.patternAlphabet=this._calculatePatternAlphabet())}var r=/ +/g,h={id:null,caseSensitive:!1,include:[],shouldSort:!0,searchFn:o,sortFn:function(t,e){return t.score-e.score},getFn:n,keys:[],verbose:!1,tokenize:!1};s.VERSION="2.2.0-beta",s.prototype.set=function(t){return this.list=t,t},s.prototype.search=function(t){this.options.verbose&&e("\nSearch term:",t,"\n"),this.pattern=t,this.results=[],this.resultMap={},this._keyMap=null,this._prepareSearchers(),this._startSearch(),this._computeScore(),this._sort();var s=this._format();return s},s.prototype._prepareSearchers=function(){var t=this.options,e=this.pattern,s=t.searchFn,n=e.split(r),i=0,o=n.length;if(this.options.tokenize)for(this.tokenSearchers=[];o>i;i++)this.tokenSearchers.push(new s(n[i],t));this.fullSeacher=new s(e,t)},s.prototype._startSearch=function(){var t,e,s,n,i=this.options,o=i.getFn,r=this.list,h=r.length,a=this.options.keys,c=a.length,p=null;if("string"==typeof r[0])for(s=0;h>s;s++)this._analyze("",r[s],s,s);else for(this._keyMap={},s=0;h>s;s++)for(p=r[s],n=0;c>n;n++){if(t=a[n],"string"!=typeof t){if(e=1-t.weight||1,this._keyMap[t.name]={weight:e},t.weight<=0||t.weight>1)throw new Error("Key weight has to be > 0 and <= 1");t=t.name}else this._keyMap[t]={weight:1};this._analyze(t,o(p,t,[]),p,s)}},s.prototype._analyze=function(t,s,n,o){var h,a,c,p,l,u,f,d,g,m,y,v,b,S,k,_=this.options,M=!1;if(void 0!==s&&null!==s)if(a=[],"string"==typeof s){if(h=s.split(r),_.verbose&&e("---------\nKey:",t),_.verbose&&e("Record:",h),this.options.tokenize){for(c=this.tokenSearchers,p=c.length,S=0;S<this.tokenSearchers.length;S++){for(m=this.tokenSearchers[S],y=[],k=0;k<h.length;k++)v=h[k],b=m.search(v),b.isMatch?(M=!0,y.push(b.score),a.push(b.score)):(y.push(1),a.push(1));_.verbose&&e("Token scores:",y)}for(u=a[0],d=a.length,S=1;d>S;S++)u+=a[S];u/=d,_.verbose&&e("Token score average:",u)}g=this.fullSeacher.search(s),_.verbose&&e("Full text score:",g.score),f=g.score,void 0!==u&&(f=(f+u)/2),_.verbose&&e("Score average:",f),(M||g.isMatch)&&(l=this.resultMap[o],l?l.output.push({key:t,score:f,matchedIndices:g.matchedIndices}):(this.resultMap[o]={item:n,output:[{key:t,score:f,matchedIndices:g.matchedIndices}]},this.results.push(this.resultMap[o])))}else if(i(s))for(S=0;S<s.length;S++)this._analyze(t,s[S],n,o)},s.prototype._computeScore=function(){var t,s,n,i,o,r,h,a,c,p=this._keyMap,l=this.results;for(this.options.verbose&&e("\n\nComputing score:\n"),t=0;t<l.length;t++){for(n=0,i=l[t].output,o=i.length,a=1,s=0;o>s;s++)r=i[s].score,h=p?p[i[s].key].weight:1,c=r*h,1!==h?a=Math.min(a,c):(n+=c,i[s].nScore=c);1===a?l[t].score=n/o:l[t].score=a,this.options.verbose&&e(l[t])}},s.prototype._sort=function(){var t=this.options;t.shouldSort&&(t.verbose&&e("\n\nSorting...."),this.results.sort(t.sortFn))},s.prototype._format=function(){var t,s,n,i,o,r=this.options,h=r.getFn,a=[],c=this.results,p=r.include;for(r.verbose&&e("\n\nOutput:\n\n",c),i=r.id?function(t){c[t].item=h(c[t].item,r.id,[])[0]}:function(){},o=function(t){var e,s,n,i,o,r=c[t];if(p.length>0){if(e={item:r.item},-1!==p.indexOf("matches"))for(n=r.output,e.matches=[],s=0;s<n.length;s++)i=n[s],o={indices:i.matchedIndices},i.key&&(o.key=i.key),e.matches.push(o);-1!==p.indexOf("score")&&(e.score=c[t].score)}else e=r.item;return e},s=0,n=c.length;n>s;s++)i(s),t=o(s),a.push(t);return a},o.defaultOptions={location:0,distance:100,threshold:.6,maxPatternLength:32},o.prototype._calculatePatternAlphabet=function(){var t={},e=0;for(e=0;e<this.patternLen;e++)t[this.pattern.charAt(e)]=0;for(e=0;e<this.patternLen;e++)t[this.pattern.charAt(e)]|=1<<this.pattern.length-e-1;return t},o.prototype._bitapScore=function(t,e){var s=t/this.patternLen,n=Math.abs(this.options.location-e);return this.options.distance?s+n/this.options.distance:n?1:s},o.prototype.search=function(t){var e,s,n,i,o,h,a,c,p,l,u,f,d,g,m,y,v,b,S,k,_,M,L=this.options;if(t=L.caseSensitive?t:t.toLowerCase(),this.pattern===t)return{isMatch:!0,score:0,matchedIndices:[[0,t.length-1]]};if(this.patternLen>L.maxPatternLength){if(v=t.match(new RegExp(this.pattern.replace(r,"|"))),b=!!v)for(k=[],e=0,_=v.length;_>e;e++)M=v[e],k.push([t.indexOf(M),M.length-1]);return{isMatch:b,score:b?.5:1,matchedIndices:k}}for(i=L.location,n=t.length,o=L.threshold,h=t.indexOf(this.pattern,i),S=[],e=0;n>e;e++)S[e]=0;for(-1!=h&&(o=Math.min(this._bitapScore(0,h),o),h=t.lastIndexOf(this.pattern,i+this.patternLen),-1!=h&&(o=Math.min(this._bitapScore(0,h),o))),h=-1,m=1,y=[],p=this.patternLen+n,e=0;e<this.patternLen;e++){for(a=0,c=p;c>a;)this._bitapScore(e,i+c)<=o?a=c:p=c,c=Math.floor((p-a)/2+a);for(p=c,l=Math.max(1,i-c+1),u=Math.min(i+c,n)+this.patternLen,f=Array(u+2),f[u+1]=(1<<e)-1,s=u;s>=l;s--)if(g=this.patternAlphabet[t.charAt(s-1)],g&&(S[s-1]=1),0===e?f[s]=(f[s+1]<<1|1)&g:f[s]=(f[s+1]<<1|1)&g|((d[s+1]|d[s])<<1|1)|d[s+1],f[s]&this.matchmask&&(m=this._bitapScore(e,s-1),o>=m)){if(o=m,h=s-1,y.push(h),!(h>i))break;l=Math.max(1,2*i-h)}if(this._bitapScore(e+1,i)>o)break;d=f}return k=this._getMatchedIndices(S),{isMatch:h>=0,score:0===m?.001:m,matchedIndices:k}},o.prototype._getMatchedIndices=function(t){for(var e,s=[],n=-1,i=-1,o=0,r=r=t.length;r>o;o++)e=t[o],e&&-1===n?n=o:e||-1===n||(i=o-1,s.push([n,i]),n=-1);return t[o-1]&&s.push([n,o-1]),s},"object"==typeof exports?module.exports=s:"function"==typeof define&&define.amd?define(function(){return s}):t.Fuse=s}(this); -- GitLab From 5e49ffe576bebfe43b9aee595d2a0f45961a857e Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 8 Mar 2016 16:53:31 +0000 Subject: [PATCH 124/223] Enter button trigger first row click when searching --- app/assets/javascripts/gl_dropdown.js.coffee | 39 ++++++++++++++----- .../stylesheets/framework/dropdowns.scss | 2 +- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index a162955d39ae..420f67ef19ee 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -1,12 +1,17 @@ class GitLabDropdownFilter BLUR_KEYCODES = [27, 40] - constructor: (@dropdown, @remote, @query, @keys, @data, @callback) -> + constructor: (@dropdown, @options) -> @input = @dropdown.find(".dropdown-input .dropdown-input-field") # Key events timeout = "" @input.on "keyup", (e) => + if e.keyCode is 13 && @input.val() isnt "" + if @options.enterCallback + @options.enterCallback() + return + clearTimeout timeout timeout = setTimeout => blur_field = @shouldBlur e.keyCode @@ -15,9 +20,9 @@ class GitLabDropdownFilter if blur_field @input.blur() - if @remote - @query search_text, (data) => - @callback(data) + if @options.remote + @options.query search_text, (data) => + @options.callback(data) else @filter search_text , 250 @@ -26,12 +31,12 @@ class GitLabDropdownFilter return BLUR_KEYCODES.indexOf(keyCode) >= 0 filter: (search_text) -> - data = @data() + data = @options.data() results = data if search_text isnt "" results = fuzzaldrinPlus.filter(data, search_text, - key: @keys + key: @options.keys ) @callback results @@ -89,10 +94,16 @@ class GitLabDropdown # Init filiterable if @options.filterable - @filter = new GitLabDropdownFilter @dropdown, @options.filterRemote, @options.data, @options.search.fields, => - return @fullData - , (data) => - @parseData data + @filter = new GitLabDropdownFilter @dropdown, + remote: @options.filterRemote + query: @options.data + keys: @options.search.fields + data: => + return @fullData + callback: (data) => + @parseData data + enterCallback: => + @selectFirstRow() # Event listeners @dropdown.on "shown.bs.dropdown", @opened @@ -234,6 +245,14 @@ class GitLabDropdown input = "<input type='hidden' name='#{fieldName}' value='#{value}' />" @dropdown.before input + selectFirstRow: -> + selector = '.dropdown-content li:first-child a' + if @dropdown.find(".dropdown-toggle-page").length + selector = ".dropdown-page-one .dropdown-content li:first-child a" + + # similute a click on the first link + $(selector).trigger "click" + $.fn.glDropdown = (opts) -> return @.each -> new GitLabDropdown @, opts diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 6d3e48437b70..009d621fc746 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -65,7 +65,7 @@ position: absolute; top: 100%; left: 0; - z-index: 9999; + z-index: 9; width: 240px; margin-top: 2px; margin-bottom: 0; -- GitLab From b42c93c9b07e08eec7642b3dfbc40435a625bb30 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 8 Mar 2016 17:33:45 +0000 Subject: [PATCH 125/223] Correctly handles multi-selected values Fixes an issue where values couldnlt be unselected --- app/assets/javascripts/gl_dropdown.js.coffee | 46 ++++++++++++------- .../javascripts/labels_select.js.coffee | 13 +++++- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 420f67ef19ee..8e1449bc59ca 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -39,7 +39,7 @@ class GitLabDropdownFilter key: @options.keys ) - @callback results + @options.callback results class GitLabDropdownRemote constructor: (@dataEndpoint, @options) -> @@ -75,6 +75,7 @@ class GitLabDropdownRemote class GitLabDropdown LOADING_CLASS = "is-loading" PAGE_TWO_CLASS = "is-page-two" + ACTIVE_CLASS = "is-active" constructor: (@el, @options) -> self = @ @@ -226,24 +227,35 @@ class GitLabDropdown rowClicked: (el) -> fieldName = @options.fieldName - selectedIndex = el.parent().index() - if @renderedData - selectedObject = @renderedData[selectedIndex] - value = if @options.id then @options.id(selectedObject, el) else selectedObject.id + field = @dropdown.parent().find("input[name='#{fieldName}']") - if @options.multiSelect - fieldName = "#{fieldName}[]" + if el.hasClass(ACTIVE_CLASS) + field.remove() else - @dropdown.find('.is-active').removeClass 'is-active' - @dropdown.parent().find("input[name='#{fieldName}']").remove() - - # Toggle active class for the tick mark - el.toggleClass "is-active" - - if value - # Create hidden input for form - input = "<input type='hidden' name='#{fieldName}' value='#{value}' />" - @dropdown.before input + fieldName = @options.fieldName + selectedIndex = el.parent().index() + if @renderedData + selectedObject = @renderedData[selectedIndex] + value = if @options.id then @options.id(selectedObject, el) else selectedObject.id + + if @options.multiSelect + oldValue = field.val() + if oldValue + value = "#{oldValue},#{value}" + else + @dropdown.find(ACTIVE_CLASS).removeClass ACTIVE_CLASS + field.remove() + + # Toggle active class for the tick mark + el.toggleClass "is-active" + + if value + if !field.length + # Create hidden input for form + input = "<input type='hidden' name='#{fieldName}' />" + @dropdown.before input + + @dropdown.parent().find("input[name='#{fieldName}']").val value selectFirstRow: -> selector = '.dropdown-content li:first-child a' diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index b4708f6537a5..5ade2cb66cb5 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -4,6 +4,8 @@ class @LabelsSelect projectId = $(dropdown).data('project-id') labelUrl = $(dropdown).data("labels") selectedLabel = $(dropdown).data('selected') + if selectedLabel + selectedLabel = selectedLabel.split(",") newLabelField = $('#new_label_name') newColorField = $('#new_label_color') showNo = $(dropdown).data('show-no') @@ -60,10 +62,17 @@ class @LabelsSelect if data.length > 2 data.splice 2, 0, "divider" - + callback data renderRow: (label) -> - selected = if label.title is selectedLabel then "is-active" else "" + if $.isArray(selectedLabel) + selected = "" + $.each selectedLabel, (i, selectedLbl) -> + selectedLbl = selectedLbl.trim() + if selected is "" && label.title is selectedLbl + selected = "is-active" + else + selected = if label.title is selectedLabel then "is-active" else "" "<li> <a href='#' class='#{selected}'> -- GitLab From a0e1018eea4dcb35da2949935ce6cfa3995f140d Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 9 Mar 2016 10:19:41 +0000 Subject: [PATCH 126/223] Fixed ruby style errors --- app/helpers/dropdowns_helper.rb | 105 ++++++++++++++------ app/views/help/ui.html.haml | 6 +- app/views/shared/issuable/_filter.html.haml | 47 ++++----- 3 files changed, 96 insertions(+), 62 deletions(-) diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 73a8794c25f5..74f326e0b838 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -1,50 +1,36 @@ module DropdownsHelper - def dropdown_tag(toggle_text, id: nil, toggle_class: nil, dropdown_class: nil, title: false, filter: false, placeholder: "", footer_content: false, data: {}, &block) + def dropdown_tag(toggle_text, options: {}, &block) content_tag :div, class: "dropdown" do - toggle_hash = data.merge({toggle: "dropdown"}) + data_attr = { toggle: "dropdown" } - dropdown_output = "" - dropdown_output += content_tag :button, class: "dropdown-menu-toggle #{toggle_class}", id: id, type: "button", data: toggle_hash do - output = content_tag(:span, toggle_text, class: "dropdown-toggle-text") - output << icon('chevron-down') - output.html_safe + if options.has_key?(:data) + data_attr = options[:data].merge(data_attr) end - dropdown_output += content_tag :div, class: "dropdown-menu dropdown-select #{dropdown_class}" do - output = "" + dropdown_output = dropdown_toggle(toggle_text, data_attr, options) - if title - output += content_tag :div, class: "dropdown-title" do - title_output = content_tag(:span, title) + dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.has_key?(:dropdown_class)}") do + output = "" - title_output += content_tag :button, class: "dropdown-title-button dropdown-menu-close", aria: {label: "close"}, type: "button" do - icon('times') - end.html_safe - end + if options.has_key?(:title) + output << dropdown_title(options[:title]) end - if filter - output += content_tag :div, class: "dropdown-input" do - filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder - filter_output += icon('search') - - filter_output.html_safe - end + if options.has_key?(:filter) + output << dropdown_filter(options[:placeholder]) end - output += content_tag :div, class: "dropdown-content" do - capture(&block) if block && !footer_content + output << content_tag(:div, class: "dropdown-content") do + capture(&block) if block && !options.has_key?(:footer_content) end - if block && footer_content - output += content_tag :div, class: "dropdown-footer" do + if block && options.has_key?(:footer_content) + output << content_tag(:div, class: "dropdown-footer") do capture(&block) end end - output += content_tag :div, class: "dropdown-loading" do - icon('spinner spin') - end + output << dropdown_loading output.html_safe end @@ -52,4 +38,63 @@ def dropdown_tag(toggle_text, id: nil, toggle_class: nil, dropdown_class: nil, t dropdown_output.html_safe end end + + def dropdown_toggle(toggle_text, data_attr, options) + content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do + output = content_tag(:span, toggle_text, class: "dropdown-toggle-text") + output << icon('chevron-down') + output.html_safe + end + end + + def dropdown_title(title, back: false) + content_tag :div, class: "dropdown-title" do + title_output = "" + + if back + title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-back", aria: { label: "Go back" }, type: "button") do + icon('arrow-left') + end + end + + title_output << content_tag(:span, title) + + title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do + icon('times') + end + + title_output.html_safe + end + end + + def dropdown_filter(placeholder) + content_tag :div, class: "dropdown-input" do + filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder + filter_output << icon('search') + + filter_output.html_safe + end + end + + def dropdown_content(&block) + content_tag(:div, class: "dropdown-content") do + if block + capture(&block) + end + end + end + + def dropdown_footer(&block) + content_tag(:div, class: "dropdown-footer") do + if block + capture(&block) + end + end + end + + def dropdown_loading + content_tag :div, class: "dropdown-loading" do + icon('spinner spin') + end + end end diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index db46e51f8d72..d084559abc33 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -409,8 +409,8 @@ = icon('spinner spin') :javascript $('#js-project-dropdown').glDropdown({ - data: function (callback) { - Api.projects("", "last_activity_at", function (data) { + data: function (term, callback) { + Api.projects(term, "last_activity_at", function (data) { callback(data); }); }, @@ -433,7 +433,7 @@ .example %div - = dropdown_tag("Projects", title: "Go to project", filter: true, placeholder: "Filter projects") + = dropdown_tag("Projects", options: { title: "Go to project", filter: true, placeholder: "Filter projects" }) %h2#panels Panels diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index d7db2ed6e5fb..1e36dd9fb820 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -9,20 +9,20 @@ .filter-item.inline - if params[:author_id] = hidden_field_tag(:author_id, params[:author_id]) - = dropdown_tag("Author", toggle_class: "js-user-search js-filter-submit", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", - placeholder: "Search authors", data: {any_user: "Any Author", first_user: true, current_user: true, project_id: @project.id, selected: params[:author_id], field_name: "author_id"}) + = dropdown_tag("Author", options: { toggle_class: "js-user-search js-filter-submit", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", + placeholder: "Search authors", data: { any_user: "Any Author", first_user: true, current_user: true, project_id: @project.id, selected: params[:author_id], field_name: "author_id" } }) .filter-item.inline - if params[:assignee_id] = hidden_field_tag(:assignee_id, params[:assignee_id]) - = dropdown_tag("Assignee", toggle_class: "js-user-search js-filter-submit", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", - placeholder: "Search assignee", data: {any_user: "Any Author", first_user: true, null_user: true, current_user: true, project_id: @project.id, selected: params[:assignee_id], field_name: "assignee_id"}) + = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-filter-submit", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", + placeholder: "Search assignee", data: { any_user: "Any Author", first_user: true, null_user: true, current_user: true, project_id: @project.id, selected: params[:assignee_id], field_name: "assignee_id" } }) .filter-item.inline.milestone-filter - if params[:milestone_title] = hidden_field_tag(:milestone_title, params[:milestone_title]) - = dropdown_tag("Milestone", title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable", - placeholder: "Search milestones", footer_content: true, data: {show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js)}) do + = dropdown_tag("Milestone", options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable", + placeholder: "Search milestones", footer_content: true, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js) } }) do %ul.dropdown-footer-list - if can? current_user, :admin_milestone, @project %li @@ -45,16 +45,10 @@ = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable .dropdown-page-one - .dropdown-title - %span - Filter by label - %button.dropdown-title-button.dropdown-menu-close{type: "button", aria: {label: "close"}} - = icon('times') - .dropdown-input - = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: "Search labels" - = icon('search') - .dropdown-content - .dropdown-footer + = dropdown_title("Filter by label") + = dropdown_filter("Search labels") + = dropdown_content + = dropdown_footer do %ul.dropdown-footer-list - if can? current_user, :admin_label, @project %li @@ -68,14 +62,8 @@ View labels - if can? current_user, :admin_label, @project .dropdown-page-two - .dropdown-title - %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}} - = icon('arrow-left') - %span - Create new label - %button.dropdown-title-button.dropdown-menu-close{type: "button", aria: {label: "close"}} - = icon('times') - .dropdown-content + = dropdown_title("Create new label", back: true) + = dropdown_content do %input#new_label_color{type: "hidden"} %input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"} .dropdown-label-color-preview.js-dropdown-label-color-preview @@ -85,6 +73,7 @@   %button.btn.btn-primary.js-new-label-btn{type: "button"} Create + = dropdown_loading .dropdown-loading = icon('spinner spin') @@ -95,18 +84,18 @@ .issues_bulk_update.hide = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do .filter-item.inline - = dropdown_tag("Status", toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-selectable", data: {field_name: "update[state_event]"}) do + = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do %ul %li %a{href: "#", data: {id: "reopen"}} Open %li %a{href: "#", data: {id: "close"}} Closed .filter-item.inline - = dropdown_tag("Assignee", toggle_class: "js-user-search", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", - placeholder: "Search authors", data: {first_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]"}) + = dropdown_tag("Assignee", options: { toggle_class: "js-user-search", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", + placeholder: "Search authors", data: { first_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) .filter-item.inline - = dropdown_tag("Milestone", title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable", - placeholder: "Search milestones", data: {show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js), use_id: true}) + = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable", + placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js), use_id: true } }) = hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag :state_event, params[:state_event] .filter-item.inline -- GitLab From 655bdb1d8834eed8e8dd15779a873f2fe1d5d8ba Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 9 Mar 2016 13:34:39 +0000 Subject: [PATCH 127/223] Fixed failing tests --- .../javascripts/milestone_select.js.coffee | 4 +- app/assets/stylesheets/framework/filters.scss | 1 + app/views/shared/issuable/_filter.html.haml | 56 ++++++++++--------- features/steps/dashboard/issues.rb | 17 ++++-- features/steps/dashboard/merge_requests.rb | 17 ++++-- .../steps/project/issues/filter_labels.rb | 5 +- features/steps/project/issues/issues.rb | 11 ++-- .../issues/filter_by_milestone_spec.rb | 5 +- 8 files changed, 72 insertions(+), 44 deletions(-) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 92b42bfe864a..5e884454a658 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -25,12 +25,12 @@ class @MilestoneSelect if showNo data.unshift( id: "0" - title: 'No milestone' + title: 'No Milestone' ) if showAny data.unshift( - title: 'Any milestone' + title: 'Any Milestone' ) if data.length > 2 diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index eab416286779..c431e2b0df3d 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -1,5 +1,6 @@ .filter-item { margin-right: 6px; + vertical-align: top; } @media (min-width: 800px) { diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 1e36dd9fb820..c3fbba2ba547 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -9,37 +9,38 @@ .filter-item.inline - if params[:author_id] = hidden_field_tag(:author_id, params[:author_id]) - = dropdown_tag("Author", options: { toggle_class: "js-user-search js-filter-submit", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", - placeholder: "Search authors", data: { any_user: "Any Author", first_user: true, current_user: true, project_id: @project.id, selected: params[:author_id], field_name: "author_id" } }) + = dropdown_tag("Author", options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author", + placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id" } }) .filter-item.inline - if params[:assignee_id] = hidden_field_tag(:assignee_id, params[:assignee_id]) = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-filter-submit", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", - placeholder: "Search assignee", data: { any_user: "Any Author", first_user: true, null_user: true, current_user: true, project_id: @project.id, selected: params[:assignee_id], field_name: "assignee_id" } }) + placeholder: "Search assignee", data: { any_user: "Any Author", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id" } }) .filter-item.inline.milestone-filter - if params[:milestone_title] = hidden_field_tag(:milestone_title, params[:milestone_title]) = dropdown_tag("Milestone", options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable", - placeholder: "Search milestones", footer_content: true, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js) } }) do - %ul.dropdown-footer-list - - if can? current_user, :admin_milestone, @project + placeholder: "Search milestones", footer_content: true, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: (@project.id if @project), milestones: (namespace_project_milestones_path(@project.namespace, @project, :js) if @project) } }) do + - if @project + %ul.dropdown-footer-list + - if can? current_user, :admin_milestone, @project + %li + = link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do + Create new %li - = link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do - Create new - %li - = link_to namespace_project_milestones_path(@project.namespace, @project) do - - if can? current_user, :admin_milestone, @project - Manage milestones - - else - View milestones + = link_to namespace_project_milestones_path(@project.namespace, @project) do + - if can? current_user, :admin_milestone, @project + Manage milestones + - else + View milestones .filter-item.inline.labels-filter - if params[:label_name] = hidden_field_tag(:label_name, params[:label_name]) .dropdown - %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.id, labels: namespace_project_labels_path(@project.namespace, @project, :js)}} + %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: (@project.id if @project), labels: (namespace_project_labels_path(@project.namespace, @project, :js) if @project)}} %span.dropdown-toggle-text Label = icon('chevron-down') @@ -48,18 +49,19 @@ = dropdown_title("Filter by label") = dropdown_filter("Search labels") = dropdown_content - = dropdown_footer do - %ul.dropdown-footer-list - - if can? current_user, :admin_label, @project + - if @project + = dropdown_footer do + %ul.dropdown-footer-list + - if can? current_user, :admin_label, @project + %li + %a.dropdown-toggle-page{href: "#"} + Create new %li - %a.dropdown-toggle-page{href: "#"} - Create new - %li - = link_to namespace_project_labels_path(@project.namespace, @project) do - - if can? current_user, :admin_label, @project - Manage labels - - else - View labels + = link_to namespace_project_labels_path(@project.namespace, @project) do + - if can? current_user, :admin_label, @project + Manage labels + - else + View labels - if can? current_user, :admin_label, @project .dropdown-page-two = dropdown_title("Create new label", back: true) @@ -92,7 +94,7 @@ %a{href: "#", data: {id: "close"}} Closed .filter-item.inline = dropdown_tag("Assignee", options: { toggle_class: "js-user-search", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", - placeholder: "Search authors", data: { first_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) + placeholder: "Search authors", data: { first_user: (current_user.username if current_user), current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) .filter-item.inline = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js), use_id: true } }) diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb index cbe54e2dc792..d723300f4854 100644 --- a/features/steps/dashboard/issues.rb +++ b/features/steps/dashboard/issues.rb @@ -36,13 +36,22 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps end step 'I click "Authored by me" link' do - select2(current_user.id, from: "#author_id") - select2(nil, from: "#assignee_id") + execute_script('$("#assignee_id").val("")') + execute_script('$(".js-user-search").first().click()') + sleep 1 + execute_script("$('.dropdown-content li:contains(\"#{current_user.to_reference}\") a').click()") + sleep 1 end step 'I click "All" link' do - select2(nil, from: "#author_id") - select2(nil, from: "#assignee_id") + execute_script('$(".js-user-search").first().click()') + sleep 1 + execute_script('$(".js-user-search").first().parent().find("li a").first().click()') + sleep 1 + execute_script('$(".js-user-search").eq(1).click()') + sleep 1 + execute_script('$(".js-user-search").eq(1).parent().find("li a").first().click()') + sleep 1 end def should_see(issue) diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb index 28c8c6b60151..7fc0e444e868 100644 --- a/features/steps/dashboard/merge_requests.rb +++ b/features/steps/dashboard/merge_requests.rb @@ -40,13 +40,22 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps end step 'I click "Authored by me" link' do - select2(current_user.id, from: "#author_id") - select2(nil, from: "#assignee_id") + execute_script('$("#assignee_id").val("")') + execute_script('$(".js-user-search").first().click()') + sleep 0.5 + execute_script("$('.dropdown-content li:contains(\"#{current_user.to_reference}\") a').click()") + sleep 2 end step 'I click "All" link' do - select2(nil, from: "#author_id") - select2(nil, from: "#assignee_id") + execute_script('$(".js-user-search").first().click()') + sleep 0.5 + execute_script('$(".js-user-search").first().parent().find("li a").first().click()') + sleep 2 + execute_script('$(".js-user-search").eq(1).click()') + sleep 0.5 + execute_script('$(".js-user-search").eq(1).parent().find("li a").first().click()') + sleep 2 end def should_see(merge_request) diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb index 50bb32429b96..6d50501a722d 100644 --- a/features/steps/project/issues/filter_labels.rb +++ b/features/steps/project/issues/filter_labels.rb @@ -29,7 +29,10 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps end step 'I click link "bug"' do - select2('bug', from: "#label_name") + page.find('.js-label-select').click + sleep 0.5 + execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()") + sleep 2 end step 'I click link "feature"' do diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index 41defe3de30b..8c31fa890b26 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -27,7 +27,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I click link "Closed"' do - click_link "Closed" + find('.issues-state-filters a', text: "Closed").click end step 'I click button "Unsubscribe"' do @@ -63,14 +63,15 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I click "author" dropdown' do - first('#s2id_author_id').click + page.find('.js-author-search').click + sleep 1 end step 'I see current user as the first user' do - expect(page).to have_selector('.user-result', visible: true, count: 3) - users = page.all('.user-name') + 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 + expect(users[1].text).to eq "#{current_user.name} #{current_user.to_reference}" end step 'I submit new issue "500 error on profile"' do diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb index 591866b40d46..e48f48153f5b 100644 --- a/spec/features/issues/filter_by_milestone_spec.rb +++ b/spec/features/issues/filter_by_milestone_spec.rb @@ -31,6 +31,9 @@ def visit_issues(project) end def filter_by_milestone(title) - select2(title, from: '#milestone_title') + find(".js-milestone-select").click + sleep 1 + find(".milestone-filter a", text: title).click + sleep 1 end end -- GitLab From 5f9fcd156cfcb2f1e9b80319336dadd12555eb7a Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 10 Mar 2016 09:51:39 +0000 Subject: [PATCH 128/223] Fixed failing milestone filter tests --- spec/features/issues/filter_by_milestone_spec.rb | 4 +--- spec/features/merge_requests/filter_by_milestone_spec.rb | 7 ++++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb index e48f48153f5b..f6e33f651c47 100644 --- a/spec/features/issues/filter_by_milestone_spec.rb +++ b/spec/features/issues/filter_by_milestone_spec.rb @@ -1,8 +1,6 @@ require 'rails_helper' feature 'Issue filtering by Milestone', feature: true do - include Select2Helper - let(:project) { create(:project, :public) } let(:milestone) { create(:milestone, project: project) } @@ -32,7 +30,7 @@ def visit_issues(project) def filter_by_milestone(title) find(".js-milestone-select").click - sleep 1 + sleep 0.5 find(".milestone-filter a", text: title).click sleep 1 end diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb index f70214e11223..1b2fd1bab108 100644 --- a/spec/features/merge_requests/filter_by_milestone_spec.rb +++ b/spec/features/merge_requests/filter_by_milestone_spec.rb @@ -1,8 +1,6 @@ require 'rails_helper' feature 'Merge Request filtering by Milestone', feature: true do - include Select2Helper - let(:project) { create(:project, :public) } let(:milestone) { create(:milestone, project: project) } @@ -31,6 +29,9 @@ def visit_merge_requests(project) end def filter_by_milestone(title) - select2(title, from: '#milestone_title') + find(".js-milestone-select").click + sleep 0.5 + find(".milestone-filter a", text: title).click + sleep 1 end end -- GitLab From 163cff871ae87f96a97c5829d0b1130fbd78d4f1 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Thu, 10 Mar 2016 15:32:31 +0100 Subject: [PATCH 129/223] Optimize Project#ci_service(s) The method Project#ci_services would load all services into memory (including _all_ their columns) and then use Enumerable#select to reduce the list. Project#ci_service in turn would further reduce this list down to just 1 Service instance. Instead of doing all this in Ruby we can just offload the work to the database, reducing the amount of time spent in these methods. These changes reduce the time of the first call to Project#ci_services from around 240 ms to around 10 ms, though the final timings will vary based on database load. Because Project#ci_service is memoized there's no further overhead introduced by using a database query. Fixes gitlab-org/gitlab-ce#14186 --- app/models/project.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 43e4c574b779..3a7d23f05439 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -528,11 +528,11 @@ def find_service(list, name) end def ci_services - services.select { |service| service.category == :ci } + services.where(category: :ci) end def ci_service - @ci_service ||= ci_services.find(&:activated?) + @ci_service ||= ci_services.reorder(nil).find_by(active: true) end def jira_tracker? -- GitLab From 705003db28dc5a79a9132961c06c581f69788fbc Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 10 Mar 2016 15:48:53 +0000 Subject: [PATCH 130/223] Removed CiBuild in favour of already available CiBuild class --- app/assets/javascripts/ci/build.coffee | 13 +++++++++++++ app/assets/javascripts/ci_build.js.coffee | 14 -------------- app/assets/javascripts/dispatcher.js.coffee | 2 -- 3 files changed, 13 insertions(+), 16 deletions(-) delete mode 100644 app/assets/javascripts/ci_build.js.coffee diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee index 44d5ddb7d951..6c75876a2077 100644 --- a/app/assets/javascripts/ci/build.coffee +++ b/app/assets/javascripts/ci/build.coffee @@ -4,6 +4,8 @@ class CiBuild constructor: (build_url, build_status) -> clearInterval(CiBuild.interval) + @initScrollButtonAffix() + if build_status == "running" || build_status == "pending" # # Bind autoscroll button to follow build output @@ -38,4 +40,15 @@ class CiBuild checkAutoscroll: -> $("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state") + initScrollButtonAffix: -> + buildScroll = $('#js-build-scroll') + body = $('body') + buildTrace = $('#build-trace') + + buildScroll.affix( + offset: + bottom: -> + body.outerHeight() - (buildTrace.outerHeight() + buildTrace.offset().top) + ) + @CiBuild = CiBuild diff --git a/app/assets/javascripts/ci_build.js.coffee b/app/assets/javascripts/ci_build.js.coffee deleted file mode 100644 index e110eece16be..000000000000 --- a/app/assets/javascripts/ci_build.js.coffee +++ /dev/null @@ -1,14 +0,0 @@ -class @CiBuild - constructor: -> - @initScrollButtonAffix() - - initScrollButtonAffix: -> - buildScroll = $('#js-build-scroll') - body = $('body') - buildTrace = $('#build-trace') - - buildScroll.affix( - offset: - bottom: -> - body.outerHeight() - (buildTrace.outerHeight() + buildTrace.offset().top) - ) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 8a95e3532350..ee81fee58681 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -104,8 +104,6 @@ class Dispatcher new ProjectFork() when 'projects:artifacts:browse' new BuildArtifacts() - when 'projects:builds:show' - new CiBuild() switch path.first() when 'admin' -- GitLab From e8785432985cfb0106a778f90dd8b5e32cc5c395 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Thu, 10 Mar 2016 17:05:36 +0100 Subject: [PATCH 131/223] Remove unnecessary random key --- lib/gitlab/exclusive_lease.rb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb index 8bf6f13b99d3..324e32a5c687 100644 --- a/lib/gitlab/exclusive_lease.rb +++ b/lib/gitlab/exclusive_lease.rb @@ -1,5 +1,3 @@ -require 'securerandom' - module Gitlab # This class implements an 'exclusive lease'. We call it a 'lease' # because it has a set expiry time. We call it 'exclusive' because only @@ -27,7 +25,7 @@ def initialize(key, timeout:) def try_obtain # This is expected to be atomic because we are talking to a # single-threaded Redis server. - !!redis.set(redis_key, redis_value, nx: true, ex: @timeout) + !!redis.set(redis_key, '1', nx: true, ex: @timeout) end private @@ -40,9 +38,5 @@ def redis def redis_key "gitlab:exclusive_lease:#{@key}" end - - def redis_value - @redis_value ||= SecureRandom.hex(10) - end end end -- GitLab From e9530af420bf1d4cbd1a6d71877f6a0e0033cd4b Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij <sytse@gitlab.com> Date: Thu, 10 Mar 2016 08:13:11 -0800 Subject: [PATCH 132/223] Reduce example documentation. --- doc/ci/README.md | 23 ++--------------------- doc/ci/examples/README.md | 20 +++++++++++++------- doc/ci/quick_start/README.md | 11 ++--------- 3 files changed, 17 insertions(+), 37 deletions(-) diff --git a/doc/ci/README.md b/doc/ci/README.md index 2120b5b28509..4abc45bf9bbd 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -3,6 +3,7 @@ ### CI User documentation - [Get started with GitLab CI](quick_start/README.md) +- [CI examples for various languages](examples/README.md) - [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md) - [Learn how `.gitlab-ci.yml` works](yaml/README.md) - [Configure a Runner, the application that runs your builds](runners/README.md) @@ -14,24 +15,4 @@ - [Build artifacts](build_artifacts/README.md) - [User permissions](permissions/README.md) - [API](api/README.md) - -### CI Examples - -- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) -- [Test your PHP applications](examples/php.md) -- [Test and deploy Ruby applications to Heroku](examples/test-and-deploy-ruby-application-to-heroku.md) -- [Test and deploy Python applications to Heroku](examples/test-and-deploy-python-application-to-heroku.md) -- [Test Clojure applications](examples/test-clojure-application.md) -- [Using `dpl` as deployment tool](deployment/README.md) -- Help your favorite programming language and GitLab by sending a merge request - with a guide for that language. - -### CI Services - -GitLab CI uses the `services` keyword to define what docker containers should -be linked with your base image. Below is a list of examples you may use: - -- [Using MySQL](services/mysql.md) -- [Using PostgreSQL](services/postgres.md) -- [Using Redis](services/redis.md) -- [Using Other Services](docker/using_docker_images.md#how-to-use-other-images-as-services) +- [CI services (linked docker containers)](services/README.md) diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index 31f29f4a082e..0a83c4dd8584 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -1,13 +1,19 @@ -## Build script examples +# CI Examples +- [Testing PHP](php.md) - [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md) - [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md) - [Test a Clojure application](test-clojure-application.md) +- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) +- [Test your PHP applications](examples/php.md) +- [Test and deploy Ruby applications to Heroku](examples/test-and-deploy-ruby-application-to-heroku.md) +- [Test and deploy Python applications to Heroku](examples/test-and-deploy-python-application-to-heroku.md) +- [Test Clojure applications](examples/test-clojure-application.md) +- [Using `dpl` as deployment tool](deployment/README.md) +- Help your favorite programming language and GitLab by sending a merge request + with a guide for that language. -## Languages - -This is a list of languages you can test with GitLab CI. Each section has -comprehensive documentation and comes with a test repository hosted on -GitLab.com. +## Outside the documentation -- [Testing PHP](php.md) +- [Blost post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/) +- [Repo's with examples for various languages](https://gitlab.com/groups/gitlab-examples) diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 624d9899c792..9aba4326e116 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -223,20 +223,13 @@ You can access a builds badge image using following link: http://example.gitlab.com/namespace/project/badges/branch/build.svg ``` +Awesome! You started using CI in GitLab! + ## Examples Visit the [examples README][examples] to see a list of examples using GitLab CI with various languages. -## Next steps - -Awesome! You started using CI in GitLab! - -Next you can look into doing more with the CI. Many people are using GitLab -to package, containerize, test and deploy software. - -Visit our various languages examples at <https://gitlab.com/groups/gitlab-examples>. - [runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#installation [blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/ [examples]: ../examples/README.md -- GitLab From a5f600184e6b996fe876ebd2919f0a192e181dd7 Mon Sep 17 00:00:00 2001 From: Sean Lang <slang800@gmail.com> Date: Tue, 8 Mar 2016 13:10:19 -0500 Subject: [PATCH 133/223] rewrite logo to simplify SVG code --- CHANGELOG | 1 + public/logo.svg | 35 +++++++++-------------------------- 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c3ef06c45811..3c5627459e50 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,7 @@ v 8.6.0 (unreleased) - Add "last_sign_in_at" and "confirmed_at" to GET /users/* API endpoints for admins (evuez) - Return empty array instead of 404 when commit has no statuses in commit status API - Decrease the font size and the padding of the `.anchor` icons used in the README (Roberto Dip) + - Rewrite logo to simplify SVG code (Sean Lang) - Add support for cross-project label references - Update documentation to reflect Guest role not being enforced on internal projects - Allow search for logged out users diff --git a/public/logo.svg b/public/logo.svg index c09785cb96f4..fc4553137f73 100644 --- a/public/logo.svg +++ b/public/logo.svg @@ -1,26 +1,9 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg width="210px" height="210px" viewBox="0 0 210 210" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"> - <!-- Generator: Sketch 3.3.2 (12043) - http://www.bohemiancoding.com/sketch --> - <title>Slice 1</title> - <desc>Created with Sketch.</desc> - <defs></defs> - <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage"> - <g id="logo" sketch:type="MSLayerGroup" transform="translate(0.000000, 10.000000)"> - <g id="Page-1" sketch:type="MSShapeGroup"> - <g id="Fill-1-+-Group-24"> - <g id="Group-24"> - <g id="Group"> - <path d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" id="Fill-4" fill="#E24329"></path> - <path d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-8" fill="#FC6D26"></path> - <path d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 L12.2685,74.7341 Z" id="Fill-12" fill="#FCA326"></path> - <path d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 L12.2685,74.7342 Z" id="Fill-16" fill="#E24329"></path> - <path d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-18" fill="#FC6D26"></path> - <path d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 L197.8544,74.7341 Z" id="Fill-20" fill="#FCA326"></path> - <path d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 L197.8544,74.7342 Z" id="Fill-22" fill="#E24329"></path> - </g> - </g> - </g> - </g> - </g> - </g> -</svg> \ No newline at end of file +<svg width="210" height="210" viewBox="0 0 210 210" xmlns="http://www.w3.org/2000/svg"> + <path d="M105.0614 203.655l38.64-118.921h-77.28l38.64 118.921z" fill="#e24329"/> + <path d="M105.0614 203.6548l-38.64-118.921h-54.153l92.793 118.921z" fill="#fc6d26"/> + <path d="M12.2685 84.7341l-11.742 36.139c-1.071 3.296.102 6.907 2.906 8.944l101.629 73.838-92.793-118.921z" fill="#fca326"/> + <path d="M12.2685 84.7342h54.153l-23.273-71.625c-1.197-3.686-6.411-3.685-7.608 0l-23.272 71.625z" fill="#e24329"/> + <path d="M105.0614 203.6548l38.64-118.921h54.153l-92.793 118.921z" fill="#fc6d26"/> + <path d="M197.8544 84.7341l11.742 36.139c1.071 3.296-.102 6.907-2.906 8.944l-101.629 73.838 92.793-118.921z" fill="#fca326"/> + <path d="M197.8544 84.7342h-54.153l23.273-71.625c1.197-3.686 6.411-3.685 7.608 0l23.272 71.625z" fill="#e24329"/> +</svg> -- GitLab From ae33a71df48ff5b4f045f7541ee5a2004355b508 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 10 Mar 2016 17:17:41 +0000 Subject: [PATCH 134/223] Changed variable names --- app/assets/javascripts/ci/build.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee index 6c75876a2077..607bb6e965f7 100644 --- a/app/assets/javascripts/ci/build.coffee +++ b/app/assets/javascripts/ci/build.coffee @@ -41,9 +41,9 @@ class CiBuild $("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state") initScrollButtonAffix: -> - buildScroll = $('#js-build-scroll') - body = $('body') - buildTrace = $('#build-trace') + $buildScroll = $('#js-build-scroll') + $body = $('body') + $buildTrace = $('#build-trace') buildScroll.affix( offset: -- GitLab From 28bfb06bcad571518f2e409aa98739ae820a2c9d Mon Sep 17 00:00:00 2001 From: Jacob Schatz <jacobschatz@Jacobs-MBP.fios-router.home> Date: Thu, 25 Feb 2016 13:51:17 -0500 Subject: [PATCH 135/223] Add error for ajax:error when submitting comments Fixes #13814 --- app/assets/javascripts/notes.js.coffee | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index daa504b7782e..b0e7af817f06 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -30,6 +30,9 @@ class @Notes $(document).on "ajax:success", ".js-main-target-form", @addNote $(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote + # catch note ajax errors + $(document).on "ajax:error", ".js-main-target-form", @addNoteError + # change note in UI after update $(document).on "ajax:success", "form.edit-note", @updateNote @@ -317,6 +320,10 @@ class @Notes addNote: (xhr, note, status) => @renderNote(note) + addNoteError: (xhr, note, status) => + flash = new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert') + flash.pinTo('.md-area') + ### Called in response to the new note form being submitted -- GitLab From 39a962b29c7f67e6601e98095b05a200d44d9312 Mon Sep 17 00:00:00 2001 From: Jacob Schatz <jacobschatz@Jacobs-MBP.fios-router.home> Date: Mon, 29 Feb 2016 13:03:21 -0500 Subject: [PATCH 136/223] Change test in an attempt to pass. Thanks @rspeicher! --- features/steps/project/source/browse_files.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 51b15791674c..243469b8e7d5 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -361,7 +361,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I can see the new rendered SVG image' do - expect(find('.file-content')).to have_css('img') + expect(page).to have_css('.file-content img') end private -- GitLab From d4b1f83b55b0dbfc45f6f9bc29c3acf246fe65fa Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 10 Mar 2016 17:34:06 +0000 Subject: [PATCH 137/223] Fixes issue with issue sidebar toggle button not working Closes #14195 --- app/assets/javascripts/application.js.coffee | 8 ++++---- app/assets/javascripts/merge_request_tabs.js.coffee | 3 +-- app/views/projects/issues/show.html.haml | 2 +- .../projects/merge_requests/show/_mr_title.html.haml | 2 +- app/views/shared/issuable/_sidebar.html.haml | 2 +- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 321da10a0099..1212e89975b0 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -220,17 +220,17 @@ $ -> .off 'breakpoint:change' .on 'breakpoint:change', (e, breakpoint) -> if breakpoint is 'sm' or breakpoint is 'xs' - $gutterIcon = $('aside .gutter-toggle').find('i') + $gutterIcon = $('.js-sidebar-toggle').find('i') if $gutterIcon.hasClass('fa-angle-double-right') $gutterIcon.closest('a').trigger('click') $(document) - .off 'click', 'aside .gutter-toggle' - .on 'click', 'aside .gutter-toggle', (e, triggered) -> + .off 'click', '.js-sidebar-toggle' + .on 'click', '.js-sidebar-toggle', (e, triggered) -> e.preventDefault() $this = $(this) $thisIcon = $this.find 'i' - $allGutterToggleIcons = $('.gutter-toggle i') + $allGutterToggleIcons = $('.js-sidebar-toggle i') if $thisIcon.hasClass('fa-angle-double-right') $allGutterToggleIcons .removeClass('fa-angle-double-right') diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 58373ba87a51..8322b4c46ad0 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -189,7 +189,7 @@ class @MergeRequestTabs $('.container-fluid').removeClass('container-limited') shrinkView: -> - $gutterIcon = $('.gutter-toggle i') + $gutterIcon = $('.js-sidebar-toggle i') # Wait until listeners are set setTimeout( -> @@ -197,4 +197,3 @@ class @MergeRequestTabs if $gutterIcon.is('.fa-angle-double-right') $gutterIcon.closest('a').trigger('click',[true]) , 0) - diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 617b04378070..2a7d0b0757d4 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -18,7 +18,7 @@ %span.hidden-sm.hidden-md.hidden-lg = icon('circle-o') - %a.btn.btn-default.pull-right.hidden-sm.hidden-md.hidden-lg.gutter-toggle{ href: "#" } + %a.btn.btn-default.pull-right.visible-xs.gutter-toggle.js-sidebar-toggle{ href: "#" } = icon('angle-double-left') .issue-meta diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index d24c12251f33..83b056eef66a 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -4,7 +4,7 @@ = @merge_request.state_human_name %span.hidden-sm.hidden-md.hidden-lg = icon(@merge_request.state_icon_name) - %a.btn.btn-default.pull-right.hidden-sm.hidden-md.hidden-lg.gutter-toggle{ href: "#" } + %a.btn.btn-default.pull-right.visible-xs.gutter-toggle.js-sidebar-toggle{ href: "#" } = icon('angle-double-left') .issue-meta %strong.identifier diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 36f063778869..9020a1330a33 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -6,7 +6,7 @@ of = issuables_count(issuable) %span.pull-right - %a.gutter-toggle{href: '#'} + %a.gutter-toggle.js-sidebar-toggle{href: '#'} = sidebar_gutter_toggle_icon .issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'} - if prev_issuable = prev_issuable_for(issuable) -- GitLab From 44a3744986e59e37fd40b4f2a59723297047814d Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Thu, 10 Mar 2016 18:39:50 +0100 Subject: [PATCH 138/223] Make comment less ambivalent --- lib/gitlab/exclusive_lease.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb index 324e32a5c687..1d2d6981c597 100644 --- a/lib/gitlab/exclusive_lease.rb +++ b/lib/gitlab/exclusive_lease.rb @@ -23,8 +23,7 @@ def initialize(key, timeout:) # Try to obtain the lease. Return true on succes, # false if the lease is already taken. def try_obtain - # This is expected to be atomic because we are talking to a - # single-threaded Redis server. + # Performing a single SET is atomic !!redis.set(redis_key, '1', nx: true, ex: @timeout) end -- GitLab From 726cccefe4c6995185e612aa91b9eca580470366 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 10 Mar 2016 17:39:53 +0000 Subject: [PATCH 139/223] Correct ruby code --- app/views/projects/issues/_merge_requests.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index c6da6f7997e1..3eb0926b7466 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -11,7 +11,7 @@ - elsif has_any_ci = icon('blank fw') %span.merge-request-id - #{merge_request.to_reference} + = merge_request.to_reference %span.merge-request-info %strong = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title" -- GitLab From 5130e8c8d11317f56615359a91d342bc81850f3c Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer <contact@jacobvosmaer.nl> Date: Thu, 10 Mar 2016 18:41:57 +0100 Subject: [PATCH 140/223] Typo --- lib/gitlab/exclusive_lease.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb index 1d2d6981c597..2ef50286b1dd 100644 --- a/lib/gitlab/exclusive_lease.rb +++ b/lib/gitlab/exclusive_lease.rb @@ -20,7 +20,7 @@ def initialize(key, timeout:) @key, @timeout = key, timeout end - # Try to obtain the lease. Return true on succes, + # Try to obtain the lease. Return true on success, # false if the lease is already taken. def try_obtain # Performing a single SET is atomic -- GitLab From 3304094b3510af2066676cba994079053027ddfa Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 10 Mar 2016 18:05:41 +0000 Subject: [PATCH 141/223] Changed named argument Set position to auto left so that if it is off-screen it goes to the right --- app/helpers/ci_status_helper.rb | 4 ++-- app/views/projects/issues/_merge_requests.html.haml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 10456189e214..f20779f2fbb4 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -42,12 +42,12 @@ def ci_icon_for_status(status) icon(icon_name + ' fw') end - def render_ci_status(ci_commit, placement: 'left') + def render_ci_status(ci_commit, tooltip_placement: 'auto left') link_to ci_status_icon(ci_commit), ci_status_path(ci_commit), class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}", title: "Build #{ci_status_label(ci_commit)}", - data: { toggle: 'tooltip', placement: placement } + data: { toggle: 'tooltip', placement: tooltip_placement } end def no_runners_for_project?(project) diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index 3eb0926b7466..d9868ad1f0a5 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -7,7 +7,7 @@ %li %span.merge-request-ci-status - if merge_request.ci_commit - = render_ci_status(merge_request.ci_commit, placement: 'bottom') + = render_ci_status(merge_request.ci_commit) - elsif has_any_ci = icon('blank fw') %span.merge-request-id -- GitLab From ad2e5d1bfbc125ef9d4bea5ef3e7a519612cd291 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran <alfredo@gitlab.com> Date: Thu, 10 Mar 2016 13:10:40 -0500 Subject: [PATCH 142/223] Avoid blinking of editor area on iOS --- app/assets/stylesheets/framework/files.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 07907e6e5a63..eb0ae0327321 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -169,6 +169,7 @@ */ &.code { padding: 0; + -webkit-overflow-scrolling: auto; // Issue #13987 } } } -- GitLab From 9818a49118b575ebeb47597cfe689b25ac796957 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran <alfredo@gitlab.com> Date: Thu, 10 Mar 2016 13:14:10 -0500 Subject: [PATCH 143/223] Prevent `.max-width-marker` to break layout on mobile --- app/assets/stylesheets/pages/commit.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index e53d6fc6bdc1..74be90b7737b 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -90,6 +90,7 @@ position: relative; font-family: $monospace_font; $left: 12px; + overflow: hidden; // Issue #13987 .max-width-marker { width: 72ch; color: rgba(0, 0, 0, 0.0); -- GitLab From 19f4211d910df9cb94c939e06c9e3484cf378020 Mon Sep 17 00:00:00 2001 From: "A. Felipe Cabargas Madrid" <felipe.cabargas@gmail.com> Date: Thu, 10 Mar 2016 15:54:13 -0300 Subject: [PATCH 144/223] Added redcarpet usage reference to markdown documentation --- doc/markdown/markdown.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index cbf57db56846..e399524e5214 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -29,6 +29,8 @@ ## GitLab Flavored Markdown (GFM) +_GitLab uses the [redcarpet ruby library][redcarpet] for markdown processing._ + For GitLab we developed something we call "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. You can use GFM in @@ -591,3 +593,4 @@ By including colons in the header row, you can align the text within that column - [Dillinger.io](http://dillinger.io) is a handy tool for testing standard markdown. [rouge]: http://rouge.jneen.net/ "Rouge website" +[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website" -- GitLab From 3a972e734d1f48b45200605592ab1a4abcbde0f1 Mon Sep 17 00:00:00 2001 From: "A. Felipe Cabargas Madrid" <felipe.cabargas@gmail.com> Date: Thu, 10 Mar 2016 16:11:59 -0300 Subject: [PATCH 145/223] Capitalize names of technologies --- doc/markdown/markdown.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index e399524e5214..593fc5a6eef8 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -29,7 +29,7 @@ ## GitLab Flavored Markdown (GFM) -_GitLab uses the [redcarpet ruby library][redcarpet] for markdown processing._ +_GitLab uses the [Redcarpet Ruby library][redcarpet] for Markdown processing._ For GitLab we developed something we call "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. -- GitLab From e3de1d311e40708d305e8a8d32fdfe263240af10 Mon Sep 17 00:00:00 2001 From: "A. Felipe Cabargas Madrid" <felipe.cabargas@gmail.com> Date: Thu, 10 Mar 2016 16:12:50 -0300 Subject: [PATCH 146/223] Capitalize Rouge mentions --- doc/markdown/markdown.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index 593fc5a6eef8..e6eb1cf38197 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -90,8 +90,8 @@ GFM will autolink almost any URL you copy and paste into your text. ## Code and Syntax Highlighting -_GitLab uses the [rouge ruby library][rouge] for syntax highlighting. For a -list of supported languages visit the rouge website._ +_GitLab uses the [Rouge Ruby library][rouge] for syntax highlighting. For a +list of supported languages visit the Rouge website._ Blocks of code are either fenced by lines with three back-ticks <code>```</code>, or are indented with four spaces. Only the fenced code blocks support syntax highlighting. -- GitLab From 9d810dcdf3efe2da610ef979917f2e1d3cff8d94 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran <alfredo@gitlab.com> Date: Thu, 10 Mar 2016 14:31:21 -0500 Subject: [PATCH 147/223] Use full URL to reference issues --- app/assets/stylesheets/framework/files.scss | 2 +- app/assets/stylesheets/pages/commit.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index eb0ae0327321..b034a4882c1a 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -169,7 +169,7 @@ */ &.code { padding: 0; - -webkit-overflow-scrolling: auto; // Issue #13987 + -webkit-overflow-scrolling: auto; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987 } } } diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 74be90b7737b..c0cc30d33a64 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -90,7 +90,7 @@ position: relative; font-family: $monospace_font; $left: 12px; - overflow: hidden; // Issue #13987 + overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987 .max-width-marker { width: 72ch; color: rgba(0, 0, 0, 0.0); -- GitLab From 111fea9fb782a74edd507cbd66be2376c2f39207 Mon Sep 17 00:00:00 2001 From: ashleys <ashley@gitlab.com> Date: Thu, 10 Mar 2016 14:48:29 -0500 Subject: [PATCH 148/223] web hooks to webhooks --- CHANGELOG | 28 +++++++++---------- app/helpers/search_helper.rb | 2 +- app/services/git_push_service.rb | 2 +- .../layouts/nav/_project_settings.html.haml | 4 +-- app/views/projects/hooks/index.html.haml | 10 +++---- doc/README.md | 6 ++-- doc/hooks/custom_hooks.md | 2 +- doc/raketasks/README.md | 2 +- doc/raketasks/web_hooks.md | 14 +++++----- doc/security/README.md | 2 +- doc/security/webhooks.md | 12 ++++---- doc/web_hooks/web_hooks.md | 10 +++---- features/steps/project/active_tab.rb | 4 +-- features/steps/project/hooks.rb | 4 +-- lib/gitlab/push_data_builder.rb | 2 +- lib/tasks/gitlab/web_hook.rake | 14 +++++----- spec/models/hooks/service_hook_spec.rb | 2 +- spec/models/hooks/web_hook_spec.rb | 2 +- spec/services/git_push_service_spec.rb | 4 +-- spec/services/git_tag_push_service_spec.rb | 4 +-- spec/workers/post_receive_spec.rb | 2 +- 21 files changed, 66 insertions(+), 66 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3c5627459e50..4801c58ce8e4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -82,7 +82,7 @@ v 8.5.1 v 8.5.0 - Fix duplicate "me" in tooltip of the "thumbsup" awards Emoji (Stan Hu) - Cache various Repository methods to improve performance (Yorick Peterse) - - Fix duplicated branch creation/deletion Web hooks/service notifications when using Web UI (Stan Hu) + - Fix duplicated branch creation/deletion Webhooks/service notifications when using Web UI (Stan Hu) - Ensure rake tasks that don't need a DB connection can be run without one - Update New Relic gem to 3.14.1.311 (Stan Hu) - Add "visibility" flag to GET /projects api endpoint @@ -215,7 +215,7 @@ v 8.4.0 - Add housekeeping function to project settings page - The default GitLab logo now acts as a loading indicator - Fix caching issue where build status was not updating in project dashboard (Stan Hu) - - Accept 2xx status codes for successful Web hook triggers (Stan Hu) + - Accept 2xx status codes for successful Webhook triggers (Stan Hu) - Fix missing date of month in network graph when commits span a month (Stan Hu) - Expire view caches when application settings change (e.g. Gravatar disabled) (Stan Hu) - Don't notify users twice if they are both project watchers and subscribers (Stan Hu) @@ -315,7 +315,7 @@ v 8.3.0 - Fix broken group avatar upload under "New group" (Stan Hu) - Update project repositorize size and commit count during import:repos task (Stan Hu) - Fix API setting of 'public' attribute to false will make a project private (Stan Hu) - - Handle and report SSL errors in Web hook test (Stan Hu) + - Handle and report SSL errors in Webhook test (Stan Hu) - Bump Redis requirement to 2.8 for Sidekiq 4 (Stan Hu) - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera) - WIP identifier on merge requests no longer requires trailing space @@ -535,7 +535,7 @@ v 8.1.0 - Ensure code blocks are properly highlighted after a note is updated - Fix wrong access level badge on MR comments - Hide password in the service settings form - - Move CI web hooks page to project settings area + - Move CI webhooks page to project settings area - Fix User Identities API. It now allows you to properly create or update user's identities. - Add user preference to change layout width (Peter Göbel) - Use commit status in merge request widget as preferred source of CI status @@ -578,7 +578,7 @@ v 8.0.3 - Fix URL shown in Slack notifications - Fix bug where projects would appear to be stuck in the forked import state (Stan Hu) - Fix Error 500 in creating merge requests with > 1000 diffs (Stan Hu) - - Add work_in_progress key to MR web hooks (Ben Boeckel) + - Add work_in_progress key to MR webhooks (Ben Boeckel) v 8.0.2 - Fix default avatar not rendering in network graph (Stan Hu) @@ -869,7 +869,7 @@ v 7.12.0 - Fix milestone "Browse Issues" button. - Set milestone on new issue when creating issue from index with milestone filter active. - Make namespace API available to all users (Stan Hu) - - Add web hook support for note events (Stan Hu) + - Add webhook support for note events (Stan Hu) - Disable "New Issue" and "New Merge Request" buttons when features are disabled in project settings (Stan Hu) - Remove Rack Attack monkey patches and bump to version 4.3.0 (Stan Hu) - Fix clone URL losing selection after a single click in Safari and Chrome (Stan Hu) @@ -976,7 +976,7 @@ v 7.11.0 - Add "Create Merge Request" buttons to commits and branches pages and push event. - Show user roles by comments. - Fix automatic blocking of auto-created users from Active Directory. - - Call merge request web hook for each new commits (Arthur Gautier) + - Call merge request webhook for each new commits (Arthur Gautier) - Use SIGKILL by default in Sidekiq::MemoryKiller - Fix mentioning of private groups. - Add style for <kbd> element in markdown @@ -1150,7 +1150,7 @@ v 7.9.0 - Add brakeman (security scanner for Ruby on Rails) - Slack username and channel options - Add grouped milestones from all projects to dashboard. - - Web hook sends pusher email as well as commiter + - Webhook sends pusher email as well as commiter - Add Bitbucket omniauth provider. - Add Bitbucket importer. - Support referencing issues to a project whose name starts with a digit @@ -1273,7 +1273,7 @@ v 7.8.0 - Allow notification email to be set separately from primary email. - API: Add support for editing an existing project (Mika Mäenpää and Hannes Rosenögger) - Don't have Markdown preview fail for long comments/wiki pages. - - When test web hook - show error message instead of 500 error page if connection to hook url was reset + - When test webhook - show error message instead of 500 error page if connection to hook url was reset - Added support for firing system hooks on group create/destroy and adding/removing users to group (Boyan Tabakov) - Added persistent collapse button for left side nav bar (Jason Blanchard) - Prevent losing unsaved comments by automatically restoring them when comment page is loaded again. @@ -1290,7 +1290,7 @@ v 7.8.0 - Show projects user contributed to on user page. Show stars near project on user page. - Improve database performance for GitLab - Add Asana service (Jeremy Benoist) - - Improve project web hooks with extra data + - Improve project webhooks with extra data v 7.7.2 - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch @@ -1775,7 +1775,7 @@ v 6.4.0 - Side-by-side diff view (Steven Thonus) - Internal projects (Jason Hollingsworth) - Allow removal of avatar (Drew Blessing) - - Project web hooks now support issues and merge request events + - Project webhooks now support issues and merge request events - Visiting project page while not logged in will redirect to sign-in instead of 404 (Jason Hollingsworth) - Expire event cache on avatar creation/removal (Drew Blessing) - Archiving old projects (Steven Thonus) @@ -1845,7 +1845,7 @@ v 6.2.0 - Added search for projects by name to api (Izaak Alpert) - Make default user theme configurable (Izaak Alpert) - Update logic for validates_merge_request for tree of MR (Andrew Kumanyaev) - - Rake tasks for web hooks management (Jonhnny Weslley) + - Rake tasks for webhooks management (Jonhnny Weslley) - Extended User API to expose admin and can_create_group for user creation/updating (Boyan Tabakov) - API: Remove group - API: Remove project @@ -2048,7 +2048,7 @@ v 4.2.0 - Async gitolite calls - added satellites logs - can_create_group, can_create_team booleans for User - - Process web hooks async + - Process webhooks async - GFM: Fix images escaped inside links - Network graph improved - Switchable branches for network graph @@ -2082,7 +2082,7 @@ v 4.1.0 v 4.0.0 - Remove project code and path from API. Use id instead - - Return valid cloneable url to repo for web hook + - Return valid cloneable url to repo for webhook - Fixed backup issue - Reorganized settings - Fixed commits compare diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 1eb790b1796f..eb84c04d22de 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -40,7 +40,7 @@ def help_autocomplete { label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") }, { label: "help: SSH Keys Help", url: help_page_path("ssh", "README") }, { label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, - { label: "help: Web Hooks Help", url: help_page_path("web_hooks", "web_hooks") }, + { label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") }, { label: "help: Workflow Help", url: help_page_path("workflow", "README") }, ] end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 93a16e88967b..bd31a617747c 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -12,7 +12,7 @@ class GitPushService < BaseService # 1. Creates the push event # 2. Updates merge requests # 3. Recognizes cross-references from commit messages - # 4. Executes the project's web hooks + # 4. Executes the project's webhooks # 5. Executes the project's services # 6. Checks if the project's main language has changed # diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 970da78a5c9c..3359716202f1 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -19,10 +19,10 @@ %span Deploy Keys = nav_link(controller: :hooks) do - = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks' do + = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do = icon('link fw') %span - Web Hooks + Webhooks = nav_link(controller: :services) do = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do = icon('cogs fw') diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index a0511819c9f9..67d016bd8710 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -1,9 +1,9 @@ -- page_title "Web Hooks" +- page_title "Webhooks" %h3.page-title - Web hooks + Webhooks %p.light - #{link_to "Web hooks ", help_page_path("web_hooks", "web_hooks"), class: "vlink"} can be + #{link_to "Webhooks ", help_page_path("web_hooks", "web_hooks"), class: "vlink"} can be used for binding events when something is happening within the project. %hr.clearfix @@ -70,12 +70,12 @@ = f.check_box :enable_ssl_verification %strong Enable SSL verification .form-actions - = f.submit "Add Web Hook", class: "btn btn-create" + = f.submit "Add Webhook", class: "btn btn-create" -if @hooks.any? .panel.panel-default .panel-heading - Web hooks (#{@hooks.count}) + Webhooks (#{@hooks.count}) %ul.well-list - @hooks.each do |hook| %li diff --git a/doc/README.md b/doc/README.md index be6c5f96ea15..0ca30e4e0f23 100644 --- a/doc/README.md +++ b/doc/README.md @@ -13,7 +13,7 @@ - [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat. - [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects. - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. -- [Web hooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. +- [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. - [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. ## CI User documentation @@ -54,7 +54,7 @@ be linked with your base image. Below is a list of examples you may use: ## Administrator documentation -- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when web hooks aren't enough. +- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when webhooks aren't enough. - [Install](install/README.md) Requirements, directory structures and installation from source. - [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components - [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, LDAP and Twitter. @@ -63,7 +63,7 @@ be linked with your base image. Below is a list of examples you may use: - [Log system](logs/logs.md) Log system. - [Environment Variables](administration/environment_variables.md) to configure GitLab. - [Operations](operations/README.md) Keeping GitLab up and running -- [Raketasks](raketasks/README.md) Backups, maintenance, automatic web hook setup and the importing of projects. +- [Raketasks](raketasks/README.md) Backups, maintenance, automatic webhook setup and the importing of projects. - [Security](security/README.md) Learn what you can do to further secure your GitLab instance. - [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed. - [Update](update/README.md) Update guides to upgrade your installation. diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md index 5a4b2f43ba77..15051dd76f97 100644 --- a/doc/hooks/custom_hooks.md +++ b/doc/hooks/custom_hooks.md @@ -2,7 +2,7 @@ **Note: Custom git hooks must be configured on the filesystem of the GitLab server. Only GitLab server administrators will be able to complete these tasks. -Please explore [web hooks](doc/web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).** +Please explore [webhooks](doc/web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).** Git natively supports hooks that are executed on different actions. Examples of server-side git hooks include pre-receive, post-receive, and update. diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md index cc8a22cd0032..6be954ad68ba 100644 --- a/doc/raketasks/README.md +++ b/doc/raketasks/README.md @@ -6,6 +6,6 @@ - [Features](features.md) - [Maintenance](maintenance.md) and self-checks - [User management](user_management.md) -- [Web hooks](web_hooks.md) +- [Webhooks](web_hooks.md) - [Import](import.md) of git repositories in bulk - [Rebuild authorized_keys file](http://doc.gitlab.com/ce/raketasks/maintenance.html#rebuild-authorized_keys-file) task for administrators diff --git a/doc/raketasks/web_hooks.md b/doc/raketasks/web_hooks.md index 5a8b94af9b4b..2ebf7c48f4e9 100644 --- a/doc/raketasks/web_hooks.md +++ b/doc/raketasks/web_hooks.md @@ -1,41 +1,41 @@ -# Web hooks +# Webhooks -## Add a web hook for **ALL** projects: +## Add a webhook for **ALL** projects: # omnibus-gitlab sudo gitlab-rake gitlab:web_hook:add URL="http://example.com/hook" # source installations bundle exec rake gitlab:web_hook:add URL="http://example.com/hook" RAILS_ENV=production -## Add a web hook for projects in a given **NAMESPACE**: +## Add a webhook for projects in a given **NAMESPACE**: # omnibus-gitlab sudo gitlab-rake gitlab:web_hook:add URL="http://example.com/hook" NAMESPACE=acme # source installations bundle exec rake gitlab:web_hook:add URL="http://example.com/hook" NAMESPACE=acme RAILS_ENV=production -## Remove a web hook from **ALL** projects using: +## Remove a webhook from **ALL** projects using: # omnibus-gitlab sudo gitlab-rake gitlab:web_hook:rm URL="http://example.com/hook" # source installations bundle exec rake gitlab:web_hook:rm URL="http://example.com/hook" RAILS_ENV=production -## Remove a web hook from projects in a given **NAMESPACE**: +## Remove a webhook from projects in a given **NAMESPACE**: # omnibus-gitlab sudo gitlab-rake gitlab:web_hook:rm URL="http://example.com/hook" NAMESPACE=acme # source installations bundle exec rake gitlab:web_hook:rm URL="http://example.com/hook" NAMESPACE=acme RAILS_ENV=production -## List **ALL** web hooks: +## List **ALL** webhooks: # omnibus-gitlab sudo gitlab-rake gitlab:web_hook:list # source installations bundle exec rake gitlab:web_hook:list RAILS_ENV=production -## List the web hooks from projects in a given **NAMESPACE**: +## List the webhooks from projects in a given **NAMESPACE**: # omnibus-gitlab sudo gitlab-rake gitlab:web_hook:list NAMESPACE=/ diff --git a/doc/security/README.md b/doc/security/README.md index be1abb88c3db..4cd0fdd40944 100644 --- a/doc/security/README.md +++ b/doc/security/README.md @@ -2,7 +2,7 @@ - [Password length limits](password_length_limits.md) - [Rack attack](rack_attack.md) -- [Web Hooks and insecure internal web services](webhooks.md) +- [Webhooks and insecure internal web services](webhooks.md) - [Information exclusivity](information_exclusivity.md) - [Reset your root password](reset_root_password.md) - [User File Uploads](user_file_uploads.md) diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md index 1e9d33e87c31..bb46aebf4b5a 100644 --- a/doc/security/webhooks.md +++ b/doc/security/webhooks.md @@ -1,13 +1,13 @@ -# Web Hooks and insecure internal web services +# Webhooks and insecure internal web services -If you have non-GitLab web services running on your GitLab server or within its local network, these may be vulnerable to exploitation via Web Hooks. +If you have non-GitLab web services running on your GitLab server or within its local network, these may be vulnerable to exploitation via Webhooks. -With [Web Hooks](../web_hooks/web_hooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way. +With [Webhooks](../web_hooks/web_hooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way. -Things get hairy, however, when a Web Hook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the web hook is triggered and the POST request is sent. +Things get hairy, however, when a Webhook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the webhook is triggered and the POST request is sent. -Because Web Hook requests are made by the GitLab server itself, these have complete access to everything running on the server (http://localhost:123) or within the server's local network (http://192.168.1.12:345), even if these services are otherwise protected and inaccessible from the outside world. +Because Webhook requests are made by the GitLab server itself, these have complete access to everything running on the server (http://localhost:123) or within the server's local network (http://192.168.1.12:345), even if these services are otherwise protected and inaccessible from the outside world. -If a web service does not require authentication, Web Hooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete". +If a web service does not require authentication, Webhooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete". To prevent this type of exploitation from happening, make sure that you are aware of every web service GitLab could potentially have access to, and that all of these are set up to require authentication for every potentially destructive command. Enabling authentication but leaving a default password is not enough. \ No newline at end of file diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index e2b53c45ab18..870494271395 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -1,4 +1,4 @@ -# Web hooks +# Webhooks _**Note:** Starting from GitLab 8.5:_ @@ -7,11 +7,11 @@ Starting from GitLab 8.5:_ - _the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key_ - _the `project.http_url` key is deprecated in favor of the `project.git_http_url` key_ -Project web hooks allow you to trigger an URL if new code is pushed or a new issue is created. +Project webhooks allow you to trigger an URL if new code is pushed or a new issue is created. -You can configure web hooks to listen for specific events like pushes, issues or merge requests. GitLab will send a POST request with data to the web hook URL. +You can configure webhooks to listen for specific events like pushes, issues or merge requests. GitLab will send a POST request with data to the webhook URL. -Web hooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server. +Webhooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server. ## SSL Verification @@ -19,7 +19,7 @@ By default, the SSL certificate of the webhook endpoint is verified based on an internal list of Certificate Authorities, which means the certificate cannot be self-signed. -You can turn this off in the web hook settings in your GitLab projects. +You can turn this off in the webhook settings in your GitLab projects.  diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index 9e96fa5ba494..19d81453d8cd 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -26,7 +26,7 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps end step 'I click the "Hooks" tab' do - click_link('Web Hooks') + click_link('Webhooks') end step 'I click the "Deploy Keys" tab' do @@ -42,7 +42,7 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps end step 'the active sub nav should be Hooks' do - ensure_active_sub_nav('Web Hooks') + ensure_active_sub_nav('Webhooks') end step 'the active sub nav should be Deploy Keys' do diff --git a/features/steps/project/hooks.rb b/features/steps/project/hooks.rb index be4db770948c..4994df589a7e 100644 --- a/features/steps/project/hooks.rb +++ b/features/steps/project/hooks.rb @@ -25,14 +25,14 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps step 'I submit new hook' do @url = FFaker::Internet.uri("http") fill_in "hook_url", with: @url - expect { click_button "Add Web Hook" }.to change(ProjectHook, :count).by(1) + expect { click_button "Add Webhook" }.to change(ProjectHook, :count).by(1) end step 'I submit new hook with SSL verification enabled' do @url = FFaker::Internet.uri("http") fill_in "hook_url", with: @url check "hook_enable_ssl_verification" - expect { click_button "Add Web Hook" }.to change(ProjectHook, :count).by(1) + expect { click_button "Add Webhook" }.to change(ProjectHook, :count).by(1) end step 'I should see newly created hook' do diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb index da1c15fef619..97d1edab9c13 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -63,7 +63,7 @@ def build(project, user, oldrev, newrev, ref, commits = [], message = nil) end # This method provide a sample data generated with - # existing project and commits to test web hooks + # existing project and commits to test webhooks def build_sample(project, user) commits = project.repository.commits(project.default_branch, nil, 3) ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}" diff --git a/lib/tasks/gitlab/web_hook.rake b/lib/tasks/gitlab/web_hook.rake index 76e443e55ee2..cc0f668474ed 100644 --- a/lib/tasks/gitlab/web_hook.rake +++ b/lib/tasks/gitlab/web_hook.rake @@ -1,13 +1,13 @@ namespace :gitlab do namespace :web_hook do - desc "GitLab | Adds a web hook to the projects" + desc "GitLab | Adds a webhook to the projects" task :add => :environment do web_hook_url = ENV['URL'] namespace_path = ENV['NAMESPACE'] projects = find_projects(namespace_path) - puts "Adding web hook '#{web_hook_url}' to:" + puts "Adding webhook '#{web_hook_url}' to:" projects.find_each(batch_size: 1000) do |project| print "- #{project.name} ... " web_hook = project.hooks.new(url: web_hook_url) @@ -20,7 +20,7 @@ namespace :gitlab do end end - desc "GitLab | Remove a web hook from the projects" + desc "GitLab | Remove a webhook from the projects" task :rm => :environment do web_hook_url = ENV['URL'] namespace_path = ENV['NAMESPACE'] @@ -28,12 +28,12 @@ namespace :gitlab do projects = find_projects(namespace_path) projects_ids = projects.pluck(:id) - puts "Removing web hooks with the url '#{web_hook_url}' ... " + puts "Removing webhooks with the url '#{web_hook_url}' ... " count = WebHook.where(url: web_hook_url, project_id: projects_ids, type: 'ProjectHook').delete_all - puts "#{count} web hooks were removed." + puts "#{count} webhooks were removed." end - desc "GitLab | List web hooks" + desc "GitLab | List webhooks" task :list => :environment do namespace_path = ENV['NAMESPACE'] @@ -43,7 +43,7 @@ namespace :gitlab do puts "#{hook.project.name.truncate(20).ljust(20)} -> #{hook.url}" end - puts "\n#{web_hooks.size} web hooks found." + puts "\n#{web_hooks.size} webhooks found." end end diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb index 1455661485bf..f800f415bd2d 100644 --- a/spec/models/hooks/service_hook_spec.rb +++ b/spec/models/hooks/service_hook_spec.rb @@ -31,7 +31,7 @@ WebMock.stub_request(:post, @service_hook.url) end - it "POSTs to the web hook URL" do + it "POSTs to the webhook URL" do @service_hook.execute(@data) expect(WebMock).to have_requested(:post, @service_hook.url).with( headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Service Hook' } diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 6ea99952a8f4..04bc2dcfb16a 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -52,7 +52,7 @@ WebMock.stub_request(:post, @project_hook.url) end - it "POSTs to the web hook URL" do + it "POSTs to the webhook URL" do @project_hook.execute(@data, 'push_hooks') expect(WebMock).to have_requested(:post, @project_hook.url).with( headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Push Hook' } diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 82813cee2272..a7e2e1b17920 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -172,8 +172,8 @@ end - describe "Web Hooks" do - context "execute web hooks" do + describe "Webhooks" do + context "execute webhooks" do it "when pushing a branch for the first time" do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb index b982274c5290..cc780587e74a 100644 --- a/spec/services/git_tag_push_service_spec.rb +++ b/spec/services/git_tag_push_service_spec.rb @@ -78,8 +78,8 @@ end end - describe "Web Hooks" do - context "execute web hooks" do + describe "Webhooks" do + context "execute webhooks" do it "when pushing tags" do expect(project).to receive(:execute_hooks) service.execute(project, user, 'oldrev', 'newrev', 'refs/tags/v1.0.0') diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index e4151b9bb6a6..0265dbe9c666 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -11,7 +11,7 @@ end end - context "web hook" do + context "webhook" do let(:project) { create(:project) } let(:key) { create(:key, user: project.owner) } let(:key_id) { key.shell_id } -- GitLab From 8b88a627f9bb3df1f2e70d2eba91c06f307c5a68 Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Thu, 10 Mar 2016 15:24:37 -0500 Subject: [PATCH 149/223] Fix help_autocomplete alignment OCD hooooooooo! [ci skip] --- app/helpers/search_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index eb84c04d22de..494dad0b41ef 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -40,7 +40,7 @@ def help_autocomplete { label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") }, { label: "help: SSH Keys Help", url: help_page_path("ssh", "README") }, { label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, - { label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") }, + { label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") }, { label: "help: Workflow Help", url: help_page_path("workflow", "README") }, ] end -- GitLab From b35421d20d237b04fd72bc7e3a0a4147f2d2f373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Thu, 10 Mar 2016 21:59:52 +0100 Subject: [PATCH 150/223] Add 8.5.5 CHANGELOG items [ci skip] --- CHANGELOG | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4801c58ce8e4..d48604de5543 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -30,8 +30,10 @@ v 8.6.0 (unreleased) - Move group activity to separate page v 8.5.5 - - Ensure removing a project removes associated Todo entries. - - Prevent a 500 error in Todos when author was removed. + - Ensure removing a project removes associated Todo entries + - Prevent a 500 error in Todos when author was removed + - Fix pagination for filtered dashboard and explore pages + - Fix "Show all" link behavior v 8.5.4 - Do not cache requests for badges (including builds badge) -- GitLab From 1520fa29af201979c80a14eafe304ef59a211d53 Mon Sep 17 00:00:00 2001 From: Patricio Cano <suprnova32@gmail.com> Date: Thu, 10 Mar 2016 16:07:10 -0500 Subject: [PATCH 151/223] Upgrade `omniauth-saml` to 1.5.0 and document it's new capabilities. --- CHANGELOG | 1 + Gemfile | 2 +- Gemfile.lock | 8 +++---- doc/integration/saml.md | 52 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d48604de5543..61ca5dbcba83 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,7 @@ v 8.6.0 (unreleased) - Fix issue when pushing to projects ending in .wiki - Fix avatar stretching by providing a cropping feature (Johann Pardanaud) - Don't load all of GitLab in mail_room + - Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set - Memoize @group in Admin::GroupsController (Yatish Mehta) - Indicate how much an MR diverged from the target branch (Pierre de La Morinerie) - Strip leading and trailing spaces in URL validator (evuez) diff --git a/Gemfile b/Gemfile index 7e70761a77a1..1550afb1b56e 100644 --- a/Gemfile +++ b/Gemfile @@ -30,7 +30,7 @@ gem 'omniauth-github', '~> 1.1.1' gem 'omniauth-gitlab', '~> 1.0.0' gem 'omniauth-google-oauth2', '~> 0.2.0' gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos -gem 'omniauth-saml', '~> 1.4.2' +gem 'omniauth-saml', '~> 1.5.0' gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd', '~> 2.2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 3777528ea4b6..d4e28db00d6b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -532,8 +532,8 @@ GEM omniauth-oauth2 (1.3.1) oauth2 (~> 1.0) omniauth (~> 1.2) - omniauth-saml (1.4.2) - omniauth (~> 1.1) + omniauth-saml (1.5.0) + omniauth (~> 1.3) ruby-saml (~> 1.1, >= 1.1.1) omniauth-shibboleth (1.2.1) omniauth (>= 1.0.0) @@ -692,7 +692,7 @@ GEM ruby-fogbugz (0.2.1) crack (~> 0.4) ruby-progressbar (1.7.5) - ruby-saml (1.1.1) + ruby-saml (1.1.2) nokogiri (>= 1.5.10) uuid (~> 2.3) ruby2ruby (2.2.0) @@ -975,7 +975,7 @@ DEPENDENCIES omniauth-gitlab (~> 1.0.0) omniauth-google-oauth2 (~> 0.2.0) omniauth-kerberos (~> 0.3.0) - omniauth-saml (~> 1.4.2) + omniauth-saml (~> 1.5.0) omniauth-shibboleth (~> 1.2.0) omniauth-twitter (~> 1.2.0) omniauth_crowd (~> 2.2.0) diff --git a/doc/integration/saml.md b/doc/integration/saml.md index 148c4ac18867..1c7c114dbded 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -131,6 +131,58 @@ On the sign in page there should now be a SAML button below the regular sign in Click the icon to begin the authentication process. If everything goes well the user will be returned to GitLab and will be signed in. +## Customization + +### attribute_statements: + +>**Note:** +This setting is only available on GitLab 8.6 and above. +This setting should only be used to map attributes that are part of the +OmniAuth info hash schema. + +Used to map Attribute Names in a SAMLResponse to entries in the OmniAuth +[info hash](https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema#schema-10-and-later). + +For example, if your SAMLResponse contains an Attribute called 'EmailAddress', +specify `{ email: ['EmailAddress'] }` to map the Attribute to the +corresponding key in the info hash. URI-named Attributes are also supported, e.g. +`{ email: ['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] }`. + +This setting allows you tell GitLab where to look for certain attributes required +to create an account. Like mentioned above, if your IdP send the user's email +address as `EmailAddress` instead of `email`, let GitLab know by setting it on +your configuration: + +```yaml +args: { + assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + idp_sso_target_url: 'https://login.example.com/idp', + issuer: 'https://gitlab.example.com', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', + attribute_statements: { :email => ['EmailAddress'] } +} +``` + +### allowed_clock_drift: + +The clock of the Identity Provider may drift slightly ahead of your system clocks. +To allow for a small amount of clock drift you can use this argument within your +settings. Its value must be given in a number (and/or fraction) of seconds. The +value given is added to the current time at which the response is validated. + +```yaml +args: { + assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', + idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', + idp_sso_target_url: 'https://login.example.com/idp', + issuer: 'https://gitlab.example.com', + name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', + attribute_statements: { :email => ['EmailAddress'] }, + allowed_clock_drift: 1 # for one second clock drift +} +``` + ## Troubleshooting ### 500 error after login -- GitLab From ae305175862140c594df014759a2f25ae332a9e4 Mon Sep 17 00:00:00 2001 From: Patricio Cano <suprnova32@gmail.com> Date: Thu, 10 Mar 2016 16:52:52 -0500 Subject: [PATCH 152/223] Consistently use the same hash syntax in the new documentation. --- doc/integration/saml.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/integration/saml.md b/doc/integration/saml.md index 1c7c114dbded..b61b7cb96780 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -160,7 +160,7 @@ args: { idp_sso_target_url: 'https://login.example.com/idp', issuer: 'https://gitlab.example.com', name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', - attribute_statements: { :email => ['EmailAddress'] } + attribute_statements: { email: ['EmailAddress'] } } ``` @@ -178,7 +178,7 @@ args: { idp_sso_target_url: 'https://login.example.com/idp', issuer: 'https://gitlab.example.com', name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', - attribute_statements: { :email => ['EmailAddress'] }, + attribute_statements: { email: ['EmailAddress'] }, allowed_clock_drift: 1 # for one second clock drift } ``` -- GitLab From 57a8f4997588ef81f6196d373e00aafcaa51fb8e Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Thu, 10 Mar 2016 18:55:18 -0300 Subject: [PATCH 153/223] Fix importing PR's from GitHub when the source repo was removed --- lib/gitlab/github_import/importer.rb | 11 ++-- .../github_import/pull_request_formatter.rb | 10 +-- .../pull_request_formatter_spec.rb | 64 ++++++++----------- 3 files changed, 40 insertions(+), 45 deletions(-) diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index e2a85f298259..172c5441e36f 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -45,10 +45,13 @@ def import_pull_requests direction: :asc).each do |raw_data| pull_request = PullRequestFormatter.new(project, raw_data) - if !pull_request.cross_project? && pull_request.valid? - merge_request = MergeRequest.create!(pull_request.attributes) - import_comments(pull_request.number, merge_request) - import_comments_on_diff(pull_request.number, merge_request) + if pull_request.valid? + merge_request = MergeRequest.new(pull_request.attributes) + + if merge_request.save + import_comments(pull_request.number, merge_request) + import_comments_on_diff(pull_request.number, merge_request) + end end end diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index f96fed0f5cfb..4e507b090e8d 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -17,16 +17,12 @@ def attributes } end - def cross_project? - source_repo.id != target_repo.id - end - def number raw_data.number end def valid? - source_branch.present? && target_branch.present? + !cross_project? && source_branch.present? && target_branch.present? end private @@ -53,6 +49,10 @@ def body raw_data.body || "" end + def cross_project? + source_repo.present? && target_repo.present? && source_repo.id != target_repo.id + end + def description formatter.author_line(author) + body end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 6cebcb5009ad..e49dcb42342f 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -127,34 +127,6 @@ end end - describe '#cross_project?' do - context 'when source, and target repositories are the same' do - let(:raw_data) { OpenStruct.new(base_data) } - - it 'returns false' do - expect(pull_request.cross_project?).to eq false - end - end - - context 'when source repo is a fork' do - let(:source_repo) { OpenStruct.new(id: 2, fork: true) } - let(:raw_data) { OpenStruct.new(base_data) } - - it 'returns true' do - expect(pull_request.cross_project?).to eq true - end - end - - context 'when target repo is a fork' do - let(:target_repo) { OpenStruct.new(id: 2, fork: true) } - let(:raw_data) { OpenStruct.new(base_data) } - - it 'returns true' do - expect(pull_request.cross_project?).to eq true - end - end - end - describe '#number' do let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) } @@ -166,24 +138,44 @@ describe '#valid?' do let(:invalid_branch) { OpenStruct.new(ref: 'invalid-branch') } - context 'when source and target branches exists' do - let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: target_branch)) } + context 'when source, and target repositories are the same' do + context 'and source and target branches exists' do + let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: target_branch)) } - it 'returns true' do - expect(pull_request.valid?).to eq true + it 'returns true' do + expect(pull_request.valid?).to eq true + end + end + + context 'and source branch doesn not exists' do + let(:raw_data) { OpenStruct.new(base_data.merge(head: invalid_branch, base: target_branch)) } + + it 'returns false' do + expect(pull_request.valid?).to eq false + end + end + + context 'and target branch doesn not exists' do + let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: invalid_branch)) } + + it 'returns false' do + expect(pull_request.valid?).to eq false + end end end - context 'when source branch doesn not exists' do - let(:raw_data) { OpenStruct.new(base_data.merge(head: invalid_branch, base: target_branch)) } + context 'when source repo is a fork' do + let(:source_repo) { OpenStruct.new(id: 2, fork: true) } + let(:raw_data) { OpenStruct.new(base_data) } it 'returns false' do expect(pull_request.valid?).to eq false end end - context 'when target branch doesn not exists' do - let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: invalid_branch)) } + context 'when target repo is a fork' do + let(:target_repo) { OpenStruct.new(id: 2, fork: true) } + let(:raw_data) { OpenStruct.new(base_data) } it 'returns false' do expect(pull_request.valid?).to eq false -- GitLab From 127345274f7f2cea431cc18667d30276bb56a9cd Mon Sep 17 00:00:00 2001 From: Josh Frye <joshfng@gmail.com> Date: Thu, 10 Mar 2016 21:22:46 -0500 Subject: [PATCH 154/223] Filter import_url params because they may contain auth information. Fixes #14199 --- config/application.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/application.rb b/config/application.rb index d8d1e7b46795..2b103c4592db 100644 --- a/config/application.rb +++ b/config/application.rb @@ -34,7 +34,7 @@ class Application < Rails::Application config.encoding = "utf-8" # Configure sensitive parameters which will be filtered from the log file. - config.filter_parameters.push(:password, :password_confirmation, :private_token, :otp_attempt, :variables) + config.filter_parameters.push(:password, :password_confirmation, :private_token, :otp_attempt, :variables, :import_url) # Enable escaping HTML in JSON. config.active_support.escape_html_entities_in_json = true -- GitLab From e9a000f2944ea0144f145bf87a103d6083e2b063 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 11 Mar 2016 08:57:53 +0000 Subject: [PATCH 155/223] Removed deprecated bootstrap classes --- app/views/projects/issues/show.html.haml | 2 +- app/views/projects/merge_requests/show/_mr_title.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 2a7d0b0757d4..f5bb5d998bbd 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -18,7 +18,7 @@ %span.hidden-sm.hidden-md.hidden-lg = icon('circle-o') - %a.btn.btn-default.pull-right.visible-xs.gutter-toggle.js-sidebar-toggle{ href: "#" } + %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.js-sidebar-toggle{ href: "#" } = icon('angle-double-left') .issue-meta diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 83b056eef66a..a75c0d96c576 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -4,7 +4,7 @@ = @merge_request.state_human_name %span.hidden-sm.hidden-md.hidden-lg = icon(@merge_request.state_icon_name) - %a.btn.btn-default.pull-right.visible-xs.gutter-toggle.js-sidebar-toggle{ href: "#" } + %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.js-sidebar-toggle{ href: "#" } = icon('angle-double-left') .issue-meta %strong.identifier -- GitLab From 187bc123b4958cb249bb7d5a2221c9f545c2e61e Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 11 Mar 2016 09:00:19 +0000 Subject: [PATCH 156/223] Fixed missing variables --- app/assets/javascripts/ci/build.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee index 607bb6e965f7..7afe8bf79e26 100644 --- a/app/assets/javascripts/ci/build.coffee +++ b/app/assets/javascripts/ci/build.coffee @@ -45,10 +45,10 @@ class CiBuild $body = $('body') $buildTrace = $('#build-trace') - buildScroll.affix( + $buildScroll.affix( offset: bottom: -> - body.outerHeight() - (buildTrace.outerHeight() + buildTrace.offset().top) + $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top) ) @CiBuild = CiBuild -- GitLab From 60400835b1b81338577ff4e5e0467a01f27b5e10 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 1 Mar 2016 11:25:59 +0000 Subject: [PATCH 157/223] Updated UI of award emoji Closes #13878 --- app/assets/javascripts/awards_handler.coffee | 37 ++-- app/assets/stylesheets/framework/blocks.scss | 4 + app/assets/stylesheets/pages/awards.scss | 189 ++++++++---------- app/views/emojis/index.html.haml | 10 +- app/views/projects/issues/show.html.haml | 2 +- .../projects/merge_requests/_show.html.haml | 2 +- app/views/votes/_votes_block.html.haml | 27 +-- 7 files changed, 131 insertions(+), 140 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 8f89d3e61a2b..351f871de94a 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -1,6 +1,6 @@ class @AwardsHandler constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) -> - $(".add-award").click (event) => + $(".js-add-award").click (event) => event.stopPropagation() event.preventDefault() @@ -11,18 +11,28 @@ class @AwardsHandler if $(".emoji-menu").is(":visible") $(".emoji-menu").hide() + $(".awards").off "click" + $(".awards").on "click", ".js-emoji-btn", @handleClick + @renderFrequentlyUsedBlock() - @setupSearch() + + handleClick: (e) -> + e.preventDefault() + emoji = $(this).find(".icon").data "emoji" + awards_handler.addAward emoji showEmojiMenu: -> if $(".emoji-menu").length $(".emoji-menu").show() $("#emoji_search").focus() else - $.get "/emojis", (response) -> - $(".add-award").after response + $('.js-add-award').addClass "is-loading" + $.get "/emojis", (response) => + $('.js-add-award').removeClass "is-loading" + $(".js-award-holder").append response $(".emoji-menu").show() $("#emoji_search").focus() + @setupSearch() addAward: (emoji) -> emoji = @normilizeEmojiName(emoji) @@ -39,7 +49,7 @@ class @AwardsHandler if @isActive(emoji) @decrementCounter(emoji) else - counter = @findEmojiIcon(emoji).siblings(".counter") + counter = @findEmojiIcon(emoji).siblings(".js-counter") counter.text(parseInt(counter.text()) + 1) counter.parent().addClass("active") @addMeToAuthorList(emoji) @@ -53,7 +63,7 @@ class @AwardsHandler @findEmojiIcon(emoji).parent().hasClass("active") decrementCounter: (emoji) -> - counter = @findEmojiIcon(emoji).siblings(".counter") + counter = @findEmojiIcon(emoji).siblings(".js-counter") emojiIcon = counter.parent() if parseInt(counter.text()) > 1 counter.text(parseInt(counter.text()) - 1) @@ -98,14 +108,13 @@ class @AwardsHandler emojiCssClass = @resolveNameToCssClass(emoji) nodes = [] - nodes.push("<div class='award active' title='me'>") - nodes.push("<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>") - nodes.push("<div class='counter'>1</div>") - nodes.push("</div>") - - emoji_node = $(nodes.join("\n")).insertBefore(".awards-controls").find(".emoji-icon").data("emoji", emoji) + nodes.push "<button class='btn award-control js-emoji-btn has_tooltip active' title='me'>" + nodes.push "<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>" + nodes.push "<span class='award-control-text js-counter'>1</span>" + nodes.push "</button>" - $(".award").tooltip() + emoji_node = $(nodes.join("\n")).insertBefore(".js-award-holder").find(".emoji-icon").data("emoji", emoji) + $('.award-control').tooltip() resolveNameToCssClass: (emoji) -> emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']") @@ -128,7 +137,7 @@ class @AwardsHandler callback.call() findEmojiIcon: (emoji) -> - $(".award [data-emoji='#{emoji}']") + $(".awards > .js-emoji-btn [data-emoji='#{emoji}']") scrollToAwards: -> $('body, html').animate({ diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index e6609ac71085..6edabe201368 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -157,3 +157,7 @@ float: right; } } + +.content-block-small { + padding: 10px 0; +} diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index 87dd30f41114..e692730bddf5 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -1,125 +1,112 @@ .awards { - @include clearfix; line-height: 34px; .emoji-icon { width: 20px; height: 20px; - margin: 7px 0 0 5px; } +} - .award { - @include border-radius(5px); +.emoji-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + min-width: 160px; + font-size: 14px; + background-color: #fff; + border: 1px solid #F1F2F4; + border-radius: 2px; + box-shadow: 0 6px 12px rgba(0,0,0,.175); + + .emoji-menu-content { + padding: $gl-padding; + width: 300px; + height: 300px; + overflow-y: scroll; + + input.emoji-search{ + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC"); + background-repeat: no-repeat; + background-position: right 5px center; + background-size: 16px; + } + } +} - border: 1px solid; - padding: 0px 10px; - float: left; - margin-right: 5px; - border-color: $border-color; - cursor: pointer; +.emoji-menu-list { + list-style: none; + padding-left: 0; + margin-bottom: 0; +} - &:hover { - background-color: #dce0e5; - } +.emoji-menu-list-item { + padding: 3px; + margin-left: 1px; + margin-right: 1px; +} + +.emoji-menu-btn { + display: block; + cursor: pointer; + width: 30px; + height: 30px; + padding: 0; + background: none; + border: 0; + border-radius: 4px; + + &:hover { + background-color: #ededed; + } + + .emoji-icon { + display: inline-block; + position: relative; + top: 3px; + } +} - &.active { - border-color: $border-gray-light; - background-color: $gray-light; +.award-menu-holder { + display: inline-block; + position: relative; +} - &:hover { - background-color: #dce0e5; - } +.award-control { + margin-right: 5px; + line-height: 20px; + outline: 0; - .counter { - font-weight: bold; - } - } + &.active, + &:active { + background-color: #ededed; + box-shadow: none; + outline: 0; + } - .icon { - float: left; - margin-right: 10px; + &.is-loading { + .award-control-icon { + display: none; } - .counter { - float: left; + .award-control-icon-loading { + display: block; } } - .awards-controls { - position: relative; - margin-left: 10px; + .icon, + .award-control-icon { float: left; + margin-right: 5px; + font-size: 20px; + } - .add-award { - font-size: 24px; - color: $gl-gray; - position: relative; - top: 2px; - - &:hover, - &:link { - text-decoration: none; - } - } + .award-control-icon-loading { + display: none; + } - .emoji-menu{ - position: absolute; - top: 100%; - left: 0; - z-index: 1000; - display: none; - float: left; - min-width: 160px; - padding: 5px 0; - margin: 2px 0 0; - font-size: 14px; - text-align: left; - list-style: none; - background-color: #fff; - -webkit-background-clip: padding-box; - background-clip: padding-box; - border: 1px solid #ccc; - border: 1px solid rgba(0,0,0,.15); - border-radius: 4px; - -webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175); - box-shadow: 0 6px 12px rgba(0,0,0,.175); - - .emoji-menu-content { - padding: $gl-padding; - width: 300px; - height: 300px; - overflow-y: scroll; - - h5 { - clear: left; - } - - ul { - list-style-type: none; - margin-left: -20px; - margin-bottom: 20px; - overflow: auto; - } - - input.emoji-search{ - background: image-url("icon-search.png") 240px no-repeat; - } - - li { - cursor: pointer; - width: 30px; - height: 30px; - text-align: center; - float: left; - margin: 3px; - list-decorate: none; - @include border-radius(5px); - - &:hover { - background-color: #ccc; - } - } - } - } + .award-control-icon { + color: #DCDCDC; } } diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml index b66e513e4d2c..3443a8e2307f 100644 --- a/app/views/emojis/index.html.haml +++ b/app/views/emojis/index.html.haml @@ -2,8 +2,10 @@ .emoji-menu-content = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control" - AwardEmoji.emoji_by_category.each do |category, emojis| - %h5= AwardEmoji::CATEGORIES[category] - %ul + %h5.emoji-menu-title + = AwardEmoji::CATEGORIES[category] + %ul.clearfix.emoji-menu-list - emojis.each do |emoji| - %li - = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"]) \ No newline at end of file + %li.pull-left.text-center.emoji-menu-list-item + %button.emoji-menu-btn.text-center.js-emoji-btn{type: "button"} + = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"]) diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index f5bb5d998bbd..0242276cd841 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -71,7 +71,7 @@ .merge-requests = render 'merge_requests' - .content-block + .content-block.content-block-small = render 'votes/votes_block', votable: @issue .row diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index b262892ac659..ee5b9fd95a88 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -68,7 +68,7 @@ .tab-content #notes.notes.tab-pane.voting_notes - .content-block.oneline-block + .content-block.content-block-small.oneline-block = render 'votes/votes_block', votable: @merge_request .row diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml index 176fd29cb57a..20d2d5f317bd 100644 --- a/app/views/votes/_votes_block.html.haml +++ b/app/views/votes/_votes_block.html.haml @@ -1,14 +1,17 @@ .awards.votes-block - awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes| - .award{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user)} + %button.btn.award-control.js-emoji-btn.has_tooltip{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user), data: {placement: "top"}} = emoji_icon(emoji) - .counter + %span.award-control-text.js-counter = notes.count - if current_user - .awards-controls - %a.add-award{"href" => "#"} - = icon('smile-o') + %div.award-menu-holder.js-award-holder + %a.btn.award-control.js-add-award{"href" => "#"} + = icon('smile-o', {class: "award-control-icon"}) + = icon('spinner spin', {class: "award-control-icon award-control-icon-loading"}) + %span.award-control-text + Add - if current_user :javascript @@ -23,17 +26,3 @@ noteable_id, aliases ); - - $(".awards").on("click", ".emoji-menu-content li", function(e) { - var emoji = $(this).find(".emoji-icon").data("emoji"); - awards_handler.addAward(emoji); - }); - - $(".awards").on("click", ".award", function(e) { - var emoji = $(this).find(".icon").data("emoji"); - awards_handler.addAward(emoji); - }); - - $(".award").tooltip(); - - $(".emoji-menu-content").niceScroll({cursorwidth: "7px", autohidemode: false}); -- GitLab From f5fb915190608c60353ec7e2123ccbbe25313005 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 1 Mar 2016 13:00:26 +0000 Subject: [PATCH 158/223] Updated award emoji tests --- app/assets/javascripts/awards_handler.coffee | 2 +- features/steps/project/issues/award_emoji.rb | 26 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 351f871de94a..829340dea6e9 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -82,7 +82,7 @@ class @AwardsHandler award_block = @findEmojiIcon(emoji).parent() authors = award_block.attr("data-original-title").split(", ") authors.splice(authors.indexOf("me"),1) - award_block.closest(".award").attr("data-original-title", authors.join(", ")) + award_block.closest(".js-emoji-btn").attr("data-original-title", authors.join(", ")) @resetTooltip(award_block) addMeToAuthorList: (emoji) -> diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb index 937fbbd34ebe..135e1d016ae0 100644 --- a/features/steps/project/issues/award_emoji.rb +++ b/features/steps/project/issues/award_emoji.rb @@ -10,7 +10,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps step 'I click the thumbsup award Emoji' do page.within '.awards' do - thumbsup = page.find('.award .emoji-1F44D') + thumbsup = page.first('.award-control') thumbsup.click thumbsup.hover sleep 0.3 @@ -18,23 +18,23 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps end step 'I click to emoji-picker' do - page.within '.awards-controls' do - page.find('.add-award').click + page.within '.awards' do + page.find('.js-add-award').click end end step 'I click to emoji in the picker' do page.within '.emoji-menu-content' do - page.first('.emoji-icon').click + page.first('.js-emoji-btn').click end end step 'I can remove it by clicking to icon' do page.within '.awards' do expect do - page.find('.award.active').click + page.find('.js-emoji-btn.active').click sleep 0.3 - end.to change{ page.all(".award").size }.from(3).to(2) + end.to change{ page.all(".award-control.js-emoji-btn").size }.from(3).to(2) end end @@ -49,23 +49,23 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps sleep 0.2 page.within '.awards' do - expect(page).to have_selector '.award' - expect(page.find('.award.active .counter')).to have_content '1' - expect(page.find('.award.active')['data-original-title']).to eq('me') + expect(page).to have_selector '.js-emoji-btn' + expect(page.find('.js-emoji-btn.active .js-counter')).to have_content '1' + expect(page.find('.js-emoji-btn.active')['data-original-title']).to eq('me') end end step 'I have no awards added' do page.within '.awards' do - expect(page).to have_selector '.award' - expect(page.all('.award').size).to eq(2) + expect(page).to have_selector '.award-control.js-emoji-btn' + expect(page.all('.award-control.js-emoji-btn').size).to eq(2) # Check tooltip data - page.all('.award').each do |element| + page.all('.award-control.js-emoji-btn').each do |element| expect(element['title']).to eq("") end - page.all('.award .counter').each do |element| + page.all('.award-control .js-counter').each do |element| expect(element).to have_content '0' end end -- GitLab From 42b28411844851c4329f3fa76f87f512c0f04e73 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 2 Mar 2016 15:40:00 +0000 Subject: [PATCH 159/223] Moved SCSS values into variables --- app/assets/javascripts/awards_handler.coffee | 14 ++++++++++---- app/assets/stylesheets/framework/variables.scss | 11 +++++++++++ app/assets/stylesheets/pages/awards.scss | 14 +++++++------- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 829340dea6e9..e9bf053aa561 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -1,6 +1,6 @@ class @AwardsHandler constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) -> - $(".js-add-award").click (event) => + $(".js-add-award").on "click", (event) => event.stopPropagation() event.preventDefault() @@ -18,7 +18,9 @@ class @AwardsHandler handleClick: (e) -> e.preventDefault() - emoji = $(this).find(".icon").data "emoji" + emoji = $(this) + .find(".icon") + .data "emoji" awards_handler.addAward emoji showEmojiMenu: -> @@ -80,9 +82,13 @@ class @AwardsHandler removeMeFromAuthorList: (emoji) -> award_block = @findEmojiIcon(emoji).parent() - authors = award_block.attr("data-original-title").split(", ") + authors = award_block + .attr("data-original-title") + .split(", ") authors.splice(authors.indexOf("me"),1) - award_block.closest(".js-emoji-btn").attr("data-original-title", authors.join(", ")) + award_block + .closest(".js-emoji-btn") + .attr("data-original-title", authors.join(", ")) @resetTooltip(award_block) addMeToAuthorList: (emoji) -> diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 6561b3de7c1d..fe8ea399d9cd 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -150,3 +150,14 @@ $dropdown-toggle-border-color: #EAEAEA; $dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%); $dropdown-toggle-icon-color: #C4C4C4; $dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color; + +/* + * Award emoji + */ +$award-emoji-menu-bg: #FFF; +$award-emoji-menu-border: #F1F2F4; +$award-emoji-menu-radius: 3px; +$award-emoji-menu-btn-radius: 4px; +$award-emoji-menu-btn-hover-bg: #EDEDED; +$award-emoji-control-active-bg: $award-emoji-menu-btn-hover-bg; +$award-emoji-new-btn-color: #DCDCDC; diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index e692730bddf5..3134511cb316 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -15,9 +15,9 @@ display: none; min-width: 160px; font-size: 14px; - background-color: #fff; - border: 1px solid #F1F2F4; - border-radius: 2px; + background-color: $award-emoji-menu-bg; + border: 1px solid $award-emoji-menu-border; + border-radius: $award-emoji-menu-radius; box-shadow: 0 6px 12px rgba(0,0,0,.175); .emoji-menu-content { @@ -55,10 +55,10 @@ padding: 0; background: none; border: 0; - border-radius: 4px; + border-radius: $award-emoji-menu-btn-radius; &:hover { - background-color: #ededed; + background-color: $award-emoji-menu-btn-hover-bg; } .emoji-icon { @@ -80,7 +80,7 @@ &.active, &:active { - background-color: #ededed; + background-color: $award-emoji-control-active-bg; box-shadow: none; outline: 0; } @@ -107,6 +107,6 @@ } .award-control-icon { - color: #DCDCDC; + color: $award-emoji-new-btn-color; } } -- GitLab From bf7a6087ce7d47f09bb0ca6936e38a0e8d5bf43c Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 2 Mar 2016 15:43:01 +0000 Subject: [PATCH 160/223] Moved method calls to separate lines --- app/assets/javascripts/awards_handler.coffee | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index e9bf053aa561..66b26e4e6d7e 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -114,12 +114,17 @@ class @AwardsHandler emojiCssClass = @resolveNameToCssClass(emoji) nodes = [] - nodes.push "<button class='btn award-control js-emoji-btn has_tooltip active' title='me'>" - nodes.push "<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>" - nodes.push "<span class='award-control-text js-counter'>1</span>" - nodes.push "</button>" - - emoji_node = $(nodes.join("\n")).insertBefore(".js-award-holder").find(".emoji-icon").data("emoji", emoji) + nodes.push( + "<button class='btn award-control js-emoji-btn has_tooltip active' title='me'>", + "<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>", + "<span class='award-control-text js-counter'>1</span>", + "</button>" + ) + + emoji_node = $(nodes.join("\n")) + .insertBefore(".js-award-holder") + .find(".emoji-icon") + .data("emoji", emoji) $('.award-control').tooltip() resolveNameToCssClass: (emoji) -> -- GitLab From cefae4e871e32130e6a2fbe8a9e27583a9de2e1e Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 2 Mar 2016 18:55:05 +0000 Subject: [PATCH 161/223] Renamed variables & removed some for award emoji --- app/assets/stylesheets/framework/variables.scss | 6 +----- app/assets/stylesheets/pages/awards.scss | 10 +++++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index fe8ea399d9cd..b038c7958d2a 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -156,8 +156,4 @@ $dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color; */ $award-emoji-menu-bg: #FFF; $award-emoji-menu-border: #F1F2F4; -$award-emoji-menu-radius: 3px; -$award-emoji-menu-btn-radius: 4px; -$award-emoji-menu-btn-hover-bg: #EDEDED; -$award-emoji-control-active-bg: $award-emoji-menu-btn-hover-bg; -$award-emoji-new-btn-color: #DCDCDC; +$award-emoji-new-btn-icon-color: #DCDCDC; diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index 3134511cb316..b0cd187fc004 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -17,7 +17,7 @@ font-size: 14px; background-color: $award-emoji-menu-bg; border: 1px solid $award-emoji-menu-border; - border-radius: $award-emoji-menu-radius; + border-radius: $border-radius-base; box-shadow: 0 6px 12px rgba(0,0,0,.175); .emoji-menu-content { @@ -55,10 +55,10 @@ padding: 0; background: none; border: 0; - border-radius: $award-emoji-menu-btn-radius; + border-radius: $border-radius-base; &:hover { - background-color: $award-emoji-menu-btn-hover-bg; + background-color: $white-dark; } .emoji-icon { @@ -80,7 +80,7 @@ &.active, &:active { - background-color: $award-emoji-control-active-bg; + background-color: $white-dark; box-shadow: none; outline: 0; } @@ -107,6 +107,6 @@ } .award-control-icon { - color: $award-emoji-new-btn-color; + color: $award-emoji-new-btn-icon-color; } } -- GitLab From 7dd82c95cc3b075e2c6359d01d5884e207dcce82 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 3 Mar 2016 08:57:40 +0000 Subject: [PATCH 162/223] Award emoji button padding --- app/assets/stylesheets/pages/awards.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index b0cd187fc004..6a19562e5920 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -75,6 +75,8 @@ .award-control { margin-right: 5px; + padding-left: 5px; + padding-right: 5px; line-height: 20px; outline: 0; -- GitLab From b726d1faa74579ac677c7e3936271f6bb376923b Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 4 Mar 2016 09:13:06 +0000 Subject: [PATCH 163/223] CoffeeScript style improv. --- app/assets/javascripts/awards_handler.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 66b26e4e6d7e..13b4a7e05e9b 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -11,8 +11,9 @@ class @AwardsHandler if $(".emoji-menu").is(":visible") $(".emoji-menu").hide() - $(".awards").off "click" - $(".awards").on "click", ".js-emoji-btn", @handleClick + $(".awards") + .off "click" + .on "click", ".js-emoji-btn", @handleClick @renderFrequentlyUsedBlock() -- GitLab From 3826a0ac914f2ab9c0c707b5add66c4ae87f02be Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 11 Mar 2016 09:11:36 +0000 Subject: [PATCH 164/223] Fixed bug with emoji search styling --- app/assets/javascripts/awards_handler.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 13b4a7e05e9b..da102db39c2d 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -185,13 +185,13 @@ class @AwardsHandler term = $(ev.target).val() # Clean previous search results - $("ul.emoji-search,h5.emoji-search").remove() + $("ul.emoji-menu-search, h5.emoji-search").remove() if term # Generate a search result block h5 = $("<h5>").text("Search results").addClass("emoji-search") found_emojis = @searchEmojis(term).show() - ul = $("<ul>").addClass("emoji-search").append(found_emojis) + ul = $("<ul>").addClass("emoji-menu-list emoji-menu-search").append(found_emojis) $(".emoji-menu-content ul, .emoji-menu-content h5").hide() $(".emoji-menu-content").append(h5).append(ul) else -- GitLab From 3574e590a2e835fc4cfa1e8be812e31820606b89 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 11 Mar 2016 09:47:18 +0000 Subject: [PATCH 165/223] Added 'surprise' animation --- app/assets/javascripts/awards_handler.coffee | 20 +++++++++++------ app/assets/stylesheets/pages/awards.scss | 23 ++++++++++++++++++-- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index da102db39c2d..03a448741613 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -9,7 +9,7 @@ class @AwardsHandler $("html").on 'click', (event) -> if !$(event.target).closest(".emoji-menu").length if $(".emoji-menu").is(":visible") - $(".emoji-menu").hide() + $(".emoji-menu").removeClass "is-visible" $(".awards") .off "click" @@ -26,23 +26,29 @@ class @AwardsHandler showEmojiMenu: -> if $(".emoji-menu").length - $(".emoji-menu").show() - $("#emoji_search").focus() + if $(".emoji-menu").is ".is-visible" + $(".emoji-menu").removeClass "is-visible" + $("#emoji_search").blur() + else + $(".emoji-menu").addClass "is-visible" + $("#emoji_search").focus() else $('.js-add-award').addClass "is-loading" $.get "/emojis", (response) => $('.js-add-award').removeClass "is-loading" $(".js-award-holder").append response - $(".emoji-menu").show() - $("#emoji_search").focus() - @setupSearch() + setTimeout => + $(".emoji-menu").addClass "is-visible" + $("#emoji_search").focus() + @setupSearch() + , 200 addAward: (emoji) -> emoji = @normilizeEmojiName(emoji) @postEmoji emoji, => @addAwardToEmojiBar(emoji) - $(".emoji-menu").hide() + $(".emoji-menu").removeClass "is-visible" addAwardToEmojiBar: (emoji) -> @addEmojiToFrequentlyUsedList(emoji) diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index 6a19562e5920..28994e60baa5 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -11,14 +11,25 @@ position: absolute; top: 100%; left: 0; + margin-top: 3px; z-index: 1000; - display: none; min-width: 160px; font-size: 14px; background-color: $award-emoji-menu-bg; border: 1px solid $award-emoji-menu-border; border-radius: $border-radius-base; box-shadow: 0 6px 12px rgba(0,0,0,.175); + pointer-events: none; + opacity: 0; + transform: scale(.2); + transform-origin: 0 -45px; + transition: all .3s cubic-bezier(.87,-.41,.19,1.44); + + &.is-visible { + pointer-events: all; + opacity: 1; + transform: scale(1); + } .emoji-menu-content { padding: $gl-padding; @@ -56,9 +67,17 @@ background: none; border: 0; border-radius: $border-radius-base; + transition: transform .15s cubic-bezier(.3, 0, .2, 2); &:hover { - background-color: $white-dark; + background-color: transparent; + outline: 0; + transform: scale(1.3); + } + + &:focus, + &:active { + outline: 0; } .emoji-icon { -- GitLab From 7099b6397ee0b8e2cc17b63060aa0e783c6e58dd Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 29 Feb 2016 13:39:29 +0000 Subject: [PATCH 166/223] Account settings Closes #13854 --- app/assets/javascripts/profile.js.coffee | 7 +- app/assets/stylesheets/framework/common.scss | 4 +- .../stylesheets/framework/variables.scss | 4 +- app/assets/stylesheets/pages/profile.scss | 48 +++- .../profiles/accounts_controller.rb | 24 ++ app/views/profiles/accounts/show.html.haml | 229 ++++++++++-------- 6 files changed, 200 insertions(+), 116 deletions(-) diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee index 9110b732adc8..59d44c30bee0 100644 --- a/app/assets/javascripts/profile.js.coffee +++ b/app/assets/javascripts/profile.js.coffee @@ -4,12 +4,13 @@ class @Profile $('.js-preferences-form').on 'change.preference', 'input[type=radio]', -> $(this).parents('form').submit() - $('.update-username form').on 'ajax:before', -> - $('.loading-gif').show() + $('.update-username').on 'ajax:before', -> + $('.loading-username').show() $(this).find('.update-success').hide() $(this).find('.update-failed').hide() - $('.update-username form').on 'ajax:complete', -> + $('.update-username').on 'ajax:complete', -> + $('.loading-username').hide() $(this).find('.btn-save').enable() $(this).find('.loading-gif').hide() diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index c98e43ad09f5..ff551f151f1e 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -12,11 +12,13 @@ .prepend-top-default { margin-top: $gl-padding !important; } .prepend-top-20 { margin-top:20px } .prepend-left-10 { margin-left:10px } -.prepend-left-default { margin-left:$gl-padding } +.prepend-left-default { margin-left: $gl-padding; } .prepend-left-20 { margin-left:20px } .append-right-5 { margin-right: 5px } .append-right-10 { margin-right:10px } +.append-right-default { margin-right: $gl-padding; } .append-right-20 { margin-right:20px } +.append-bottom-0 { margin-bottom:0 } .append-bottom-10 { margin-bottom:10px } .append-bottom-15 { margin-bottom:15px } .append-bottom-20 { margin-bottom:20px } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index b038c7958d2a..625573706df8 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -70,7 +70,7 @@ $orange-light: rgba(252, 109, 38, 0.80); $orange-normal: #E75E40; $orange-dark: #CE5237; -$red-light: #F43263; +$red-light: #F06559; $red-normal: #E52C5A; $red-dark: #D22852; @@ -94,7 +94,7 @@ $border-orange-light: #fc6d26; $border-orange-normal: #CE5237; $border-orange-dark: #C14E35; -$border-red-light: #E52C5A; +$border-red-light: #F24F41; $border-red-normal: #D22852; $border-red-dark: #CA264F; diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 4826b994e372..f8aeab6857f3 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -1,7 +1,6 @@ -.account-page { - fieldset { - margin-bottom: 15px; - padding-bottom: 15px; +.profile-avatar-form-option { + hr { + margin: 10px 0; } } @@ -175,3 +174,44 @@ color: $profile-settings-link-color; } } + +.change-username-title { + color: #FC6D26; +} + +.remove-account-title { + color: #F00; +} + +.provider-btn-group { + display: inline-block; + margin-right: 10px; + border: 1px solid #E5E5E5; + border-radius: 3px; + + &:last-child { + margin-right: 0; + } +} + +.provider-btn-image { + display: inline-block; + padding: 5px 10px; + border-right: 1px solid #E5E5E5; + + > img { + width: 20px; + } +} + +.provider-btn { + display: inline-block; + padding: 5px 10px; + margin-left: -3px; + line-height: 22px; + background-color: $gray-light; + + &.not-active { + color: #4688F1; + } +} diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb index 175afbf84259..669fe05e5c7f 100644 --- a/app/controllers/profiles/accounts_controller.rb +++ b/app/controllers/profiles/accounts_controller.rb @@ -1,6 +1,18 @@ class Profiles::AccountsController < Profiles::ApplicationController def show + unless current_user.otp_secret + current_user.otp_secret = User.generate_otp_secret(32) + end + + unless current_user.otp_grace_period_started_at && two_factor_grace_period + current_user.otp_grace_period_started_at = Time.current + end + + current_user.save! if current_user.changed? + @user = current_user + + @qr_code = build_qr_code end def unlink @@ -8,4 +20,16 @@ def unlink current_user.identities.find_by(provider: provider).destroy redirect_to profile_account_path end + + private + + def build_qr_code + issuer = "#{issuer_host} | #{current_user.email}" + uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer) + RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3) + end + + def issuer_host + Gitlab.config.gitlab.host + end end diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 9fa96084f942..3e8f66063582 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -5,114 +5,131 @@ .alert.alert-info Some options are unavailable for LDAP accounts -.account-page.prepend-top-default - .panel.panel-default.update-token - .panel-heading - Reset Private token - .panel-body - = form_for @user, url: reset_private_token_profile_path, method: :put do |f| - .data - %p - Your private token is used to access application resources without authentication. - %br - It can be used for atom feeds or the API. - %span.cred - Keep it secret! - - %p.cgray - - if current_user.private_token - = text_field_tag "token", current_user.private_token, class: "form-control" - - else - %span You don`t have one yet. Click generate to fix it. - - .form-actions - - if current_user.private_token - = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default" +.row.prepend-top-default + .col-lg-3.profile-settings-sidebar + %h4.prepend-top-0 + Private Token + %p + Your private token is used to access application resources without authentication. + .col-lg-9 + = form_for @user, url: reset_private_token_profile_path, method: :put do |f| + %p.cgray + - if current_user.private_token + = label_tag "token", "Private token", class: "label-light" + = text_field_tag "token", current_user.private_token, class: "form-control" + - else + %span You don`t have one yet. Click generate to fix it. + %p.help-block + It can be used for atom feeds or the API. Keep it secret! + .prepend-top-default + - if current_user.private_token + = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default" + - else + = f.submit 'Generate', class: "btn btn-default" +%hr +.row.prepend-top-default + .col-lg-3.profile-settings-sidebar + %h4.prepend-top-0 + Two-factor Authentication + %p + Increase your account's security by enabling two-factor authentication (2FA). + .col-lg-9 + %p + Status: #{current_user.two_factor_enabled? ? 'enabled' : 'disabled'} + - if !current_user.two_factor_enabled? + %p + Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code. + More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. + .row.append-bottom-10 + .col-md-3 + = raw @qr_code + .col-md-9 + .account-well + %p.prepend-top-0.append-bottom-0 + Can't scan the code? + %p.prepend-top-0.append-bottom-0 + To add the entry manually, provide the following details to the application on your phone. + %p.prepend-top-0.append-bottom-0 + Account: + = current_user.email + %p.prepend-top-0.append-bottom-0 + Key: + = current_user.otp_secret.scan(/.{4}/).join(' ') + %p.two-factor-new-manual-content + Time based: Yes + = form_for @user, url: "", method: :put do |f| + .form-group + = label_tag :pin_code, nil, class: "label-light" + = text_field_tag :pin_code, nil, class: "form-control", required: true + .prepend-top-default + = submit_tag 'Enable two-factor authentication', class: 'btn btn-success' +%hr +- if button_based_providers.any? + .row.prepend-top-default + .col-lg-3.profile-settings-sidebar + %h4.prepend-top-0 + Social sign-in + %p + Activate signin with one of the following services + .col-lg-9 + %label.label-light + Connected Accounts + %p Click on icon to activate signin with one of the following services + - button_based_providers.each do |provider| + .provider-btn-group + .provider-btn-image + = provider_image_tag(provider) + - if auth_active?(provider) + = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do + Disconnect - else - = f.submit 'Generate', class: "btn btn-default" + = link_to user_omniauth_authorize_path(provider), method: :post, class: "provider-btn #{'not-active' if !auth_active?(provider)}", "data-no-turbolink" => "true" do + Connect + %hr +- if current_user.can_change_username? + .row.prepend-top-default + .col-lg-3.profile-settings-sidebar + %h4.prepend-top-0.change-username-title + Change username + %p + Changing your username will change path to all personal projects! + .col-lg-9 + = form_for @user, url: update_username_profile_path, method: :put, remote: true, html: {class: "update-username"} do |f| + .form-group + = f.label :username, "Path", class: "label-light" + .input-group + .input-group-addon + = "#{root_url}u/" + = f.text_field :username, required: true, class: 'form-control' + .help-block + Current path: + = "#{root_url}u/#{current_user.username}" + .prepend-top-default + = f.button class: "btn btn-warning", type: "submit" do + = icon "spinner spin", class: "hidden loading-username" + Update username + %hr - .panel.panel-default - .panel-heading - Two-factor Authentication - .panel-body - - if current_user.two_factor_enabled? - .pull-right - = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-close btn-sm', - data: { confirm: 'Are you sure?' } - %p.text-success - %strong - Two-factor Authentication is enabled +- if signup_enabled? + .row.prepend-top-default + .col-lg-3.profile-settings-sidebar + %h4.prepend-top-0.remove-account-title + Remove account + .col-lg-9 + - if @user.can_be_removed? %p - If you lose your recovery codes you can - %strong - = succeed ',' do - = link_to 'generate new ones', codes_profile_two_factor_auth_path, method: :post, data: { confirm: 'Are you sure?' } - invalidating all previous codes. - + Deleting an account has the following effects: + %ul + %li All user content like authored issues, snippets, comments will be removed + - rp = current_user.personal_projects.count + - unless rp.zero? + %li #{pluralize rp, 'personal project'} will be removed and cannot be restored + = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove" - else - %p - Increase your account's security by enabling two-factor authentication (2FA). - %p - Each time you log in you’ll be required to provide your username and - password as usual, plus a randomly-generated code from your phone. - - .form-actions - = link_to 'Enable Two-factor Authentication', new_profile_two_factor_auth_path, class: 'btn btn-success' - - - if button_based_providers.any? - .panel.panel-default - .panel-heading - Connected Accounts - .panel-body - .oauth-buttons.append-bottom-10 - %p Click on icon to activate signin with one of the following services - - button_based_providers.each do |provider| - .btn-group - = link_to provider_image_tag(provider), user_omniauth_authorize_path(provider), method: :post, class: "btn btn-lg #{'active' if auth_active?(provider)}", "data-no-turbolink" => "true" - - - if auth_active?(provider) - = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'btn btn-lg' do - = icon('close') - - - if current_user.can_change_username? - .panel.panel-warning.update-username - .panel-heading - Change Username - .panel-body - = form_for @user, url: update_username_profile_path, method: :put, remote: true do |f| + - if @user.solo_owned_groups.present? %p - Changing your username will change path to all personal projects! - %div - .input-group - .input-group-addon - = "#{root_url}u/" - = f.text_field :username, required: true, class: 'form-control' - - .loading-gif.hide - %p - = icon('spinner spin') - Saving new username - .form-actions - = f.submit 'Save username', class: "btn btn-warning" - - - if signup_enabled? - .panel.panel-danger.remove-account - .panel-heading - Remove account - .panel-body - - if @user.can_be_removed? - %p Deleting an account has the following effects: - %ul - %li All user content like authored issues, snippets, comments will be removed - - rp = current_user.personal_projects.count - - unless rp.zero? - %li #{pluralize rp, 'personal project'} will be removed and cannot be restored - .form-actions - = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove" - - else - - if @user.solo_owned_groups.present? - %p - Your account is currently an owner in these groups: - %strong #{@user.solo_owned_groups.map(&:name).join(', ')} - %p - You must transfer ownership or delete these groups before you can delete your account. + Your account is currently an owner in these groups: + %strong #{@user.solo_owned_groups.map(&:name).join(', ')} + %p + You must transfer ownership or delete these groups before you can delete your account. +.append-bottom-default -- GitLab From 06cad950e5cd9b19cc3bdc30de7d38db6a8ee83d Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 1 Mar 2016 15:58:00 +0000 Subject: [PATCH 167/223] Fixed failing tests --- app/views/profiles/accounts/show.html.haml | 2 +- features/steps/profile/profile.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 3e8f66063582..9fe064e5883c 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -12,7 +12,7 @@ %p Your private token is used to access application resources without authentication. .col-lg-9 - = form_for @user, url: reset_private_token_profile_path, method: :put do |f| + = form_for @user, url: reset_private_token_profile_path, method: :put, html: {class: "private-token"} do |f| %p.cgray - if current_user.private_token = label_tag "token", "Private token", class: "label-light" diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index 0c60328583a7..d9436e9e21a5 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -99,9 +99,9 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I reset my token' do - page.within '.update-token' do + page.within '.private-token' do @old_token = @user.private_token - click_button "Reset" + click_button "Reset private token" end end -- GitLab From 4932ad5e3c2b6a4ccda523a1b55be94e5262a870 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 1 Mar 2016 16:22:04 +0000 Subject: [PATCH 168/223] Updated CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 61ca5dbcba83..4bd6c1dbcfaa 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -44,6 +44,7 @@ v 8.5.3 - Sort starred projects on dashboard based on last activity by default - Show commit message in JIRA mention comment - Makes issue page and merge request page usable on mobile browsers. + - Improved UI for profile settings v 8.5.2 - Fix sidebar overlapping content when screen width was below 1200px -- GitLab From 502e7cf712a5d74b30996050cb38a9bc81e0f605 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 2 Mar 2016 10:34:25 +0000 Subject: [PATCH 169/223] Fixed heading weight issue Moved colours to variables --- app/assets/stylesheets/framework/variables.scss | 12 ++++++++++++ app/assets/stylesheets/pages/profile.scss | 10 +++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 625573706df8..f75ec3790035 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -41,6 +41,12 @@ $btn-transparent-color: #8F8F8F; $ssh-key-icon-color: #8F8F8F; $ssh-key-icon-size: 18px; +$change-username-title-color: #FC6D26; +$remove-account-title-color: #F00; + +$provider-btn-group-border: #E5E5E5; +$provider-btn-not-active-color: #4688F1; + /* * Color schema */ @@ -98,8 +104,14 @@ $border-red-light: #F24F41; $border-red-normal: #D22852; $border-red-dark: #CA264F; +<<<<<<< 454832ace49f1b1742b380441817663051ba8ac8 $help-well-bg: #FAFAFA; $help-well-border: #E5E5E5; +======= +$account-well-bg: #FAFAFA; +$account-well-border: #E5E5E5; +$account-well-radius: 3px; +>>>>>>> Fixed heading weight issue /* header */ $light-grey-header: #faf9f9; diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index f8aeab6857f3..40037c548f3d 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -176,17 +176,17 @@ } .change-username-title { - color: #FC6D26; + color: $change-username-title-color; } .remove-account-title { - color: #F00; + color: $remove-account-title-color; } .provider-btn-group { display: inline-block; margin-right: 10px; - border: 1px solid #E5E5E5; + border: 1px solid $provider-btn-group-border; border-radius: 3px; &:last-child { @@ -197,7 +197,7 @@ .provider-btn-image { display: inline-block; padding: 5px 10px; - border-right: 1px solid #E5E5E5; + border-right: 1px solid $provider-btn-group-border; > img { width: 20px; @@ -212,6 +212,6 @@ background-color: $gray-light; &.not-active { - color: #4688F1; + color: $provider-btn-not-active-color; } } -- GitLab From 039cc237bc8c27f27c5f913a34a360b4cda2ec92 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 2 Mar 2016 17:34:31 +0000 Subject: [PATCH 170/223] Fixed issue with 2fa not enabling Added in disable button for 2fa --- app/controllers/application_controller.rb | 2 +- .../profiles/accounts_controller.rb | 11 ++++++++ .../profiles/two_factor_auths_controller.rb | 26 +++---------------- app/views/profiles/accounts/show.html.haml | 8 +++++- 4 files changed, 22 insertions(+), 25 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1f55b18e0b16..fef79cefc928 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -240,7 +240,7 @@ def check_password_expiration def check_2fa_requirement if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled && !skip_two_factor? - redirect_to new_profile_two_factor_auth_path + redirect_to profile_account_path end end diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb index 669fe05e5c7f..bd827f2ab1b1 100644 --- a/app/controllers/profiles/accounts_controller.rb +++ b/app/controllers/profiles/accounts_controller.rb @@ -1,4 +1,6 @@ class Profiles::AccountsController < Profiles::ApplicationController + skip_before_action :check_2fa_requirement + def show unless current_user.otp_secret current_user.otp_secret = User.generate_otp_secret(32) @@ -10,6 +12,15 @@ def show current_user.save! if current_user.changed? + if two_factor_authentication_required? + if two_factor_grace_period_expired? + flash.now[:alert] = 'You must enable Two-factor Authentication for your account.' + else + grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours + flash.now[:alert] = "You must enable Two-factor Authentication for your account before #{l(grace_period_deadline)}." + end + end + @user = current_user @qr_code = build_qr_code diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 8f83fdd02bc7..65ecee0746e4 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -2,26 +2,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController skip_before_action :check_2fa_requirement def new - unless current_user.otp_secret - current_user.otp_secret = User.generate_otp_secret(32) - end - - unless current_user.otp_grace_period_started_at && two_factor_grace_period - current_user.otp_grace_period_started_at = Time.current - end - - current_user.save! if current_user.changed? - - if two_factor_authentication_required? - if two_factor_grace_period_expired? - flash.now[:alert] = 'You must enable Two-factor Authentication for your account.' - else - grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours - flash.now[:alert] = "You must enable Two-factor Authentication for your account before #{l(grace_period_deadline)}." - end - end - - @qr_code = build_qr_code + redirect_to profile_account_path end def create @@ -32,10 +13,9 @@ def create render 'create' else - @error = 'Invalid pin code' - @qr_code = build_qr_code + error = 'Invalid pin code' - render 'new' + redirect_to profile_account_path, flash: { error: error } end end diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 9fe064e5883c..a54176550018 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -57,12 +57,18 @@ = current_user.otp_secret.scan(/.{4}/).join(' ') %p.two-factor-new-manual-content Time based: Yes - = form_for @user, url: "", method: :put do |f| + = form_for @user, url: profile_two_factor_auth_path, method: :post do |f| + - if flash[:error] + .alert.alert-danger + = flash[:error] .form-group = label_tag :pin_code, nil, class: "label-light" = text_field_tag :pin_code, nil, class: "form-control", required: true .prepend-top-default = submit_tag 'Enable two-factor authentication', class: 'btn btn-success' + - else + = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-danger', + data: { confirm: 'Are you sure?' } %hr - if button_based_providers.any? .row.prepend-top-default -- GitLab From 888958ce32a11043aa085d887a55691f84db8635 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 3 Mar 2016 09:28:33 +0000 Subject: [PATCH 171/223] Moved 2fa into separate view --- app/controllers/application_controller.rb | 2 +- .../profiles/accounts_controller.rb | 35 --------- .../profiles/two_factor_auths_controller.rb | 26 ++++++- app/views/profiles/accounts/show.html.haml | 28 +------ .../profiles/two_factor_auths/new.html.haml | 77 +++++++++---------- 5 files changed, 64 insertions(+), 104 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index fef79cefc928..1f55b18e0b16 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -240,7 +240,7 @@ def check_password_expiration def check_2fa_requirement if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled && !skip_two_factor? - redirect_to profile_account_path + redirect_to new_profile_two_factor_auth_path end end diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb index bd827f2ab1b1..175afbf84259 100644 --- a/app/controllers/profiles/accounts_controller.rb +++ b/app/controllers/profiles/accounts_controller.rb @@ -1,29 +1,6 @@ class Profiles::AccountsController < Profiles::ApplicationController - skip_before_action :check_2fa_requirement - def show - unless current_user.otp_secret - current_user.otp_secret = User.generate_otp_secret(32) - end - - unless current_user.otp_grace_period_started_at && two_factor_grace_period - current_user.otp_grace_period_started_at = Time.current - end - - current_user.save! if current_user.changed? - - if two_factor_authentication_required? - if two_factor_grace_period_expired? - flash.now[:alert] = 'You must enable Two-factor Authentication for your account.' - else - grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours - flash.now[:alert] = "You must enable Two-factor Authentication for your account before #{l(grace_period_deadline)}." - end - end - @user = current_user - - @qr_code = build_qr_code end def unlink @@ -31,16 +8,4 @@ def unlink current_user.identities.find_by(provider: provider).destroy redirect_to profile_account_path end - - private - - def build_qr_code - issuer = "#{issuer_host} | #{current_user.email}" - uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer) - RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3) - end - - def issuer_host - Gitlab.config.gitlab.host - end end diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 65ecee0746e4..8f83fdd02bc7 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -2,7 +2,26 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController skip_before_action :check_2fa_requirement def new - redirect_to profile_account_path + unless current_user.otp_secret + current_user.otp_secret = User.generate_otp_secret(32) + end + + unless current_user.otp_grace_period_started_at && two_factor_grace_period + current_user.otp_grace_period_started_at = Time.current + end + + current_user.save! if current_user.changed? + + if two_factor_authentication_required? + if two_factor_grace_period_expired? + flash.now[:alert] = 'You must enable Two-factor Authentication for your account.' + else + grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours + flash.now[:alert] = "You must enable Two-factor Authentication for your account before #{l(grace_period_deadline)}." + end + end + + @qr_code = build_qr_code end def create @@ -13,9 +32,10 @@ def create render 'create' else - error = 'Invalid pin code' + @error = 'Invalid pin code' + @qr_code = build_qr_code - redirect_to profile_account_path, flash: { error: error } + render 'new' end end diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index a54176550018..6efd119f2609 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -40,32 +40,8 @@ %p Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code. More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. - .row.append-bottom-10 - .col-md-3 - = raw @qr_code - .col-md-9 - .account-well - %p.prepend-top-0.append-bottom-0 - Can't scan the code? - %p.prepend-top-0.append-bottom-0 - To add the entry manually, provide the following details to the application on your phone. - %p.prepend-top-0.append-bottom-0 - Account: - = current_user.email - %p.prepend-top-0.append-bottom-0 - Key: - = current_user.otp_secret.scan(/.{4}/).join(' ') - %p.two-factor-new-manual-content - Time based: Yes - = form_for @user, url: profile_two_factor_auth_path, method: :post do |f| - - if flash[:error] - .alert.alert-danger - = flash[:error] - .form-group - = label_tag :pin_code, nil, class: "label-light" - = text_field_tag :pin_code, nil, class: "form-control", required: true - .prepend-top-default - = submit_tag 'Enable two-factor authentication', class: 'btn btn-success' + .append-bottom-10 + = link_to 'Enable two-factor authentication', new_profile_two_factor_auth_path, class: 'btn btn-success' - else = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-danger', data: { confirm: 'Are you sure?' } diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml index b2830aa08343..c82c59621912 100644 --- a/app/views/profiles/two_factor_auths/new.html.haml +++ b/app/views/profiles/two_factor_auths/new.html.haml @@ -1,41 +1,40 @@ - page_title 'Two-factor Authentication', 'Account' -%h2.page-title Two-factor Authentication (2FA) -%p - Download the Google Authenticator application from App Store for iOS or Google - Play for Android and scan this code. - - More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. - -%hr - -= form_tag profile_two_factor_auth_path, method: :post, class: 'form-horizontal two-factor-new' do |f| - - if @error - .alert.alert-danger - = @error - .form-group - .col-lg-2.col-lg-offset-2 - = raw @qr_code - .col-lg-7.col-lg-offset-1.manual-instructions - %h3 Can't scan the code? - - %p - To add the entry manually, provide the following details to the - application on your phone. - - %dl - %dt Account - %dd= current_user.email - %dl - %dt Key - %dd= current_user.otp_secret.scan(/.{4}/).join(' ') - %dl - %dt Time based - %dd Yes - .form-group - = label_tag :pin_code, nil, class: "control-label" - .col-lg-10 - = text_field_tag :pin_code, nil, class: "form-control", required: true, autofocus: true - .form-actions - = submit_tag 'Submit', class: 'btn btn-success' - = link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable? +.row.prepend-top-default + .col-lg-3 + %h4.prepend-top-0 + Two-factor Authentication (2FA) + %p + Increase your account's security by enabling two-factor authentication (2FA). + .col-lg-9 + %p + Status: #{current_user.two_factor_enabled? ? 'enabled' : 'disabled'} + %p + Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code. + More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. + .row.append-bottom-10 + .col-md-3 + = raw @qr_code + .col-md-9 + .account-well + %p.prepend-top-0.append-bottom-0 + Can't scan the code? + %p.prepend-top-0.append-bottom-0 + To add the entry manually, provide the following details to the application on your phone. + %p.prepend-top-0.append-bottom-0 + Account: + = current_user.email + %p.prepend-top-0.append-bottom-0 + Key: + = current_user.otp_secret.scan(/.{4}/).join(' ') + %p.two-factor-new-manual-content + Time based: Yes + = form_tag profile_two_factor_auth_path, method: :post do |f| + - if @error + .alert.alert-danger + = @error + .form-group + = label_tag :pin_code, nil, class: "label-light" + = text_field_tag :pin_code, nil, class: "form-control", required: true + .prepend-top-default + = submit_tag 'Enable two-factor authentication', class: 'btn btn-success' -- GitLab From a68339902118e0624c1e51387308a8e81b8b4e8b Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 3 Mar 2016 16:59:33 +0000 Subject: [PATCH 172/223] Added back 2fa configure later button --- app/views/profiles/two_factor_auths/new.html.haml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml index c82c59621912..5d342ef58e5b 100644 --- a/app/views/profiles/two_factor_auths/new.html.haml +++ b/app/views/profiles/two_factor_auths/new.html.haml @@ -38,3 +38,4 @@ = text_field_tag :pin_code, nil, class: "form-control", required: true .prepend-top-default = submit_tag 'Enable two-factor authentication', class: 'btn btn-success' + = link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable? -- GitLab From 69e388c61ce9448480e1e0cfc21dcb98451768d4 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 4 Mar 2016 09:03:50 +0000 Subject: [PATCH 173/223] Used standard variable colours --- app/assets/stylesheets/framework/variables.scss | 10 ---------- app/assets/stylesheets/pages/profile.scss | 8 ++++---- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index f75ec3790035..0261c384a587 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -34,16 +34,12 @@ $error-exclamation-point: #E62958; $border-radius-default: 3px; $list-title-color: #333333; $list-text-color: #555555; -$profile-settings-link-color: $md-link-color; $btn-transparent-color: #8F8F8F; $ssh-key-icon-color: #8F8F8F; $ssh-key-icon-size: 18px; -$change-username-title-color: #FC6D26; -$remove-account-title-color: #F00; - $provider-btn-group-border: #E5E5E5; $provider-btn-not-active-color: #4688F1; @@ -104,14 +100,8 @@ $border-red-light: #F24F41; $border-red-normal: #D22852; $border-red-dark: #CA264F; -<<<<<<< 454832ace49f1b1742b380441817663051ba8ac8 $help-well-bg: #FAFAFA; $help-well-border: #E5E5E5; -======= -$account-well-bg: #FAFAFA; -$account-well-border: #E5E5E5; -$account-well-radius: 3px; ->>>>>>> Fixed heading weight issue /* header */ $light-grey-header: #faf9f9; diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 40037c548f3d..248c56e459d7 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -19,7 +19,7 @@ .account-btn-link, .profile-settings-sidebar a { - color: $profile-settings-link-color; + color: $md-link-color; } .oauth-buttons { @@ -171,16 +171,16 @@ .profile-settings-content { a { - color: $profile-settings-link-color; + color: $md-link-color; } } .change-username-title { - color: $change-username-title-color; + color: $gl-warning; } .remove-account-title { - color: $remove-account-title-color; + color: $gl-danger; } .provider-btn-group { -- GitLab From 432eb5722f1bf507a4a2d83253932fe03b82f519 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 11 Mar 2016 10:03:35 +0000 Subject: [PATCH 174/223] Correctly uses a variable with the text --- app/assets/javascripts/notes.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index b0e7af817f06..75d7f52bbb6e 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -624,7 +624,7 @@ class @Notes reopenbtn.text(reopentext) if closebtn.text() isnt closetext - closebtn.text(closebtn.data('original-text')) + closebtn.text(closetext) if reopenbtn.is(':not(.btn-comment-and-reopen)') reopenbtn.removeClass('btn-comment-and-reopen') -- GitLab From 49287dc715977ea31b5f01d78b074743abc9adc7 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 11 Mar 2016 15:29:29 +0000 Subject: [PATCH 175/223] Improved search results filter dropdown --- app/views/search/_filter.html.haml | 46 +++++++++++++++++++----------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml index ec478a5963d1..4ef544136a84 100644 --- a/app/views/search/_filter.html.haml +++ b/app/views/search/_filter.html.haml @@ -6,14 +6,21 @@ - else Any %b.caret - %ul.dropdown-menu - %li - = link_to search_filter_path(group_id: nil) do - Any - - current_user.authorized_groups.sort_by(&:name).each do |group| - %li - = link_to search_filter_path(group_id: group.id, project_id: nil) do - = group.name + .dropdown-menu.dropdown-select.dropdown-menu-selectable + .dropdown-title + %span Filter results by group + %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + = icon('times') + .dropdown-content + %ul + %li + = link_to search_filter_path(group_id: nil), class: ("is-active" if !params[:group_id].present?) do + Any + %li.divider + - current_user.authorized_groups.sort_by(&:name).each do |group| + %li + = link_to search_filter_path(group_id: group.id, project_id: nil), class: ("is-active" if params[:group_id] == group.id.to_s) do + = group.name .dropdown.inline.prepend-left-10.project-filter %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} @@ -23,11 +30,18 @@ - else Any %b.caret - %ul.dropdown-menu - %li - = link_to search_filter_path(project_id: nil) do - Any - - current_user.authorized_projects.sort_by(&:name_with_namespace).each do |project| - %li - = link_to search_filter_path(project_id: project.id, group_id: nil) do - = project.name_with_namespace + .dropdown-menu.dropdown-select.dropdown-menu-selectable + .dropdown-title + %span Filter results by project + %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} + = icon('times') + .dropdown-content + %ul + %li + = link_to search_filter_path(project_id: nil), class: ("is-active" if !params[:project_id].present?) do + Any + %li.divider + - current_user.authorized_projects.sort_by(&:name_with_namespace).each do |project| + %li + = link_to search_filter_path(project_id: project.id, group_id: nil), class: ("is-active" if params[:project_id] == project.id.to_s) do + = project.name_with_namespace -- GitLab From 8ddf430ffe6f243c856a01e82f6e96e7db807833 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 11 Mar 2016 16:30:58 +0000 Subject: [PATCH 176/223] Increased dropdown max height --- app/assets/stylesheets/framework/dropdowns.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 009d621fc746..5b647fc6176b 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -294,7 +294,7 @@ } .dropdown-content { - max-height: 200px; + max-height: 215px; overflow-y: scroll; } -- GitLab From 00e577cf5a9865dd87dd0479f88f77897c14c7e5 Mon Sep 17 00:00:00 2001 From: Patricio Cano <suprnova32@gmail.com> Date: Fri, 11 Mar 2016 11:47:34 -0500 Subject: [PATCH 177/223] Doc syntax fixes [ci skip] --- doc/integration/saml.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/integration/saml.md b/doc/integration/saml.md index b61b7cb96780..1c3dc707f6d7 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -133,15 +133,15 @@ will be returned to GitLab and will be signed in. ## Customization -### attribute_statements: +### `attribute_statements` >**Note:** This setting is only available on GitLab 8.6 and above. This setting should only be used to map attributes that are part of the OmniAuth info hash schema. -Used to map Attribute Names in a SAMLResponse to entries in the OmniAuth -[info hash](https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema#schema-10-and-later). +`attribute_statements` is used to map Attribute Names in a SAMLResponse to entries +in the OmniAuth [info hash](https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema#schema-10-and-later). For example, if your SAMLResponse contains an Attribute called 'EmailAddress', specify `{ email: ['EmailAddress'] }` to map the Attribute to the @@ -149,7 +149,7 @@ corresponding key in the info hash. URI-named Attributes are also supported, e. `{ email: ['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] }`. This setting allows you tell GitLab where to look for certain attributes required -to create an account. Like mentioned above, if your IdP send the user's email +to create an account. Like mentioned above, if your IdP sends the user's email address as `EmailAddress` instead of `email`, let GitLab know by setting it on your configuration: @@ -164,12 +164,12 @@ args: { } ``` -### allowed_clock_drift: +### `allowed_clock_drift` The clock of the Identity Provider may drift slightly ahead of your system clocks. -To allow for a small amount of clock drift you can use this argument within your -settings. Its value must be given in a number (and/or fraction) of seconds. The -value given is added to the current time at which the response is validated. +To allow for a small amount of clock drift you can use `allowed_clock_drift` within +your settings. Its value must be given in a number (and/or fraction) of seconds. +The value given is added to the current time at which the response is validated. ```yaml args: { -- GitLab From 1339de6785564d985c920ff13d0124c018c447ff Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 11 Mar 2016 21:26:18 +0200 Subject: [PATCH 178/223] Fix incorrect gitlab.rb variable in CI docs [ci skip] --- doc/ci/enable_or_disable_ci.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ci/enable_or_disable_ci.md b/doc/ci/enable_or_disable_ci.md index 9bd2f5aff228..c10f82054e2d 100644 --- a/doc/ci/enable_or_disable_ci.md +++ b/doc/ci/enable_or_disable_ci.md @@ -64,7 +64,7 @@ Save the file and restart GitLab: `sudo service gitlab restart`. For Omnibus installations, edit `/etc/gitlab/gitlab.rb` and add the line: ``` -gitlab-rails['gitlab_default_projects_features_builds'] = false +gitlab_rails['gitlab_default_projects_features_builds'] = false ``` Save the file and reconfigure GitLab: `sudo gitlab-ctl reconfigure`. -- GitLab From ae33cce36b7c0de3c9efa52efc4756248a874e14 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Fri, 26 Feb 2016 17:14:44 +0100 Subject: [PATCH 179/223] Added trigram indexes for various searched columns This allows the LIKE condition to use an index. Without a GIN + trigram index LIKE queries using a wildcard at the start _won't_ use an index and instead perform a sequence scan. --- ...14608_add_trigram_indexes_for_searching.rb | 50 +++++++++++++++++++ db/schema.rb | 21 ++++++++ 2 files changed, 71 insertions(+) create mode 100644 db/migrate/20160226114608_add_trigram_indexes_for_searching.rb diff --git a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb new file mode 100644 index 000000000000..fca5ac01a085 --- /dev/null +++ b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb @@ -0,0 +1,50 @@ +class AddTrigramIndexesForSearching < ActiveRecord::Migration + disable_ddl_transaction! + + def up + return unless Gitlab::Database.postgresql? + + unless trigrams_enabled? + raise 'You must enable the pg_trgm extension as a PostgreSQL super user' + end + + # trigram indexes are case-insensitive so we can just index the column + # instead of indexing lower(column) + to_index.each do |table, columns| + columns.each do |column| + execute "CREATE INDEX CONCURRENTLY index_#{table}_on_#{column}_trigram ON #{table} USING gin(#{column} gin_trgm_ops);" + end + end + end + + def down + return unless Gitlab::Database.postgresql? + + to_index.each do |table, columns| + columns.each do |column| + remove_index table, name: "index_#{table}_on_#{column}_trigram" + end + end + end + + def trigrams_enabled? + res = execute("SELECT true AS enabled FROM pg_available_extensions WHERE name = 'pg_trgm' AND installed_version IS NOT NULL;") + row = res.first + + row && row['enabled'] == 't' ? true : false + end + + def to_index + { + ci_runners: [:token, :description], + issues: [:title, :description], + merge_requests: [:title, :description], + milestones: [:title, :description], + namespaces: [:name, :path], + notes: [:note], + projects: [:name, :path, :description], + snippets: [:title, :file_name], + users: [:username, :name, :email] + } + end +end diff --git a/db/schema.rb b/db/schema.rb index a74b86d8e2fd..8f44ccebac86 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -15,6 +15,7 @@ # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + enable_extension "pg_trgm" create_table "abuse_reports", force: :cascade do |t| t.integer "reporter_id" @@ -258,6 +259,9 @@ t.string "architecture" end + add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin + add_index "ci_runners", ["token"], name: "index_ci_runners_on_token_trigram", using: :gin + create_table "ci_services", force: :cascade do |t| t.string "type" t.string "title" @@ -417,11 +421,13 @@ add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree add_index "issues", ["created_at", "id"], name: "index_issues_on_created_at_and_id", using: :btree add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree + add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree add_index "issues", ["state"], name: "index_issues_on_state", using: :btree add_index "issues", ["title"], name: "index_issues_on_title", using: :btree + add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin create_table "keys", force: :cascade do |t| t.integer "user_id" @@ -543,12 +549,14 @@ add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree add_index "merge_requests", ["created_at", "id"], name: "index_merge_requests_on_created_at_and_id", using: :btree add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree + add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree add_index "merge_requests", ["source_project_id"], name: "index_merge_requests_on_source_project_id", using: :btree add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree + add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin create_table "milestones", force: :cascade do |t| t.string "title", null: false @@ -562,10 +570,12 @@ end add_index "milestones", ["created_at", "id"], name: "index_milestones_on_created_at_and_id", using: :btree + add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree + add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin create_table "namespaces", force: :cascade do |t| t.string "name", null: false @@ -580,8 +590,10 @@ add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree + add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree + add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree create_table "notes", force: :cascade do |t| @@ -607,6 +619,7 @@ add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree add_index "notes", ["is_award"], name: "index_notes_on_is_award", using: :btree add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree + add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree add_index "notes", ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree @@ -705,9 +718,12 @@ add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree + add_index "projects", ["description"], name: "index_projects_on_description_trigram", using: :gin add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree + add_index "projects", ["name"], name: "index_projects_on_name_trigram", using: :gin add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree add_index "projects", ["path"], name: "index_projects_on_path", using: :btree + add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree @@ -785,7 +801,9 @@ add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree add_index "snippets", ["created_at", "id"], name: "index_snippets_on_created_at_and_id", using: :btree add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree + add_index "snippets", ["file_name"], name: "index_snippets_on_file_name_trigram", using: :gin add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree + add_index "snippets", ["title"], name: "index_snippets_on_title_trigram", using: :gin add_index "snippets", ["updated_at"], name: "index_snippets_on_updated_at", using: :btree add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree @@ -919,9 +937,12 @@ add_index "users", ["created_at", "id"], name: "index_users_on_created_at_and_id", using: :btree add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree + add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin add_index "users", ["name"], name: "index_users_on_name", using: :btree + add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree add_index "users", ["username"], name: "index_users_on_username", using: :btree + add_index "users", ["username"], name: "index_users_on_username_trigram", using: :gin create_table "users_star_projects", force: :cascade do |t| t.integer "project_id", null: false -- GitLab From 68d2a8b2453118255eed85bb1536d0867b82322b Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 1 Mar 2016 12:02:06 +0100 Subject: [PATCH 180/223] Use ILIKE/LIKE + UNION in Project.search This chance is broken up in two steps: 1. Use ILIKE on PostgreSQL and LIKE on MySQL, instead of using "WHERE lower(x) LIKE lower(y)" as ILIKE is significantly faster than using lower(). In many cases the use of lower() will force a slow sequence scan. 2. Instead of using 1 query that searches both projects and namespaces using a JOIN we're using 2 separate queries that are UNION'd together. Using a JOIN would force a slow sequence scan, using a UNION avoids this. This method now uses Arel as Arel automatically uses ILIKE on PostgreSQL and LIKE on MySQL, removing the need to handle this manually. --- app/models/project.rb | 30 +++++++++++++++++----- spec/models/project_spec.rb | 51 +++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 3a7d23f05439..b45d4a0bc180 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -266,13 +266,31 @@ def active joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') end + # Searches for a list of projects based on the query given in `query`. + # + # On PostgreSQL this method uses "ILIKE" to perform a case-insensitive + # search. On MySQL a regular "LIKE" is used as it's already + # case-insensitive. + # + # query - The search query as a String. def search(query) - joins(:namespace). - where('LOWER(projects.name) LIKE :query OR - LOWER(projects.path) LIKE :query OR - LOWER(namespaces.name) LIKE :query OR - LOWER(projects.description) LIKE :query', - query: "%#{query.try(:downcase)}%") + ptable = Project.arel_table + ntable = Namespace.arel_table + pattern = "%#{query}%" + + projects = select(:id).where( + ptable[:path].matches(pattern). + or(ptable[:name].matches(pattern)). + or(ptable[:description].matches(pattern)) + ) + + namespaces = select(:id). + joins(:namespace). + where(ntable[:name].matches(pattern)) + + union = Gitlab::SQL::Union.new([projects, namespaces]) + + where("projects.id IN (#{union.to_sql})") end def search_by_visibility(level) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 2fa38a5d3d38..6627432aa83a 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -582,7 +582,58 @@ it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_truthy } it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PUBLIC)).to be_falsey } end + end + + describe '.search' do + let(:project) { create(:project, description: 'kitten mittens') } + + it 'returns projects with a matching name' do + expect(described_class.search(project.name)).to eq([project]) + end + it 'returns projects with a partially matching name' do + expect(described_class.search(project.name[0..2])).to eq([project]) + end + + it 'returns projects with a matching name regardless of the casing' do + expect(described_class.search(project.name.upcase)).to eq([project]) + end + + it 'returns projects with a matching description' do + expect(described_class.search(project.description)).to eq([project]) + end + + it 'returns projects with a partially matching description' do + expect(described_class.search('kitten')).to eq([project]) + end + + it 'returns projects with a matching description regardless of the casing' do + expect(described_class.search('KITTEN')).to eq([project]) + end + + it 'returns projects with a matching path' do + expect(described_class.search(project.path)).to eq([project]) + end + + it 'returns projects with a partially matching path' do + expect(described_class.search(project.path[0..2])).to eq([project]) + end + + it 'returns projects with a matching path regardless of the casing' do + expect(described_class.search(project.path.upcase)).to eq([project]) + end + + it 'returns projects with a matching namespace name' do + expect(described_class.search(project.namespace.name)).to eq([project]) + end + + it 'returns projects with a partially matching namespace name' do + expect(described_class.search(project.namespace.name[0..2])).to eq([project]) + end + + it 'returns projects with a matching namespace name regardless of the casing' do + expect(described_class.search(project.namespace.name.upcase)).to eq([project]) + end end describe '#rename_repo' do -- GitLab From c957321bff3e478d41a4320547688247ffe561e5 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 1 Mar 2016 12:16:15 +0100 Subject: [PATCH 181/223] Use ILIKE in Project.search_by_title Similar to the changes made to Project.search the method Project.search_by_title now also uses Arel so it can automatically use ILIKE/LIKE instead of the lower() function. --- app/models/project.rb | 5 ++++- spec/models/project_spec.rb | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index b45d4a0bc180..e55d3e7f7873 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -298,7 +298,10 @@ def search_by_visibility(level) end def search_by_title(query) - non_archived.where('LOWER(projects.name) LIKE :query', query: "%#{query.downcase}%") + pattern = "%#{query}%" + table = Project.arel_table + + non_archived.where(table[:name].matches(pattern)) end def find_with_namespace(id) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 6627432aa83a..59c5ffa6b9c3 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -698,4 +698,20 @@ project.expire_caches_before_rename('foo') end end + + describe '.search_by_title' do + let(:project) { create(:project, name: 'kittens') } + + it 'returns projects with a matching name' do + expect(described_class.search_by_title(project.name)).to eq([project]) + end + + it 'returns projects with a partially matching name' do + expect(described_class.search_by_title('kitten')).to eq([project]) + end + + it 'returns projects with a matching name regardless of the casing' do + expect(described_class.search_by_title('KITTENS')).to eq([project]) + end + end end -- GitLab From e5be8ea6ae02e968d1a7de8710af6ae4b9095f19 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 1 Mar 2016 12:51:01 +0100 Subject: [PATCH 182/223] Use ILIKE/LIKE for searching snippets Previously this used a regular LIKE which is case-sensitive on PostgreSQL. This ensures that for both PostgreSQL and MySQL the searching is case-insensitive similar to searching for projects. --- app/models/snippet.rb | 24 ++++++++++++++++++-- spec/models/snippet_spec.rb | 44 +++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/app/models/snippet.rb b/app/models/snippet.rb index dd3925c7a7d4..35d05af38bfe 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -113,12 +113,32 @@ def visibility_level_field end class << self + # Searches for snippets with a matching title or file name. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String. + # + # Returns an ActiveRecord::Relation. def search(query) - where('(title LIKE :query OR file_name LIKE :query)', query: "%#{query}%") + t = Snippet.arel_table + pattern = "%#{query}%" + + where(t[:title].matches(pattern).or(t[:file_name].matches(pattern))) end + # Searches for snippets with matching content. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String. + # + # Returns an ActiveRecord::Relation. def search_code(query) - where('(content LIKE :query)', query: "%#{query}%") + table = Snippet.arel_table + pattern = "%#{query}%" + + where(table[:content].matches(pattern)) end def accessible_to(user) diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index 7e5b5499aeaa..5077ac7b62bd 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -59,4 +59,48 @@ expect(snippet.to_reference(cross)).to eq "#{project.to_reference}$#{snippet.id}" end end + + describe '.search' do + let(:snippet) { create(:snippet) } + + it 'returns snippets with a matching title' do + expect(described_class.search(snippet.title)).to eq([snippet]) + end + + it 'returns snippets with a partially matching title' do + expect(described_class.search(snippet.title[0..2])).to eq([snippet]) + end + + it 'returns snippets with a matching title regardless of the casing' do + expect(described_class.search(snippet.title.upcase)).to eq([snippet]) + end + + it 'returns snippets with a matching file name' do + expect(described_class.search(snippet.file_name)).to eq([snippet]) + end + + it 'returns snippets with a partially matching file name' do + expect(described_class.search(snippet.file_name[0..2])).to eq([snippet]) + end + + it 'returns snippets with a matching file name regardless of the casing' do + expect(described_class.search(snippet.file_name.upcase)).to eq([snippet]) + end + end + + describe '#search_code' do + let(:snippet) { create(:snippet, content: 'class Foo; end') } + + it 'returns snippets with matching content' do + expect(described_class.search_code(snippet.content)).to eq([snippet]) + end + + it 'returns snippets with partially matching content' do + expect(described_class.search_code('class')).to eq([snippet]) + end + + it 'returns snippets with matching content regardless of the casing' do + expect(described_class.search_code('FOO')).to eq([snippet]) + end + end end -- GitLab From 05c7619ec597b2bcc5cef4345eadd832a413fd7d Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 1 Mar 2016 15:43:19 +0100 Subject: [PATCH 183/223] Use ILIKE/LIKE for searching notes --- app/models/note.rb | 12 +++++++++++- spec/models/note_spec.rb | 12 +++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/models/note.rb b/app/models/note.rb index 3b20d5d22b68..76e86fdbcaa5 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -105,8 +105,18 @@ def build_discussion_id(type, id, line_code) [:discussion, type.try(:underscore), id, line_code].join("-").to_sym end + # Searches for notes matching the given query. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String. + # + # Returns an ActiveRecord::Relation. def search(query) - where("LOWER(note) like :query", query: "%#{query.downcase}%") + table = Note.arel_table + pattern = "%#{query}%" + + where(table[:note].matches(pattern)) end def grouped_awards diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 33085dac4eae..cd620ea5440a 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -140,10 +140,16 @@ end end - describe :search do - let!(:note) { create(:note, note: "WoW") } + describe '.search' do + let(:note) { create(:note, note: 'WoW') } - it { expect(Note.search('wow')).to include(note) } + it 'returns notes with matching content' do + expect(described_class.search(note.note)).to eq([note]) + end + + it 'returns notes with matching content regardless of the casing' do + expect(described_class.search('WOW')).to eq([note]) + end end describe :grouped_awards do -- GitLab From b49a095d8a81e8a168cd3d4bb2f0cdcc3a2736de Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 1 Mar 2016 16:08:48 +0100 Subject: [PATCH 184/223] Use ILIKE/LIKE for searching users --- app/models/user.rb | 16 +++++++++++++- spec/models/user_spec.rb | 48 +++++++++++++++++++++++++++++++--------- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 505a547d8ec6..fc4cf92be60b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -286,8 +286,22 @@ def filter(filter_name) end end + # Searches users matching the given query. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String + # + # Returns an ActiveRecord::Relation. def search(query) - where("lower(name) LIKE :query OR lower(email) LIKE :query OR lower(username) LIKE :query", query: "%#{query.downcase}%") + table = User.arel_table + pattern = "%#{query}%" + + where( + table[:name].matches(pattern). + or(table[:email].matches(pattern)). + or(table[:username].matches(pattern)) + ) end def by_login(login) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 412101ac9f9b..90207f5153a9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -463,17 +463,43 @@ end end - describe 'search' do - let(:user1) { create(:user, username: 'James', email: 'james@testing.com') } - let(:user2) { create(:user, username: 'jameson', email: 'jameson@example.com') } - - it "should be case insensitive" do - expect(User.search(user1.username.upcase).to_a).to eq([user1]) - expect(User.search(user1.username.downcase).to_a).to eq([user1]) - expect(User.search(user2.username.upcase).to_a).to eq([user2]) - expect(User.search(user2.username.downcase).to_a).to eq([user2]) - expect(User.search(user1.username.downcase).to_a.size).to eq(2) - expect(User.search(user2.username.downcase).to_a.size).to eq(1) + describe '.search' do + let(:user) { create(:user) } + + it 'returns users with a matching name' do + expect(described_class.search(user.name)).to eq([user]) + end + + it 'returns users with a partially matching name' do + expect(described_class.search(user.name[0..2])).to eq([user]) + end + + it 'returns users with a matching name regarding of the casing' do + expect(described_class.search(user.name.upcase)).to eq([user]) + end + + it 'returns users with a matching Email' do + expect(described_class.search(user.email)).to eq([user]) + end + + it 'returns users with a partially matching Email' do + expect(described_class.search(user.email[0..2])).to eq([user]) + end + + it 'returns users with a matching Email regarding of the casing' do + expect(described_class.search(user.email.upcase)).to eq([user]) + end + + it 'returns users with a matching username' do + expect(described_class.search(user.username)).to eq([user]) + end + + it 'returns users with a partially matching username' do + expect(described_class.search(user.username[0..2])).to eq([user]) + end + + it 'returns users with a matching username regarding of the casing' do + expect(described_class.search(user.username.upcase)).to eq([user]) end end -- GitLab From 4f4101ed902e289356ff20f0085e8469a93443e2 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 1 Mar 2016 16:15:42 +0100 Subject: [PATCH 185/223] Use ILIKE/LIKE for searching groups --- app/models/group.rb | 12 +++++++++++- spec/models/group_spec.rb | 26 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/app/models/group.rb b/app/models/group.rb index 76042b3e3fd3..bfeddf8b4d20 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -33,8 +33,18 @@ class Group < Namespace after_destroy :post_destroy_hook class << self + # Searches for groups matching the given query. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String + # + # Returns an ActiveRecord::Relation. def search(query) - where("LOWER(namespaces.name) LIKE :query or LOWER(namespaces.path) LIKE :query", query: "%#{query.downcase}%") + table = Group.arel_table + pattern = "%#{query}%" + + where(table[:name].matches(pattern).or(table[:path].matches(pattern))) end def sort(method) diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 3c995053eecf..c9245fc95354 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -103,4 +103,30 @@ expect(group.avatar_type).to eq(["only images allowed"]) end end + + describe '.search' do + it 'returns groups with a matching name' do + expect(described_class.search(group.name)).to eq([group]) + end + + it 'returns groups with a partially matching name' do + expect(described_class.search(group.name[0..2])).to eq([group]) + end + + it 'returns groups with a matching name regardless of the casing' do + expect(described_class.search(group.name.upcase)).to eq([group]) + end + + it 'returns groups with a matching path' do + expect(described_class.search(group.path)).to eq([group]) + end + + it 'returns groups with a partially matching path' do + expect(described_class.search(group.path[0..2])).to eq([group]) + end + + it 'returns groups with a matching path regardless of the casing' do + expect(described_class.search(group.path.upcase)).to eq([group]) + end + end end -- GitLab From 0b95ce106d4cc95f0df13792cab69e6dc54da19e Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 1 Mar 2016 16:41:18 +0100 Subject: [PATCH 186/223] Use ILIKE/LIKE for searching CI runners --- app/models/ci/runner.rb | 20 +++++++++++++++++--- spec/models/ci/runner_spec.rb | 28 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index e725a6d468c2..ce2750b071c2 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -23,7 +23,7 @@ class Runner < ActiveRecord::Base LAST_CONTACT_TIME = 5.minutes.ago AVAILABLE_SCOPES = ['specific', 'shared', 'active', 'paused', 'online'] - + has_many :builds, class_name: 'Ci::Build' has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' has_many :projects, through: :runner_projects, class_name: '::Project', foreign_key: :gl_project_id @@ -46,9 +46,23 @@ class Runner < ActiveRecord::Base acts_as_taggable + # Searches for runners matching the given query. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # This method performs a *partial* match on tokens, thus a query for "a" + # will match any runner where the token contains the letter "a". As a result + # you should *not* use this method for non-admin purposes as otherwise users + # might be able to query a list of all runners. + # + # query - The search query as a String + # + # Returns an ActiveRecord::Relation. def self.search(query) - where('LOWER(ci_runners.token) LIKE :query OR LOWER(ci_runners.description) like :query', - query: "%#{query.try(:downcase)}%") + t = Ci::Runner.arel_table + pattern = "%#{query}%" + + where(t[:token].matches(pattern).or(t[:description].matches(pattern))) end def set_default_values diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index e891838672ea..25e9e5eca48a 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -132,4 +132,32 @@ expect(runner.belongs_to_one_project?).to be_truthy end end + + describe '#search' do + let(:runner) { create(:ci_runner, token: '123abc') } + + it 'returns runners with a matching token' do + expect(described_class.search(runner.token)).to eq([runner]) + end + + it 'returns runners with a partially matching token' do + expect(described_class.search(runner.token[0..2])).to eq([runner]) + end + + it 'returns runners with a matching token regardless of the casing' do + expect(described_class.search(runner.token.upcase)).to eq([runner]) + end + + it 'returns runners with a matching description' do + expect(described_class.search(runner.description)).to eq([runner]) + end + + it 'returns runners with a partially matching description' do + expect(described_class.search(runner.description[0..2])).to eq([runner]) + end + + it 'returns runners with a matching description regardless of the casing' do + expect(described_class.search(runner.description.upcase)).to eq([runner]) + end + end end -- GitLab From 04e477f37496f8da56574baf3a328ca7f603b757 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 1 Mar 2016 16:59:36 +0100 Subject: [PATCH 187/223] Use ILIKE/LIKE for Issuable.search and full_search --- app/models/concerns/issuable.rb | 21 ++++++++++-- spec/models/concerns/issuable_spec.rb | 47 ++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 27b97944e381..3c42f582937f 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -61,12 +61,29 @@ module Issuable end module ClassMethods + # Searches for records with a matching title. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String + # + # Returns an ActiveRecord::Relation. def search(query) - where("LOWER(title) like :query", query: "%#{query.downcase}%") + where(arel_table[:title].matches("%#{query}%")) end + # Searches for records with a matching title or description. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String + # + # Returns an ActiveRecord::Relation. def full_search(query) - where("LOWER(title) like :query OR LOWER(description) like :query", query: "%#{query.downcase}%") + t = arel_table + pattern = "%#{query}%" + + where(t[:title].matches(pattern).or(t[:description].matches(pattern))) end def sort(method) diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 600089802b2a..f0e4de8539ec 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -32,9 +32,54 @@ describe ".search" do let!(:searchable_issue) { create(:issue, title: "Searchable issue") } - it "matches by title" do + it 'returns notches with a matching title' do + expect(described_class.search(searchable_issue.title)). + to eq([searchable_issue]) + end + + it 'returns notes with a partially matching title' do expect(described_class.search('able')).to eq([searchable_issue]) end + + it 'returns notes with a matching title regardless of the casing' do + expect(described_class.search(searchable_issue.title.upcase)). + to eq([searchable_issue]) + end + end + + describe ".full_search" do + let!(:searchable_issue) do + create(:issue, title: "Searchable issue", description: 'kittens') + end + + it 'returns notches with a matching title' do + expect(described_class.full_search(searchable_issue.title)). + to eq([searchable_issue]) + end + + it 'returns notes with a partially matching title' do + expect(described_class.full_search('able')).to eq([searchable_issue]) + end + + it 'returns notes with a matching title regardless of the casing' do + expect(described_class.full_search(searchable_issue.title.upcase)). + to eq([searchable_issue]) + end + + it 'returns notches with a matching description' do + expect(described_class.full_search(searchable_issue.description)). + to eq([searchable_issue]) + end + + it 'returns notes with a partially matching description' do + expect(described_class.full_search(searchable_issue.description)). + to eq([searchable_issue]) + end + + it 'returns notes with a matching description regardless of the casing' do + expect(described_class.full_search(searchable_issue.description.upcase)). + to eq([searchable_issue]) + end end describe "#today?" do -- GitLab From aaf0c671f75ee97451a24586b377653b777ae234 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 1 Mar 2016 17:05:26 +0100 Subject: [PATCH 188/223] Use ILIKE/LIKE for searching milestones --- app/models/milestone.rb | 13 +++++++++++-- spec/models/milestone_spec.rb | 30 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index e3969f32dd6a..e3b6c552f92f 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -58,9 +58,18 @@ class Milestone < ActiveRecord::Base alias_attribute :name, :title class << self + # Searches for milestones matching the given query. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String + # + # Returns an ActiveRecord::Relation. def search(query) - query = "%#{query}%" - where("title like ? or description like ?", query, query) + t = arel_table + pattern = "%#{query}%" + + where(t[:title].matches(pattern).or(t[:description].matches(pattern))) end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 28f13100d15a..de1757bf67a5 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -181,4 +181,34 @@ expect(issue4.position).to eq(42) end end + + describe '.search' do + let(:milestone) { create(:milestone, title: 'foo', description: 'bar') } + + it 'returns milestones with a matching title' do + expect(described_class.search(milestone.title)).to eq([milestone]) + end + + it 'returns milestones with a partially matching title' do + expect(described_class.search(milestone.title[0..2])).to eq([milestone]) + end + + it 'returns milestones with a matching title regardless of the casing' do + expect(described_class.search(milestone.title.upcase)).to eq([milestone]) + end + + it 'returns milestones with a matching description' do + expect(described_class.search(milestone.description)).to eq([milestone]) + end + + it 'returns milestones with a partially matching description' do + expect(described_class.search(milestone.description[0..2])). + to eq([milestone]) + end + + it 'returns milestones with a matching description regardless of the casing' do + expect(described_class.search(milestone.description.upcase)). + to eq([milestone]) + end + end end -- GitLab From f84f367c9dc65c3cf9d58733b03e68efb790b635 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 1 Mar 2016 17:56:14 +0100 Subject: [PATCH 189/223] Refactor Gitlab::SearchResults Instead of plucking IDs this class now uses ActiveRecord::Relation objects. Plucking IDs is problematic as searching for projects can lead to a huge amount of IDs being loaded into memory only to be used as an argument for another query (instead of just using a sub-query). --- app/services/search/global_service.rb | 3 +- lib/gitlab/search_results.rb | 25 +++++++----- spec/lib/gitlab/search_results_spec.rb | 55 ++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 spec/lib/gitlab/search_results_spec.rb diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb index e904cb6c6fcc..e1e94c5cc38d 100644 --- a/app/services/search/global_service.rb +++ b/app/services/search/global_service.rb @@ -10,9 +10,8 @@ def execute group = Group.find_by(id: params[:group_id]) if params[:group_id].present? projects = ProjectsFinder.new.execute(current_user) projects = projects.in_namespace(group.id) if group - project_ids = projects.pluck(:id) - Gitlab::SearchResults.new(project_ids, params[:search]) + Gitlab::SearchResults.new(projects, params[:search]) end end end diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 2ab2d4af797d..036fa6f937c3 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -2,12 +2,12 @@ module Gitlab class SearchResults attr_reader :query - # Limit search results by passed project ids + # Limit search results by passed projects # It allows us to search only for projects user has access to - attr_reader :limit_project_ids + attr_reader :limit_projects - def initialize(limit_project_ids, query) - @limit_project_ids = limit_project_ids || Project.all + def initialize(limit_projects, query) + @limit_projects = limit_projects || Project.all @query = Shellwords.shellescape(query) if query.present? end @@ -27,7 +27,8 @@ def objects(scope, page = nil) end def total_count - @total_count ||= projects_count + issues_count + merge_requests_count + milestones_count + @total_count ||= projects_count + issues_count + merge_requests_count + + milestones_count end def projects_count @@ -53,27 +54,29 @@ def empty? private def projects - Project.where(id: limit_project_ids).search(query) + limit_projects.search(query) end def issues - issues = Issue.where(project_id: limit_project_ids) + issues = Issue.where(project_id: project_ids_relation) + if query =~ /#(\d+)\z/ issues = issues.where(iid: $1) else issues = issues.full_search(query) end + issues.order('updated_at DESC') end def milestones - milestones = Milestone.where(project_id: limit_project_ids) + milestones = Milestone.where(project_id: project_ids_relation) milestones = milestones.search(query) milestones.order('updated_at DESC') end def merge_requests - merge_requests = MergeRequest.in_projects(limit_project_ids) + merge_requests = MergeRequest.in_projects(project_ids_relation) if query =~ /[#!](\d+)\z/ merge_requests = merge_requests.where(iid: $1) else @@ -89,5 +92,9 @@ def default_scope def per_page 20 end + + def project_ids_relation + limit_projects.select(:id) + end end end diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb new file mode 100644 index 000000000000..bb18f4178582 --- /dev/null +++ b/spec/lib/gitlab/search_results_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Gitlab::SearchResults do + let!(:project) { create(:project, name: 'foo') } + let!(:issue) { create(:issue, project: project, title: 'foo') } + + let!(:merge_request) do + create(:merge_request, source_project: project, title: 'foo') + end + + let!(:milestone) { create(:milestone, project: project, title: 'foo') } + let(:results) { described_class.new(Project.all, 'foo') } + + describe '#total_count' do + it 'returns the total amount of search hits' do + expect(results.total_count).to eq(4) + end + end + + describe '#projects_count' do + it 'returns the total amount of projects' do + expect(results.projects_count).to eq(1) + end + end + + describe '#issues_count' do + it 'returns the total amount of issues' do + expect(results.issues_count).to eq(1) + end + end + + describe '#merge_requests_count' do + it 'returns the total amount of merge requests' do + expect(results.merge_requests_count).to eq(1) + end + end + + describe '#milestones_count' do + it 'returns the total amount of milestones' do + expect(results.milestones_count).to eq(1) + end + end + + describe '#empty?' do + it 'returns true when there are no search results' do + allow(results).to receive(:total_count).and_return(0) + + expect(results.empty?).to eq(true) + end + + it 'returns false when there are search results' do + expect(results.empty?).to eq(false) + end + end +end -- GitLab From d726d994af6c1e2e314b999ee78200346e3bbcfd Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 1 Mar 2016 18:01:09 +0100 Subject: [PATCH 190/223] Refactor Gitlab::SnippetSearchResults This removes the need for plucking snippet IDs into memory. --- app/services/search/snippet_service.rb | 5 ++-- lib/gitlab/snippet_search_results.rb | 10 ++++---- .../lib/gitlab/snippet_search_results_spec.rb | 25 +++++++++++++++++++ 3 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 spec/lib/gitlab/snippet_search_results_spec.rb diff --git a/app/services/search/snippet_service.rb b/app/services/search/snippet_service.rb index 8ca0877321d7..0b3e713e2200 100644 --- a/app/services/search/snippet_service.rb +++ b/app/services/search/snippet_service.rb @@ -7,8 +7,9 @@ def initialize(user, params) end def execute - snippet_ids = Snippet.accessible_to(current_user).pluck(:id) - Gitlab::SnippetSearchResults.new(snippet_ids, params[:search]) + snippets = Snippet.accessible_to(current_user) + + Gitlab::SnippetSearchResults.new(snippets, params[:search]) end end end diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb index addda95be2ba..e0e74ff8359f 100644 --- a/lib/gitlab/snippet_search_results.rb +++ b/lib/gitlab/snippet_search_results.rb @@ -2,10 +2,10 @@ module Gitlab class SnippetSearchResults < SearchResults include SnippetsHelper - attr_reader :limit_snippet_ids + attr_reader :limit_snippets - def initialize(limit_snippet_ids, query) - @limit_snippet_ids = limit_snippet_ids + def initialize(limit_snippets, query) + @limit_snippets = limit_snippets @query = query end @@ -35,11 +35,11 @@ def snippet_blobs_count private def snippet_titles - Snippet.where(id: limit_snippet_ids).search(query).order('updated_at DESC') + limit_snippets.search(query).order('updated_at DESC') end def snippet_blobs - Snippet.where(id: limit_snippet_ids).search_code(query).order('updated_at DESC') + limit_snippets.search_code(query).order('updated_at DESC') end def default_scope diff --git a/spec/lib/gitlab/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb new file mode 100644 index 000000000000..e86b9ef6a634 --- /dev/null +++ b/spec/lib/gitlab/snippet_search_results_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Gitlab::SnippetSearchResults do + let!(:snippet) { create(:snippet, content: 'foo', file_name: 'foo') } + + let(:results) { described_class.new(Snippet.all, 'foo') } + + describe '#total_count' do + it 'returns the total amount of search hits' do + expect(results.total_count).to eq(2) + end + end + + describe '#snippet_titles_count' do + it 'returns the amount of matched snippet titles' do + expect(results.snippet_titles_count).to eq(1) + end + end + + describe '#snippet_blobs_count' do + it 'returns the amount of matched snippet blobs' do + expect(results.snippet_blobs_count).to eq(1) + end + end +end -- GitLab From a8b35a5716721ee6c839820f3a29024583a4216a Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Tue, 1 Mar 2016 18:04:37 +0100 Subject: [PATCH 191/223] Refactor Gitlab::ProjectSearchResults Previously this class would be given a project ID which was then used to retrieve the corresponding Project object. However, in all cases the Project object was already known as it was used to grab the ID to pass to ProjectSearchResults. By just passing a Project instead we remove the need for an extra query as well as the need for some other complexity in this class. --- app/services/search/project_service.rb | 2 +- lib/gitlab/project_search_results.rb | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb index f630c0a37903..c08881dce4b8 100644 --- a/app/services/search/project_service.rb +++ b/app/services/search/project_service.rb @@ -7,7 +7,7 @@ def initialize(project, user, params) end def execute - Gitlab::ProjectSearchResults.new(project.id, + Gitlab::ProjectSearchResults.new(project, params[:search], params[:repository_ref]) end diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 70de6a74e767..f36600c57569 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -2,8 +2,8 @@ module Gitlab class ProjectSearchResults < SearchResults attr_reader :project, :repository_ref - def initialize(project_id, query, repository_ref = nil) - @project = Project.find(project_id) + def initialize(project, query, repository_ref = nil) + @project = project @repository_ref = if repository_ref.present? repository_ref else @@ -73,7 +73,7 @@ def wiki_blobs end def notes - Note.where(project_id: limit_project_ids).user.search(query).order('updated_at DESC') + project.notes.user.search(query).order('updated_at DESC') end def commits @@ -83,9 +83,5 @@ def commits project.repository.find_commits_by_message(query).compact end end - - def limit_project_ids - [project.id] - end end end -- GitLab From a14f2c3c4870e60e3e14c6b1cdae3faa76e623ed Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Thu, 3 Mar 2016 12:35:06 +0100 Subject: [PATCH 192/223] Backport Rails support for PostgreSQL opclasses This is needed to support creating/dumping/loading indexes that use the gin_trgm_ops operator class on PostgreSQL. These changes are taken from Rails pull request https://github.com/rails/rails/pull/19090. --- .../postgresql_opclasses_support.rb | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 config/initializers/postgresql_opclasses_support.rb diff --git a/config/initializers/postgresql_opclasses_support.rb b/config/initializers/postgresql_opclasses_support.rb new file mode 100644 index 000000000000..ac803778ab00 --- /dev/null +++ b/config/initializers/postgresql_opclasses_support.rb @@ -0,0 +1,186 @@ +# These changes add support for PostgreSQL operator classes when creating +# indexes and dumping/loading schemas. Taken from Rails pull request +# https://github.com/rails/rails/pull/19090. +# +# License: +# +# Copyright (c) 2004-2016 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# 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. + +require 'date' +require 'set' +require 'bigdecimal' +require 'bigdecimal/util' + +# As the Struct definition is changed in this PR/patch we have to first remove +# the existing one. +ActiveRecord::ConnectionAdapters.send(:remove_const, :IndexDefinition) + +module ActiveRecord + module ConnectionAdapters #:nodoc: + # Abstract representation of an index definition on a table. Instances of + # this type are typically created and returned by methods in database + # adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes + class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :opclasses) #:nodoc: + end + end +end + + +module ActiveRecord + module ConnectionAdapters # :nodoc: + module SchemaStatements + def add_index_options(table_name, column_name, options = {}) #:nodoc: + column_names = Array(column_name) + index_name = index_name(table_name, column: column_names) + + options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type, :opclasses) + + index_type = options[:unique] ? "UNIQUE" : "" + index_type = options[:type].to_s if options.key?(:type) + index_name = options[:name].to_s if options.key?(:name) + max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length + + if options.key?(:algorithm) + algorithm = index_algorithms.fetch(options[:algorithm]) { + raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}") + } + end + + using = "USING #{options[:using]}" if options[:using].present? + + if supports_partial_index? + index_options = options[:where] ? " WHERE #{options[:where]}" : "" + end + + if index_name.length > max_index_length + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters" + end + if table_exists?(table_name) && index_name_exists?(table_name, index_name, false) + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" + end + index_columns = quoted_columns_for_index(column_names, options).join(", ") + + [index_name, index_type, index_columns, index_options, algorithm, using] + end + end + end +end + +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module SchemaStatements + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) + result = query(<<-SQL, 'SCHEMA') + SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid + FROM pg_class t + INNER JOIN pg_index d ON t.oid = d.indrelid + INNER JOIN pg_class i ON d.indexrelid = i.oid + WHERE i.relkind = 'i' + AND d.indisprimary = 'f' + AND t.relname = '#{table_name}' + AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) ) + ORDER BY i.relname + SQL + + result.map do |row| + index_name = row[0] + unique = row[1] == 't' + indkey = row[2].split(" ") + inddef = row[3] + oid = row[4] + + columns = Hash[query(<<-SQL, "SCHEMA")] + SELECT a.attnum, a.attname + FROM pg_attribute a + WHERE a.attrelid = #{oid} + AND a.attnum IN (#{indkey.join(",")}) + SQL + + column_names = columns.values_at(*indkey).compact + + unless column_names.empty? + # add info on sort order for columns (only desc order is explicitly specified, asc is the default) + desc_order_columns = inddef.scan(/(\w+) DESC/).flatten + orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} + where = inddef.scan(/WHERE (.+)$/).flatten[0] + using = inddef.scan(/USING (.+?) /).flatten[0].to_sym + opclasses = Hash[inddef.scan(/\((.+)\)$/).flatten[0].split(',').map do |column_and_opclass| + column, opclass = column_and_opclass.split(' ').map(&:strip) + [column, opclass] if opclass + end.compact] + + IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using, opclasses) + end + end.compact + end + + def add_index(table_name, column_name, options = {}) #:nodoc: + index_name, index_type, index_columns_and_opclasses, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options) + execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns_and_opclasses})#{index_options}" + end + + protected + + def quoted_columns_for_index(column_names, options = {}) + column_opclasses = options[:opclasses] || {} + column_names.map {|name| "#{quote_column_name(name)} #{column_opclasses[name]}"} + end + end + end + end +end + +module ActiveRecord + class SchemaDumper + private + + def indexes(table, stream) + if (indexes = @connection.indexes(table)).any? + add_index_statements = indexes.map do |index| + statement_parts = [ + "add_index #{remove_prefix_and_suffix(index.table).inspect}", + index.columns.inspect, + "name: #{index.name.inspect}", + ] + statement_parts << 'unique: true' if index.unique + + index_lengths = (index.lengths || []).compact + statement_parts << "length: #{Hash[index.columns.zip(index.lengths)].inspect}" if index_lengths.any? + + index_orders = index.orders || {} + statement_parts << "order: #{index.orders.inspect}" if index_orders.any? + statement_parts << "where: #{index.where.inspect}" if index.where + statement_parts << "using: #{index.using.inspect}" if index.using + statement_parts << "type: #{index.type.inspect}" if index.type + statement_parts << "opclasses: #{index.opclasses}" if index.opclasses.present? + + " #{statement_parts.join(', ')}" + end + + stream.puts add_index_statements.sort.join("\n") + stream.puts + end + end + end +end -- GitLab From de1788dc8f243ea83b190a8393810ebf7ccc7667 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Thu, 3 Mar 2016 12:36:28 +0100 Subject: [PATCH 193/223] Updated schema to include Pg operator classes This also includes e.g. the appearances table which apparently wasn't already included in the schema. --- db/schema.rb | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index 8f44ccebac86..3ac6203632d1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -259,8 +259,8 @@ t.string "architecture" end - add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin - add_index "ci_runners", ["token"], name: "index_ci_runners_on_token_trigram", using: :gin + add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} + add_index "ci_runners", ["token"], name: "index_ci_runners_on_token_trigram", using: :gin, opclasses: {"token"=>"gin_trgm_ops"} create_table "ci_services", force: :cascade do |t| t.string "type" @@ -421,13 +421,13 @@ add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree add_index "issues", ["created_at", "id"], name: "index_issues_on_created_at_and_id", using: :btree add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree - add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin + add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree add_index "issues", ["state"], name: "index_issues_on_state", using: :btree add_index "issues", ["title"], name: "index_issues_on_title", using: :btree - add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin + add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} create_table "keys", force: :cascade do |t| t.integer "user_id" @@ -549,14 +549,14 @@ add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree add_index "merge_requests", ["created_at", "id"], name: "index_merge_requests_on_created_at_and_id", using: :btree add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree - add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin + add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree add_index "merge_requests", ["source_project_id"], name: "index_merge_requests_on_source_project_id", using: :btree add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree - add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin + add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} create_table "milestones", force: :cascade do |t| t.string "title", null: false @@ -570,12 +570,12 @@ end add_index "milestones", ["created_at", "id"], name: "index_milestones_on_created_at_and_id", using: :btree - add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin + add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree - add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin + add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} create_table "namespaces", force: :cascade do |t| t.string "name", null: false @@ -590,10 +590,10 @@ add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree - add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin + add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree - add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin + add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree create_table "notes", force: :cascade do |t| @@ -619,7 +619,7 @@ add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree add_index "notes", ["is_award"], name: "index_notes_on_is_award", using: :btree add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree - add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin + add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"} add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree add_index "notes", ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree @@ -718,12 +718,12 @@ add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree - add_index "projects", ["description"], name: "index_projects_on_description_trigram", using: :gin + add_index "projects", ["description"], name: "index_projects_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree - add_index "projects", ["name"], name: "index_projects_on_name_trigram", using: :gin + add_index "projects", ["name"], name: "index_projects_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree add_index "projects", ["path"], name: "index_projects_on_path", using: :btree - add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin + add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree @@ -801,9 +801,9 @@ add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree add_index "snippets", ["created_at", "id"], name: "index_snippets_on_created_at_and_id", using: :btree add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree - add_index "snippets", ["file_name"], name: "index_snippets_on_file_name_trigram", using: :gin + add_index "snippets", ["file_name"], name: "index_snippets_on_file_name_trigram", using: :gin, opclasses: {"file_name"=>"gin_trgm_ops"} add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree - add_index "snippets", ["title"], name: "index_snippets_on_title_trigram", using: :gin + add_index "snippets", ["title"], name: "index_snippets_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} add_index "snippets", ["updated_at"], name: "index_snippets_on_updated_at", using: :btree add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree @@ -937,12 +937,12 @@ add_index "users", ["created_at", "id"], name: "index_users_on_created_at_and_id", using: :btree add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree - add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin + add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"} add_index "users", ["name"], name: "index_users_on_name", using: :btree - add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin + add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree add_index "users", ["username"], name: "index_users_on_username", using: :btree - add_index "users", ["username"], name: "index_users_on_username_trigram", using: :gin + add_index "users", ["username"], name: "index_users_on_username_trigram", using: :gin, opclasses: {"username"=>"gin_trgm_ops"} create_table "users_star_projects", force: :cascade do |t| t.integer "project_id", null: false -- GitLab From 53c892a4238c95008597b579c411f834501cf79a Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Thu, 3 Mar 2016 12:55:44 +0100 Subject: [PATCH 194/223] Patch MySQL to ignore PostgreSQL schema options This ensures that options such as `using: :gin` and PostgreSQL operator classes are ignored when loading a schema into a MySQL database. --- .../mysql_ignore_postgresql_options.rb | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 config/initializers/mysql_ignore_postgresql_options.rb diff --git a/config/initializers/mysql_ignore_postgresql_options.rb b/config/initializers/mysql_ignore_postgresql_options.rb new file mode 100644 index 000000000000..835f3ec55744 --- /dev/null +++ b/config/initializers/mysql_ignore_postgresql_options.rb @@ -0,0 +1,49 @@ +# This patches ActiveRecord so indexes created using the MySQL adapter ignore +# any PostgreSQL specific options (e.g. `using: :gin`). +# +# These patches do the following for MySQL: +# +# 1. Indexes created using the :opclasses option are ignored (as they serve no +# purpose on MySQL). +# 2. When creating an index with `using: :gin` the `using` option is discarded +# as :gin is not a valid value for MySQL. +# 3. The `:opclasses` option is stripped from add_index_options in case it's +# used anywhere other than in the add_index methods. + +if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter) + module ActiveRecord + module ConnectionAdapters + class Mysql2Adapter < AbstractMysqlAdapter + alias_method :__gitlab_add_index, :add_index + alias_method :__gitlab_add_index_sql, :add_index_sql + alias_method :__gitlab_add_index_options, :add_index_options + + def add_index(table_name, column_name, options = {}) + unless options[:opclasses] + __gitlab_add_index(table_name, column_name, options) + end + end + + def add_index_sql(table_name, column_name, options = {}) + unless options[:opclasses] + __gitlab_add_index_sql(table_name, column_name, options) + end + end + + def add_index_options(table_name, column_name, options = {}) + if options[:using] and options[:using] == :gin + options = options.dup + options.delete(:using) + end + + if options[:opclasses] + options = options.dup + options.delete(:opclasses) + end + + __gitlab_add_index_options(table_name, column_name, options) + end + end + end + end +end -- GitLab From e479b49edd9feebcba636308b52a4b2a3f156410 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Thu, 3 Mar 2016 13:09:28 +0100 Subject: [PATCH 195/223] Disable Rubocop for PostgreSQL patches This code is mostly a copy-paste from existing pull requests so there's no point in running Rubocop on it. --- config/initializers/postgresql_opclasses_support.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/initializers/postgresql_opclasses_support.rb b/config/initializers/postgresql_opclasses_support.rb index ac803778ab00..820cc89ef574 100644 --- a/config/initializers/postgresql_opclasses_support.rb +++ b/config/initializers/postgresql_opclasses_support.rb @@ -1,3 +1,5 @@ +# rubocop:disable all + # These changes add support for PostgreSQL operator classes when creating # indexes and dumping/loading schemas. Taken from Rails pull request # https://github.com/rails/rails/pull/19090. -- GitLab From c7cac9c9eb53bc775c03804e84d943a35f70690c Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Thu, 3 Mar 2016 13:12:00 +0100 Subject: [PATCH 196/223] Fixed ProjectSearchResults spec to use a Project This spec was still passing an ID to the #initialize method instead of a Project instance. --- spec/lib/gitlab/project_search_results_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index efc2e5f4ef1f..09adbc07dcbb 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -5,7 +5,7 @@ let(:query) { 'hello world' } describe 'initialize with empty ref' do - let(:results) { Gitlab::ProjectSearchResults.new(project.id, query, '') } + let(:results) { Gitlab::ProjectSearchResults.new(project, query, '') } it { expect(results.project).to eq(project) } it { expect(results.repository_ref).to be_nil } @@ -14,7 +14,7 @@ describe 'initialize with ref' do let(:ref) { 'refs/heads/test' } - let(:results) { Gitlab::ProjectSearchResults.new(project.id, query, ref) } + let(:results) { Gitlab::ProjectSearchResults.new(project, query, ref) } it { expect(results.project).to eq(project) } it { expect(results.repository_ref).to eq(ref) } -- GitLab From 33d86b55c9b70d8e3dc2534f44d93b900e96b390 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Thu, 3 Mar 2016 15:10:14 +0100 Subject: [PATCH 197/223] Added ProjectSearchResults#project_ids_relation This ensures some other methods such as the "issues" method still work. --- lib/gitlab/project_search_results.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index f36600c57569..0607a8b95927 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -83,5 +83,9 @@ def commits project.repository.find_commits_by_message(query).compact end end + + def project_ids_relation + project + end end end -- GitLab From a12ccdace6c710f3fbfa380cda7d8a9ea4c03743 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Thu, 3 Mar 2016 18:35:17 +0100 Subject: [PATCH 198/223] Removed order from sub-query projects for search There's no need to order queries used as sub-queries and doing so can add potential overhead. --- lib/gitlab/search_results.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 036fa6f937c3..f13528a2eea5 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -94,7 +94,7 @@ def per_page end def project_ids_relation - limit_projects.select(:id) + limit_projects.select(:id).reorder(nil) end end end -- GitLab From c1c5fa14ef466c682d419b0d2ff5f7663fccc07d Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Fri, 4 Mar 2016 11:39:00 +0100 Subject: [PATCH 199/223] Clean up ProjectsFinder for getting user projects We don't need the extra layer of nesting of UNION queries here (as User#authorized_projects already returns a UNION'd query). --- app/finders/projects_finder.rb | 5 ++++- app/models/user.rb | 9 ++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 3b4e0362e04b..2b8fba77bb15 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -52,7 +52,10 @@ def group_projects(current_user, group) def all_projects(current_user) if current_user - [current_user.authorized_projects, public_and_internal_projects] + [ + *current_user.project_relations, + public_and_internal_projects + ] else [Project.public_only] end diff --git a/app/models/user.rb b/app/models/user.rb index fc4cf92be60b..725f748faf06 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -442,6 +442,11 @@ def authorized_projects Project.where("projects.id IN (#{projects_union.to_sql})") end + # Returns all the project relations + def project_relations + [personal_projects, groups_projects, projects] + end + def owned_projects @owned_projects ||= Project.where('namespace_id IN (?) OR namespace_id = ?', @@ -830,9 +835,7 @@ def ci_authorized_runners private def projects_union - Gitlab::SQL::Union.new([personal_projects.select(:id), - groups_projects.select(:id), - projects.select(:id)]) + Gitlab::SQL::Union.new(project_relations.map { |r| r.select(:id) }) end def ci_projects_union -- GitLab From 14a2124f558449663d84a56b6eb3c874b74009d5 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Fri, 4 Mar 2016 11:54:37 +0100 Subject: [PATCH 200/223] Fixed a few spec typos --- spec/models/concerns/issuable_spec.rb | 6 +++--- spec/models/user_spec.rb | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index f0e4de8539ec..aff384c29491 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -32,7 +32,7 @@ describe ".search" do let!(:searchable_issue) { create(:issue, title: "Searchable issue") } - it 'returns notches with a matching title' do + it 'returns notes with a matching title' do expect(described_class.search(searchable_issue.title)). to eq([searchable_issue]) end @@ -52,7 +52,7 @@ create(:issue, title: "Searchable issue", description: 'kittens') end - it 'returns notches with a matching title' do + it 'returns notes with a matching title' do expect(described_class.full_search(searchable_issue.title)). to eq([searchable_issue]) end @@ -66,7 +66,7 @@ to eq([searchable_issue]) end - it 'returns notches with a matching description' do + it 'returns notes with a matching description' do expect(described_class.full_search(searchable_issue.description)). to eq([searchable_issue]) end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 90207f5153a9..909b67965915 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -474,7 +474,7 @@ expect(described_class.search(user.name[0..2])).to eq([user]) end - it 'returns users with a matching name regarding of the casing' do + it 'returns users with a matching name regardless of the casing' do expect(described_class.search(user.name.upcase)).to eq([user]) end @@ -486,7 +486,7 @@ expect(described_class.search(user.email[0..2])).to eq([user]) end - it 'returns users with a matching Email regarding of the casing' do + it 'returns users with a matching Email regardless of the casing' do expect(described_class.search(user.email.upcase)).to eq([user]) end @@ -498,7 +498,7 @@ expect(described_class.search(user.username[0..2])).to eq([user]) end - it 'returns users with a matching username regarding of the casing' do + it 'returns users with a matching username regardless of the casing' do expect(described_class.search(user.username.upcase)).to eq([user]) end end -- GitLab From 0836f5ca071056af8df0a659eece2d678213f917 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Fri, 4 Mar 2016 12:01:21 +0100 Subject: [PATCH 201/223] Removed arel_table receiver from search methods We can just use "arel_table" in these cases instead of "SomeClass.arel_table". --- app/models/ci/runner.rb | 2 +- app/models/group.rb | 2 +- app/models/note.rb | 2 +- app/models/project.rb | 2 +- app/models/snippet.rb | 2 +- app/models/user.rb | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index ce2750b071c2..90349a075949 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -59,7 +59,7 @@ class Runner < ActiveRecord::Base # # Returns an ActiveRecord::Relation. def self.search(query) - t = Ci::Runner.arel_table + t = arel_table pattern = "%#{query}%" where(t[:token].matches(pattern).or(t[:description].matches(pattern))) diff --git a/app/models/group.rb b/app/models/group.rb index bfeddf8b4d20..afbc29220135 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -41,7 +41,7 @@ class << self # # Returns an ActiveRecord::Relation. def search(query) - table = Group.arel_table + table = Namespace.arel_table pattern = "%#{query}%" where(table[:name].matches(pattern).or(table[:path].matches(pattern))) diff --git a/app/models/note.rb b/app/models/note.rb index 76e86fdbcaa5..8b0610ff77e2 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -113,7 +113,7 @@ def build_discussion_id(type, id, line_code) # # Returns an ActiveRecord::Relation. def search(query) - table = Note.arel_table + table = arel_table pattern = "%#{query}%" where(table[:note].matches(pattern)) diff --git a/app/models/project.rb b/app/models/project.rb index e55d3e7f7873..1f18ad78164f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -274,7 +274,7 @@ def active # # query - The search query as a String. def search(query) - ptable = Project.arel_table + ptable = arel_table ntable = Namespace.arel_table pattern = "%#{query}%" diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 35d05af38bfe..b9e835a44862 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -121,7 +121,7 @@ class << self # # Returns an ActiveRecord::Relation. def search(query) - t = Snippet.arel_table + t = arel_table pattern = "%#{query}%" where(t[:title].matches(pattern).or(t[:file_name].matches(pattern))) diff --git a/app/models/user.rb b/app/models/user.rb index 725f748faf06..101303e1f1f7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -294,7 +294,7 @@ def filter(filter_name) # # Returns an ActiveRecord::Relation. def search(query) - table = User.arel_table + table = arel_table pattern = "%#{query}%" where( -- GitLab From 0d99868469b502f2f37e5cc3393dea17c5a3a9a1 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Fri, 4 Mar 2016 12:06:25 +0100 Subject: [PATCH 202/223] Make Namespace.search case-insensitive This ensures searching namespaces works exactly the same as searching for any other resource. --- app/models/namespace.rb | 12 +++++++++++- spec/models/namespace_spec.rb | 29 ++++++++++++++++++++++++----- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/app/models/namespace.rb b/app/models/namespace.rb index bdb33f374954..55842df1e2d3 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -52,8 +52,18 @@ def find_by_path_or_name(path) find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase) end + # Searches for namespaces matching the given query. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String + # + # Returns an ActiveRecord::Relation def search(query) - where("name LIKE :query OR path LIKE :query", query: "%#{query}%") + t = arel_table + pattern = "%#{query}%" + + where(t[:name].matches(pattern).or(t[:path].matches(pattern))) end def clean_path(path) diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index e0b3290e4166..bc31b49742fb 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -41,13 +41,32 @@ it { expect(namespace.human_name).to eq(namespace.owner_name) } end - describe :search do - before do - @namespace = create :namespace + describe '#search' do + let(:namespace) { create(:namespace) } + + it 'returns namespaces with a matching name' do + expect(described_class.search(namespace.name)).to eq([namespace]) + end + + it 'returns namespaces with a partially matching name' do + expect(described_class.search(namespace.name[0..2])).to eq([namespace]) + end + + it 'returns namespaces with a matching name regardless of the casing' do + expect(described_class.search(namespace.name.upcase)).to eq([namespace]) + end + + it 'returns namespaces with a matching path' do + expect(described_class.search(namespace.path)).to eq([namespace]) end - it { expect(Namespace.search(@namespace.path)).to eq([@namespace]) } - it { expect(Namespace.search('unknown')).to eq([]) } + it 'returns namespaces with a partially matching path' do + expect(described_class.search(namespace.path[0..2])).to eq([namespace]) + end + + it 'returns namespaces with a matching path regardless of the casing' do + expect(described_class.search(namespace.path.upcase)).to eq([namespace]) + end end describe :move_dir do -- GitLab From e94f8e6af20bc7a48c60c40336819e0bc5929b87 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Fri, 4 Mar 2016 12:15:30 +0100 Subject: [PATCH 203/223] Corrected spec title for Namespace.search --- spec/models/namespace_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index bc31b49742fb..3c3a580942a9 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -41,7 +41,7 @@ it { expect(namespace.human_name).to eq(namespace.owner_name) } end - describe '#search' do + describe '.search' do let(:namespace) { create(:namespace) } it 'returns namespaces with a matching name' do -- GitLab From 75dc2f798006af5a63c1b5c1a7503190db3f5da1 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Fri, 4 Mar 2016 12:21:26 +0100 Subject: [PATCH 204/223] Added pg_trgm to the PostgreSQL requirements --- doc/install/requirements.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 8df142c531b1..d59b7f0e84dc 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -97,6 +97,17 @@ To change the Unicorn workers when you have the Omnibus package please see [the If you want to run the database separately expect a size of about 1 MB per user. +### PostgreSQL Requirements + +Users using PostgreSQL must ensure the `pg_trgm` extension is loaded into every +GitLab database. This extension can be enabled (using a PostgreSQL super user) +by running the following query for every database: + + CREATE EXTENSION pg_trgm; + +On some systems you may need to install an additional package (e.g. +`postgresql-contrib`) for this extension to become available. + ## Redis and Sidekiq Redis stores all user sessions and the background task queue. -- GitLab From 9f4b5e49d2f4ee6caa3c31ba8dfacf402415fb8a Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Fri, 4 Mar 2016 12:23:51 +0100 Subject: [PATCH 205/223] Added changelog entry for search performance --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 4bd6c1dbcfaa..128a20b48a01 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -17,6 +17,7 @@ v 8.6.0 (unreleased) - Return empty array instead of 404 when commit has no statuses in commit status API - Decrease the font size and the padding of the `.anchor` icons used in the README (Roberto Dip) - Rewrite logo to simplify SVG code (Sean Lang) + - Refactor and greatly improve search performance - Add support for cross-project label references - Update documentation to reflect Guest role not being enforced on internal projects - Allow search for logged out users -- GitLab From 2ddbf7c4274438e78f2ee70c5bd7f4709fb4da3d Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Mon, 7 Mar 2016 12:56:46 +0100 Subject: [PATCH 206/223] Use a UNION in MergeRequest.in_projects The OR condition for source_project_id/target_project_id leads to a query plan that performs rather poorly on PostgreSQL due to the use of sub-queries. Because Rails offers no easy alternative for this particular problem we're forced to using a UNION for both conditions. The resulting query performs much faster than just using an OR. --- app/models/merge_request.rb | 19 ++++++++++++++++++- spec/models/merge_request_spec.rb | 6 ++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index c1e18bb3cc51..188325045e2f 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -135,7 +135,6 @@ class MergeRequest < ActiveRecord::Base scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } - scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) } scope :of_projects, ->(ids) { where(target_project_id: ids) } scope :merged, -> { with_state(:merged) } scope :closed_and_merged, -> { with_states(:closed, :merged) } @@ -161,6 +160,24 @@ def self.link_reference_pattern super("merge_requests", /(?<merge_request>\d+)/) end + # Returns all the merge requests from an ActiveRecord:Relation. + # + # This method uses a UNION as it usually operates on the result of + # ProjectsFinder#execute. PostgreSQL in particular doesn't always like queries + # using multiple sub-queries especially when combined with an OR statement. + # UNIONs on the other hand perform much better in these cases. + # + # relation - An ActiveRecord::Relation that returns a list of Projects. + # + # Returns an ActiveRecord::Relation. + def self.in_projects(relation) + source = where(source_project_id: relation).select(:id) + target = where(target_project_id: relation).select(:id) + union = Gitlab::SQL::Union.new([source, target]) + + where("merge_requests.id IN (#{union.to_sql})") + end + def to_reference(from_project = nil) reference = "#{self.class.reference_prefix}#{iid}" diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 59c40922abb6..8bf68013fd26 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -80,6 +80,12 @@ it { is_expected.to respond_to(:merge_when_build_succeeds) } end + describe '.in_projects' do + it 'returns the merge requests for a set of projects' do + expect(described_class.in_projects(Project.all)).to eq([subject]) + end + end + describe '#to_reference' do it 'returns a String reference to the object' do expect(subject.to_reference).to eq "!#{subject.iid}" -- GitLab From 51fd00dff4d7cd69f9124af35919153a1a59b8cd Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Thu, 10 Mar 2016 14:49:15 +0100 Subject: [PATCH 207/223] More detailed trigram migration error message This explains the user what they need to run and where to go in case they want to learn more about "CREATE EXTENSION". --- .../20160226114608_add_trigram_indexes_for_searching.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb index fca5ac01a085..003169c13c6e 100644 --- a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb +++ b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb @@ -5,7 +5,10 @@ def up return unless Gitlab::Database.postgresql? unless trigrams_enabled? - raise 'You must enable the pg_trgm extension as a PostgreSQL super user' + raise 'You must enable the pg_trgm extension. You can do so by running ' \ + '"CREATE EXTENSION pg_trgm;" as a PostgreSQL super user, this must be ' \ + 'done for every GitLab database. For more information see ' \ + 'http://www.postgresql.org/docs/current/static/sql-createextension.html' end # trigram indexes are case-insensitive so we can just index the column -- GitLab From fd8934d8094b887d8b8b32e646952850ab92a54c Mon Sep 17 00:00:00 2001 From: Dennis van de Hoef <dennisvdhoef@gmail.com> Date: Fri, 11 Mar 2016 22:34:51 +0100 Subject: [PATCH 208/223] Fix responsive bug top navigation --- app/assets/stylesheets/framework/nav.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 7de874c8bcd8..b2fbc95e0436 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -63,7 +63,7 @@ border-bottom: none; /* Small devices (phones, tablets, 768px and lower) */ - @media (max-width: $screen-sm-min) { + @media (max-width: $screen-sm-max) { width: 100%; } } -- GitLab From e9a5d6d16a953c19ee173aa876e87b7764eccd49 Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Fri, 11 Mar 2016 17:46:50 -0500 Subject: [PATCH 209/223] Disallow blank (non-null) values for a Note's `line_code` attribute It's unclear how these blank values got added, but GitLab.com had a few: ``` irb(main):002:0> Note.where("line_code IS NOT NULL AND line_code = ''").count => 439 ``` We've added a migration to convert any existing records to use a NULL value when blank, and updated Note to set blank values to nil before validation. --- app/models/note.rb | 7 ++++++- .../20160307221555_disallow_blank_line_code_on_note.rb | 9 +++++++++ spec/models/note_spec.rb | 8 ++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20160307221555_disallow_blank_line_code_on_note.rb diff --git a/app/models/note.rb b/app/models/note.rb index 8b0610ff77e2..2e084b5c80c7 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -44,6 +44,7 @@ class Note < ActiveRecord::Base delegate :name, :email, to: :author, prefix: true before_validation :set_award! + before_validation :clear_blank_line_code! validates :note, :project, presence: true validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award } @@ -63,7 +64,7 @@ class Note < ActiveRecord::Base scope :nonawards, ->{ where(is_award: false) } scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } scope :inline, ->{ where("line_code IS NOT NULL") } - scope :not_inline, ->{ where(line_code: [nil, '']) } + scope :not_inline, ->{ where(line_code: nil) } scope :system, ->{ where(system: true) } scope :user, ->{ where(system: false) } scope :common, ->{ where(noteable_type: ["", nil]) } @@ -375,6 +376,10 @@ def set_award! private + def clear_blank_line_code! + self.line_code = nil if self.line_code.blank? + end + def awards_supported? (for_issue? || for_merge_request?) && !for_diff_line? end diff --git a/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb b/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb new file mode 100644 index 000000000000..49e787d9a9a5 --- /dev/null +++ b/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb @@ -0,0 +1,9 @@ +class DisallowBlankLineCodeOnNote < ActiveRecord::Migration + def up + execute("UPDATE notes SET line_code = NULL WHERE line_code = ''") + end + + def down + # noop + end +end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index cd620ea5440a..b854de1d3d52 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -226,4 +226,12 @@ expect(note.is_award?).to be_falsy end end + + describe 'clear_blank_line_code!' do + it 'clears a blank line code before validation' do + note = build(:note, line_code: ' ') + + expect { note.valid? }.to change(note, :line_code).to(nil) + end + end end -- GitLab From 54bd20977deeabc60dcd94ffc71f2fed68c3c608 Mon Sep 17 00:00:00 2001 From: Stan Hu <stanhu@gmail.com> Date: Sat, 12 Mar 2016 01:09:37 -0800 Subject: [PATCH 210/223] Support Golang subpackage fetching Closes #13805 --- CHANGELOG | 1 + config/initializers/go_get.rb | 1 + lib/gitlab/middleware/go.rb | 51 +++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 config/initializers/go_get.rb create mode 100644 lib/gitlab/middleware/go.rb diff --git a/CHANGELOG b/CHANGELOG index 128a20b48a01..10764da25464 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.6.0 (unreleased) + - Support Golang subpackage fetching (Stan Hu) - Contributions to forked projects are included in calendar - Improve the formatting for the user page bio (Connor Shea) - Removed the default password from the initial admin account created during diff --git a/config/initializers/go_get.rb b/config/initializers/go_get.rb new file mode 100644 index 000000000000..7e7896b4900b --- /dev/null +++ b/config/initializers/go_get.rb @@ -0,0 +1 @@ +Rails.application.config.middleware.use(Gitlab::Middleware::Go) diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb new file mode 100644 index 000000000000..bc9bee7cbb60 --- /dev/null +++ b/lib/gitlab/middleware/go.rb @@ -0,0 +1,51 @@ +# A dumb middleware that returns a Go HTML document if the go-get=1 query string +# is used irrespective if the namespace/project exists +module Gitlab + module Middleware + class Go + def initialize(app) + @app = app + end + + def call(env) + request = Rack::Request.new(env) + + if go_request?(request) + render_go_doc(request) + else + @app.call(env) + end + end + + private + + def render_go_doc(request) + body = go_body(request) + response = Rack::Response.new(body, 200, { 'Content-Type' => 'text/html' }) + response.finish + end + + def go_request?(request) + return request["go-get"].to_i == 1 + end + + def go_body(request) + base_url = Settings.gitlab['url'] + # Go subpackages may be in the form of namespace/project/path1/path2/../pathN + # We can just ignore the paths and leave the namespace/project + path_info = request.env["PATH_INFO"] + path_info.sub!(/^\//, '') + project_path = path_info.split('/').first(2).join('/') + request_url = URI.join(base_url, project_path) + domain_path = strip_url(request_url.to_s) + + "<!DOCTYPE html><html><head><meta content='#{domain_path} git #{request_url}.git' name='go-import'></head></html>\n"; + end + + def strip_url(url) + url.gsub('http://', ''). + gsub('https://', '') + end + end + end +end -- GitLab From 2716901431c209e3fb2c190a67118305d508853d Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Sat, 12 Mar 2016 15:44:48 +0100 Subject: [PATCH 211/223] Removed User#project_relations GitLab EE adds an extra relation that selects a "project_id" column instead of an "id" column, making it very hard for this method to be re-used in EE. Since using User#authorized_groups in ProjectsFinder#all_groups apparently has no performance impact we can just use it and keep everything compatible with EE. --- app/finders/projects_finder.rb | 2 +- app/models/user.rb | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 2b8fba77bb15..0e5a8f5ee0fe 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -53,7 +53,7 @@ def group_projects(current_user, group) def all_projects(current_user) if current_user [ - *current_user.project_relations, + current_user.authorized_projects, public_and_internal_projects ] else diff --git a/app/models/user.rb b/app/models/user.rb index 101303e1f1f7..043bc825ade7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -442,11 +442,6 @@ def authorized_projects Project.where("projects.id IN (#{projects_union.to_sql})") end - # Returns all the project relations - def project_relations - [personal_projects, groups_projects, projects] - end - def owned_projects @owned_projects ||= Project.where('namespace_id IN (?) OR namespace_id = ?', @@ -835,7 +830,9 @@ def ci_authorized_runners private def projects_union - Gitlab::SQL::Union.new(project_relations.map { |r| r.select(:id) }) + Gitlab::SQL::Union.new([personal_projects.select(:id), + groups_projects.select(:id), + projects.select(:id)]) end def ci_projects_union -- GitLab From 451b5947c22b438131a1ff68aa2db98115530cde Mon Sep 17 00:00:00 2001 From: Stan Hu <stanhu@gmail.com> Date: Sat, 12 Mar 2016 07:49:16 -0800 Subject: [PATCH 212/223] Remove existing go_import.html.haml implementation --- app/controllers/projects_controller.rb | 11 ----------- app/views/projects/go_import.html.haml | 5 ----- 2 files changed, 16 deletions(-) delete mode 100644 app/views/projects/go_import.html.haml diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index aea08ecce3e3..c70add86a20f 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,7 +1,6 @@ class ProjectsController < ApplicationController include ExtractsPath - prepend_before_action :render_go_import, only: [:show] skip_before_action :authenticate_user!, only: [:show, :activity] before_action :project, except: [:new, :create] before_action :repository, except: [:new, :create] @@ -242,16 +241,6 @@ def autocomplete_emojis end end - def render_go_import - return unless params["go-get"] == "1" - - @namespace = params[:namespace_id] - @id = params[:project_id] || params[:id] - @id = @id.gsub(/\.git\Z/, "") - - render "go_import", layout: false - end - def repo_exists? project.repository_exists? && !project.empty_repo? end diff --git a/app/views/projects/go_import.html.haml b/app/views/projects/go_import.html.haml deleted file mode 100644 index 87ac75a350fd..000000000000 --- a/app/views/projects/go_import.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -!!! 5 -%html - %head - - web_url = [Gitlab.config.gitlab.url, @namespace, @id].join('/') - %meta{name: "go-import", content: "#{web_url.split('://')[1]} git #{web_url}.git"} -- GitLab From 024835459e14e76952669cca51e999102fb2094e Mon Sep 17 00:00:00 2001 From: Stan Hu <stanhu@gmail.com> Date: Sat, 12 Mar 2016 07:50:02 -0800 Subject: [PATCH 213/223] Simplify code --- lib/gitlab/middleware/go.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb index bc9bee7cbb60..44d378642a14 100644 --- a/lib/gitlab/middleware/go.rb +++ b/lib/gitlab/middleware/go.rb @@ -26,11 +26,11 @@ def render_go_doc(request) end def go_request?(request) - return request["go-get"].to_i == 1 + request["go-get"].to_i == 1 end def go_body(request) - base_url = Settings.gitlab['url'] + base_url = Gitlab.config.gitlab.url # Go subpackages may be in the form of namespace/project/path1/path2/../pathN # We can just ignore the paths and leave the namespace/project path_info = request.env["PATH_INFO"] @@ -43,8 +43,7 @@ def go_body(request) end def strip_url(url) - url.gsub('http://', ''). - gsub('https://', '') + url.gsub(/\Ahttps?:\/\//, '') end end end -- GitLab From 488748deb9b5f9ca0376789d6b4ed038eee077b2 Mon Sep 17 00:00:00 2001 From: Stan Hu <stanhu@gmail.com> Date: Sat, 12 Mar 2016 08:10:38 -0800 Subject: [PATCH 214/223] Add spec for go-import middleware --- spec/lib/gitlab/middleware/go_spec.rb | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 spec/lib/gitlab/middleware/go_spec.rb diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb new file mode 100644 index 000000000000..117a15264da2 --- /dev/null +++ b/spec/lib/gitlab/middleware/go_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Gitlab::Middleware::Go, lib: true do + let(:app) { double(:app) } + let(:middleware) { described_class.new(app) } + + describe '#call' do + describe 'when go-get=0' do + it 'skips go-import generation' do + env = { 'rack.input' => '', + 'QUERY_STRING' => 'go-get=0' } + expect(app).to receive(:call).with(env).and_return('no-go') + middleware.call(env) + end + end + + describe 'when go-get=1' do + it 'returns a document' do + env = { 'rack.input' => '', + 'QUERY_STRING' => 'go-get=1', + 'PATH_INFO' => '/group/project/path' } + resp = middleware.call(env) + expect(resp[0]).to eq(200) + expect(resp[1]['Content-Type']).to eq('text/html') + expected_body = "<!DOCTYPE html><html><head><meta content='localhost/group/project git http://localhost/group/project.git' name='go-import'></head></html>\n" + expect(resp[2].body).to eq([expected_body]) + end + end + end +end -- GitLab From 2bac2691db8a232d6b7c47f33ad9f10906445197 Mon Sep 17 00:00:00 2001 From: Stan Hu <stanhu@gmail.com> Date: Sat, 12 Mar 2016 08:10:54 -0800 Subject: [PATCH 215/223] Ensure PATH_INFO exists for go-get --- lib/gitlab/middleware/go.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb index 44d378642a14..50b0dd323803 100644 --- a/lib/gitlab/middleware/go.rb +++ b/lib/gitlab/middleware/go.rb @@ -26,7 +26,7 @@ def render_go_doc(request) end def go_request?(request) - request["go-get"].to_i == 1 + request["go-get"].to_i == 1 && request.env["PATH_INFO"].present? end def go_body(request) -- GitLab From e364ce8ce3d331858459196005bc3eb26e27c62a Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@gitlab.com> Date: Sat, 12 Mar 2016 17:42:51 +0000 Subject: [PATCH 216/223] Revert "Merge branch 'support-go-subpackages' into 'master' " This reverts merge request !3191 --- CHANGELOG | 1 - app/controllers/projects_controller.rb | 11 ++++++ app/views/projects/go_import.html.haml | 5 +++ config/initializers/go_get.rb | 1 - lib/gitlab/middleware/go.rb | 50 -------------------------- spec/lib/gitlab/middleware/go_spec.rb | 30 ---------------- 6 files changed, 16 insertions(+), 82 deletions(-) create mode 100644 app/views/projects/go_import.html.haml delete mode 100644 config/initializers/go_get.rb delete mode 100644 lib/gitlab/middleware/go.rb delete mode 100644 spec/lib/gitlab/middleware/go_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 10764da25464..128a20b48a01 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,6 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.6.0 (unreleased) - - Support Golang subpackage fetching (Stan Hu) - Contributions to forked projects are included in calendar - Improve the formatting for the user page bio (Connor Shea) - Removed the default password from the initial admin account created during diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index c70add86a20f..aea08ecce3e3 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,6 +1,7 @@ class ProjectsController < ApplicationController include ExtractsPath + prepend_before_action :render_go_import, only: [:show] skip_before_action :authenticate_user!, only: [:show, :activity] before_action :project, except: [:new, :create] before_action :repository, except: [:new, :create] @@ -241,6 +242,16 @@ def autocomplete_emojis end end + def render_go_import + return unless params["go-get"] == "1" + + @namespace = params[:namespace_id] + @id = params[:project_id] || params[:id] + @id = @id.gsub(/\.git\Z/, "") + + render "go_import", layout: false + end + def repo_exists? project.repository_exists? && !project.empty_repo? end diff --git a/app/views/projects/go_import.html.haml b/app/views/projects/go_import.html.haml new file mode 100644 index 000000000000..87ac75a350fd --- /dev/null +++ b/app/views/projects/go_import.html.haml @@ -0,0 +1,5 @@ +!!! 5 +%html + %head + - web_url = [Gitlab.config.gitlab.url, @namespace, @id].join('/') + %meta{name: "go-import", content: "#{web_url.split('://')[1]} git #{web_url}.git"} diff --git a/config/initializers/go_get.rb b/config/initializers/go_get.rb deleted file mode 100644 index 7e7896b4900b..000000000000 --- a/config/initializers/go_get.rb +++ /dev/null @@ -1 +0,0 @@ -Rails.application.config.middleware.use(Gitlab::Middleware::Go) diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb deleted file mode 100644 index 50b0dd323803..000000000000 --- a/lib/gitlab/middleware/go.rb +++ /dev/null @@ -1,50 +0,0 @@ -# A dumb middleware that returns a Go HTML document if the go-get=1 query string -# is used irrespective if the namespace/project exists -module Gitlab - module Middleware - class Go - def initialize(app) - @app = app - end - - def call(env) - request = Rack::Request.new(env) - - if go_request?(request) - render_go_doc(request) - else - @app.call(env) - end - end - - private - - def render_go_doc(request) - body = go_body(request) - response = Rack::Response.new(body, 200, { 'Content-Type' => 'text/html' }) - response.finish - end - - def go_request?(request) - request["go-get"].to_i == 1 && request.env["PATH_INFO"].present? - end - - def go_body(request) - base_url = Gitlab.config.gitlab.url - # Go subpackages may be in the form of namespace/project/path1/path2/../pathN - # We can just ignore the paths and leave the namespace/project - path_info = request.env["PATH_INFO"] - path_info.sub!(/^\//, '') - project_path = path_info.split('/').first(2).join('/') - request_url = URI.join(base_url, project_path) - domain_path = strip_url(request_url.to_s) - - "<!DOCTYPE html><html><head><meta content='#{domain_path} git #{request_url}.git' name='go-import'></head></html>\n"; - end - - def strip_url(url) - url.gsub(/\Ahttps?:\/\//, '') - end - end - end -end diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb deleted file mode 100644 index 117a15264da2..000000000000 --- a/spec/lib/gitlab/middleware/go_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Middleware::Go, lib: true do - let(:app) { double(:app) } - let(:middleware) { described_class.new(app) } - - describe '#call' do - describe 'when go-get=0' do - it 'skips go-import generation' do - env = { 'rack.input' => '', - 'QUERY_STRING' => 'go-get=0' } - expect(app).to receive(:call).with(env).and_return('no-go') - middleware.call(env) - end - end - - describe 'when go-get=1' do - it 'returns a document' do - env = { 'rack.input' => '', - 'QUERY_STRING' => 'go-get=1', - 'PATH_INFO' => '/group/project/path' } - resp = middleware.call(env) - expect(resp[0]).to eq(200) - expect(resp[1]['Content-Type']).to eq('text/html') - expected_body = "<!DOCTYPE html><html><head><meta content='localhost/group/project git http://localhost/group/project.git' name='go-import'></head></html>\n" - expect(resp[2].body).to eq([expected_body]) - end - end - end -end -- GitLab From d9748aaca460495536a7a5c3a1818eba667ebe44 Mon Sep 17 00:00:00 2001 From: Stan Hu <stanhu@gmail.com> Date: Sat, 12 Mar 2016 11:14:56 -0800 Subject: [PATCH 217/223] Remove unnecessary go-get test (superseded by middleware) --- spec/controllers/projects_controller_spec.rb | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 6eee4dfe229c..1893e946f5cd 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -9,19 +9,6 @@ describe "GET show" do - context "when requested by `go get`" do - render_views - - it "renders the go-import meta tag" do - get :show, "go-get" => "1", namespace_id: "bogus_namespace", id: "bogus_project" - - expect(response.body).to include("name='go-import'") - - content = "localhost/bogus_namespace/bogus_project git http://localhost/bogus_namespace/bogus_project.git" - expect(response.body).to include("content='#{content}'") - end - end - context "rendering default project view" do render_views -- GitLab From a232e5dc07ac4dafbec1a534cd1ba1c9403f5b95 Mon Sep 17 00:00:00 2001 From: Stan Hu <stanhu@gmail.com> Date: Sat, 12 Mar 2016 21:05:23 +0000 Subject: [PATCH 218/223] Revert "Revert "Merge branch 'support-go-subpackages' into 'master' "" This reverts commit 5a586f364c5d2b866d6a074eff7996f05585b7d5 --- CHANGELOG | 1 + app/controllers/projects_controller.rb | 11 ------ app/views/projects/go_import.html.haml | 5 --- config/initializers/go_get.rb | 1 + lib/gitlab/middleware/go.rb | 50 ++++++++++++++++++++++++++ spec/lib/gitlab/middleware/go_spec.rb | 30 ++++++++++++++++ 6 files changed, 82 insertions(+), 16 deletions(-) delete mode 100644 app/views/projects/go_import.html.haml create mode 100644 config/initializers/go_get.rb create mode 100644 lib/gitlab/middleware/go.rb create mode 100644 spec/lib/gitlab/middleware/go_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 128a20b48a01..10764da25464 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.6.0 (unreleased) + - Support Golang subpackage fetching (Stan Hu) - Contributions to forked projects are included in calendar - Improve the formatting for the user page bio (Connor Shea) - Removed the default password from the initial admin account created during diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index aea08ecce3e3..c70add86a20f 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,7 +1,6 @@ class ProjectsController < ApplicationController include ExtractsPath - prepend_before_action :render_go_import, only: [:show] skip_before_action :authenticate_user!, only: [:show, :activity] before_action :project, except: [:new, :create] before_action :repository, except: [:new, :create] @@ -242,16 +241,6 @@ def autocomplete_emojis end end - def render_go_import - return unless params["go-get"] == "1" - - @namespace = params[:namespace_id] - @id = params[:project_id] || params[:id] - @id = @id.gsub(/\.git\Z/, "") - - render "go_import", layout: false - end - def repo_exists? project.repository_exists? && !project.empty_repo? end diff --git a/app/views/projects/go_import.html.haml b/app/views/projects/go_import.html.haml deleted file mode 100644 index 87ac75a350fd..000000000000 --- a/app/views/projects/go_import.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -!!! 5 -%html - %head - - web_url = [Gitlab.config.gitlab.url, @namespace, @id].join('/') - %meta{name: "go-import", content: "#{web_url.split('://')[1]} git #{web_url}.git"} diff --git a/config/initializers/go_get.rb b/config/initializers/go_get.rb new file mode 100644 index 000000000000..7e7896b4900b --- /dev/null +++ b/config/initializers/go_get.rb @@ -0,0 +1 @@ +Rails.application.config.middleware.use(Gitlab::Middleware::Go) diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb new file mode 100644 index 000000000000..50b0dd323803 --- /dev/null +++ b/lib/gitlab/middleware/go.rb @@ -0,0 +1,50 @@ +# A dumb middleware that returns a Go HTML document if the go-get=1 query string +# is used irrespective if the namespace/project exists +module Gitlab + module Middleware + class Go + def initialize(app) + @app = app + end + + def call(env) + request = Rack::Request.new(env) + + if go_request?(request) + render_go_doc(request) + else + @app.call(env) + end + end + + private + + def render_go_doc(request) + body = go_body(request) + response = Rack::Response.new(body, 200, { 'Content-Type' => 'text/html' }) + response.finish + end + + def go_request?(request) + request["go-get"].to_i == 1 && request.env["PATH_INFO"].present? + end + + def go_body(request) + base_url = Gitlab.config.gitlab.url + # Go subpackages may be in the form of namespace/project/path1/path2/../pathN + # We can just ignore the paths and leave the namespace/project + path_info = request.env["PATH_INFO"] + path_info.sub!(/^\//, '') + project_path = path_info.split('/').first(2).join('/') + request_url = URI.join(base_url, project_path) + domain_path = strip_url(request_url.to_s) + + "<!DOCTYPE html><html><head><meta content='#{domain_path} git #{request_url}.git' name='go-import'></head></html>\n"; + end + + def strip_url(url) + url.gsub(/\Ahttps?:\/\//, '') + end + end + end +end diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb new file mode 100644 index 000000000000..117a15264da2 --- /dev/null +++ b/spec/lib/gitlab/middleware/go_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Gitlab::Middleware::Go, lib: true do + let(:app) { double(:app) } + let(:middleware) { described_class.new(app) } + + describe '#call' do + describe 'when go-get=0' do + it 'skips go-import generation' do + env = { 'rack.input' => '', + 'QUERY_STRING' => 'go-get=0' } + expect(app).to receive(:call).with(env).and_return('no-go') + middleware.call(env) + end + end + + describe 'when go-get=1' do + it 'returns a document' do + env = { 'rack.input' => '', + 'QUERY_STRING' => 'go-get=1', + 'PATH_INFO' => '/group/project/path' } + resp = middleware.call(env) + expect(resp[0]).to eq(200) + expect(resp[1]['Content-Type']).to eq('text/html') + expected_body = "<!DOCTYPE html><html><head><meta content='localhost/group/project git http://localhost/group/project.git' name='go-import'></head></html>\n" + expect(resp[2].body).to eq([expected_body]) + end + end + end +end -- GitLab From 3a979fccaee183a1f13390cc49eb7ec454a00956 Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij <sytse@gitlab.com> Date: Sat, 12 Mar 2016 17:28:54 -0800 Subject: [PATCH 219/223] Remove duplicate entries. --- doc/ci/examples/README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index 0a83c4dd8584..cc059dc43763 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -1,14 +1,9 @@ # CI Examples -- [Testing PHP](php.md) +- [Testing a PHP application](php.md) - [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md) - [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md) - [Test a Clojure application](test-clojure-application.md) -- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) -- [Test your PHP applications](examples/php.md) -- [Test and deploy Ruby applications to Heroku](examples/test-and-deploy-ruby-application-to-heroku.md) -- [Test and deploy Python applications to Heroku](examples/test-and-deploy-python-application-to-heroku.md) -- [Test Clojure applications](examples/test-clojure-application.md) - [Using `dpl` as deployment tool](deployment/README.md) - Help your favorite programming language and GitLab by sending a merge request with a guide for that language. @@ -17,3 +12,4 @@ - [Blost post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/) - [Repo's with examples for various languages](https://gitlab.com/groups/gitlab-examples) +- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) -- GitLab From 2048c0f1beb65aa784831a543b623d05877f6b86 Mon Sep 17 00:00:00 2001 From: tiagonbotelho <tiagonbotelho@hotmail.com> Date: Tue, 15 Mar 2016 12:40:13 +0000 Subject: [PATCH 220/223] moves the stage and ref from the table in a ci commit build to a plain one line text --- app/views/projects/commit/_builds.html.haml | 15 +++++++++------ .../commit_statuses/_commit_status.html.haml | 10 ---------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml index befad27666c3..95c6353d0c58 100644 --- a/app/views/projects/commit/_builds.html.haml +++ b/app/views/projects/commit/_builds.html.haml @@ -12,9 +12,16 @@ - if defined?(link_to_commit) && link_to_commit for commit = link_to @ci_commit.short_sha, namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), class: "monospace" - - if @ci_commit.duration > 0 in - = time_interval_in_words @ci_commit.duration + - if @ci_commit.refs + = link_to @ci_commit.refs.first, namespace_project_commits_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.refs.first) + - else + .light none + - if defined?(@ci_commit.stage) + at stage + = @ci_commit.stage + - if @ci_commit.duration > 0 + = "(#{time_interval_in_words @ci_commit.duration})" - if @ci_commit.yaml_errors.present? .bs-callout.bs-callout-danger @@ -34,8 +41,6 @@ %tr %th Status %th Build ID - %th Ref - %th Stage %th Name %th Duration %th Finished at @@ -56,8 +61,6 @@ %tr %th Status %th Build ID - %th Ref - %th Stage %th Name %th Duration %th Finished at diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index 205122e5d938..28a07c314a9f 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -27,12 +27,6 @@ - else .light none - %td - - if commit_status.ref - = link_to commit_status.ref, namespace_project_commits_path(commit_status.project.namespace, commit_status.project, commit_status.ref) - - else - .light none - - if defined?(runner) && runner %td - if commit_status.try(:runner) @@ -40,10 +34,6 @@ - else .light none - - if defined?(stage) && stage - %td - = commit_status.stage - %td = commit_status.name -- GitLab From 0d0cfb05ded3372666cdbcb3426dce53dd369e6e Mon Sep 17 00:00:00 2001 From: tiagonbotelho <tiagonbotelho@hotmail.com> Date: Thu, 17 Mar 2016 16:59:43 +0000 Subject: [PATCH 221/223] changes stage back to original position --- app/views/projects/commit/_builds.html.haml | 2 ++ app/views/projects/commit_statuses/_commit_status.html.haml | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml index 95c6353d0c58..f3580771e259 100644 --- a/app/views/projects/commit/_builds.html.haml +++ b/app/views/projects/commit/_builds.html.haml @@ -41,6 +41,7 @@ %tr %th Status %th Build ID + %th Stage %th Name %th Duration %th Finished at @@ -61,6 +62,7 @@ %tr %th Status %th Build ID + %th Stage %th Name %th Duration %th Finished at diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index 28a07c314a9f..d6dd084a7c49 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -34,6 +34,10 @@ - else .light none + - if defined?(stage) && runner + %td + = commit_status.stage + %td = commit_status.name -- GitLab From c4d18b09d8958ab493f5f9ff6d5dc0372801e083 Mon Sep 17 00:00:00 2001 From: tiagonbotelho <tiagonbotelho@hotmail.com> Date: Wed, 23 Mar 2016 18:30:44 +0100 Subject: [PATCH 222/223] fixes defined? issue for ref and initializes it on the partials as false --- app/views/projects/commit/_builds.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml index f3580771e259..789569284f81 100644 --- a/app/views/projects/commit/_builds.html.haml +++ b/app/views/projects/commit/_builds.html.haml @@ -50,7 +50,7 @@ %th - @ci_commit.refs.each do |ref| = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered, - locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true } + locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true, ref: false, allow_retry: true } - if @ci_commit.retried.any? .gray-content-block.second-block @@ -70,4 +70,4 @@ %th Coverage %th = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried, - locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true } + locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true, ref: false } -- GitLab From 4dc07b9ebd2ba7b26eaf6bc84c8b2c4b51b8482c Mon Sep 17 00:00:00 2001 From: tiagonbotelho <tiagonbotelho@hotmail.com> Date: Wed, 23 Mar 2016 18:37:49 +0100 Subject: [PATCH 223/223] adds missing td --- app/views/projects/commit_statuses/_commit_status.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml index d6dd084a7c49..b36f6de480ca 100644 --- a/app/views/projects/commit_statuses/_commit_status.html.haml +++ b/app/views/projects/commit_statuses/_commit_status.html.haml @@ -22,7 +22,8 @@ = link_to commit_status.short_sha, namespace_project_commit_path(commit_status.project.namespace, commit_status.project, commit_status.sha), class: "monospace" - if defined?(ref) && ref - = link_to commit_status.ref, namespace_project_commits_path(commit_status.project.namespace, commit_status.project, commit_status.ref) + %td + = link_to commit_status.ref, namespace_project_commits_path(commit_status.project.namespace, commit_status.project, commit_status.ref) - else .light none -- GitLab