From 7ea3bbd4e0b01b4d25a984f5e6c0471053d8874a Mon Sep 17 00:00:00 2001 From: Christian Mehlmauer Date: Sun, 14 Feb 2016 10:31:28 +0100 Subject: [PATCH 001/255] 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 42b2643984..c8083a4547 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 a10e62877b..ce7ae0d285 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 ``` From d24d806ea9743e05072a3260e7a4c1e8edd5e2dd Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 29 Feb 2016 10:20:45 +0100 Subject: [PATCH 002/255] 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 cf9710ede5..cb860bc43f 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 From 616281f6a9a5056c368f62c35fe91f7b19d37210 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 29 Feb 2016 12:00:42 +0100 Subject: [PATCH 003/255] 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 cb860bc43f..d573bea0f8 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 018ca22dbb..b82f2d19b1 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 From cf73dc34f7ea6908e2d5d65cfc372ecebfbe09f6 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 29 Feb 2016 12:12:51 +0100 Subject: [PATCH 004/255] 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 d573bea0f8..94ef4d2c38 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) From 5b3b4ff65e7cc809bdf026f21a9a56649e073e18 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 29 Feb 2016 12:14:46 +0100 Subject: [PATCH 005/255] 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 e9033aeacd..c471698235 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 remove runner. -Used to removing runners. - - 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" -} -``` From ec7c6a084c5c366f67932bc21e45ef7fff4abac9 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 29 Feb 2016 12:16:32 +0100 Subject: [PATCH 006/255] 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 e4d436b8e5..6341440c58 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 94ef4d2c38..aea808007f 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 871de7abcc..0000000000 --- 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 fe6b1c0135..0000000000 --- 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 From 74b963268c781ab714983a5c88e40f737c24c84b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 1 Mar 2016 10:57:12 +0100 Subject: [PATCH 007/255] 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 b82f2d19b1..d100e26117 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 c471698235..2f01da4bd7 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 From 4ed40db9cefc35d06a13adeed55a76021cd8522c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Tue, 1 Mar 2016 19:17:22 +0000 Subject: [PATCH 008/255] 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 ecbe0d3e88..f6ba80f46d 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. From 9c593a51c377f474f1aac1ed16b1dc8eafc6f6d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 2 Mar 2016 10:33:38 +0000 Subject: [PATCH 009/255] 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 d6842affa6..f96ac6f975 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -7,8 +7,6 @@ Rails.application.configure do # and recreated between test runs. Don't rely on the data there! config.cache_classes = false - config.cache_store = :null_store - # Configure static asset server for tests with Cache-Control for performance config.serve_static_files = true config.static_cache_control = "public, max-age=3600" From 70623cd423b0c7e26e56422bf25c413d9921ee88 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Wed, 2 Mar 2016 12:18:43 +0100 Subject: [PATCH 010/255] 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 6f5d592755..6c9377448e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -889,13 +889,13 @@ class Project < ActiveRecord::Base 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 e10b5529b4..f328deda35 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 From 3447ffbe9a11375a0cee40e2d5fa47e632361a3b Mon Sep 17 00:00:00 2001 From: nico de ceulaer Date: Thu, 3 Mar 2016 14:50:21 +0100 Subject: [PATCH 011/255] 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 f0aa2b5712..40a8b512e4 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -72,7 +72,7 @@ module ApplicationHelper if user_or_email.is_a?(User) user = user_or_email else - user = User.find_by(email: user_or_email.downcase) + user = User.find_by_any_email(user_or_email.downcase) end if user From 21a05328ffd5cb9130ae516faa7dd672cacba90c Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 3 Mar 2016 15:19:27 +0200 Subject: [PATCH 012/255] 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 9ba200f7bd..b50a7a4217 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -96,7 +96,9 @@ class GitPushService < BaseService # a different branch. closed_issues = commit.closes_issues(current_user) closed_issues.each do |issue| - Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit) + if can?(current_user, :update_issue, issue) + Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit) + end end end diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb index 8f25c5e249..ebb67c7db6 100644 --- a/app/services/merge_requests/post_merge_service.rb +++ b/app/services/merge_requests/post_merge_service.rb @@ -21,7 +21,9 @@ module MergeRequests closed_issues = merge_request.closes_issues(current_user) closed_issues.each do |issue| - Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request) + if can?(current_user, :update_issue, issue) + Issues::CloseService.new(project, current_user, {}).execute(issue, merge_request) + end end end From cf232411ae37b68c9d6ab20deac3fc8d99b44124 Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Thu, 3 Mar 2016 11:21:32 -0500 Subject: [PATCH 013/255] 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 281eea8363..7c8f785a61 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 f256477196..ac15a2cdff 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 c84113556c..16e47bb99b 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 (``) +- 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 7c12557a32..623ec857cb 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 5076b2697a..36cb9da238 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 b59e92cb31..ba91685a20 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -1,17 +1,21 @@ # Git LFS -Managing large files such as audio, video and graphics files has always been one of the shortcomings of Git. -The general recommendation is to not have Git repositories larger than 1GB to preserve performance. +Managing large files such as audio, video and graphics files has always been one +of the shortcomings of Git. The general recommendation is to not have Git repositories +larger than 1GB to preserve performance. -GitLab already supports [managing large files with git annex](http://doc.gitlab.com/ee/workflow/git_annex.html) (EE only), however in certain -environments it is not always convenient to use different commands to differentiate between the large files and regular ones. +GitLab already supports [managing large files with git annex](http://doc.gitlab.com/ee/workflow/git_annex.html) +(EE only), however in certain environments it is not always convenient to use +different commands to differentiate between the large files and regular ones. -Git LFS makes this simpler for the end user by removing the requirement to learn new commands. +Git LFS makes this simpler for the end user by removing the requirement to +learn new commands. ## How it works -Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication to authorize client requests. -Once the request is authorized, Git LFS client receives instructions from where to fetch or where to push the large file. +Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication +to authorize client requests. Once the request is authorized, Git LFS client receives +instructions from where to fetch or where to push the large file. ## GitLab server configuration @@ -24,15 +28,19 @@ Documentation for GitLab instance administrators is under [LFS administration do ## Known limitations -* Git LFS v1 original API is not supported since it was deprecated early in LFS development +* Git LFS v1 original API is not supported since it was deprecated early in LFS + development * When SSH is set as a remote, Git LFS objects still go through HTTPS -* Any Git LFS request will ask for HTTPS credentials to be provided so good Git credentials store is recommended -* Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have to add the URL to Git config manually (see #troubleshooting) +* Any Git LFS request will ask for HTTPS credentials to be provided so good Git + credentials store is recommended +* Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have + to add the URL to Git config manually (see #troubleshooting) ## Using Git LFS -Lets take a look at the workflow when you need to check large files into your Git repository with Git LFS: -For example, if you want to upload a very large file and check it into your Git repository: +Lets take a look at the workflow when you need to check large files into your Git +repository with Git LFS. For example, if you want to upload a very large file and +check it into your Git repository: ```bash git clone git@gitlab.example.com:group/project.git @@ -40,7 +48,8 @@ git lfs init # initialize the Git LFS project project git lfs track "*.iso" # select the file extensions that you want to treat as large files ``` -Once a certain file extension is marked for tracking as a LFS object you can use Git as usual without having to redo the command to track a file with the same extension: +Once a certain file extension is marked for tracking as a LFS object you can use +Git as usual without having to redo the command to track a file with the same extension: ```bash cp ~/tmp/debian.iso ./ # copy a large file into the current directory @@ -49,13 +58,17 @@ git commit -am "Added Debian iso" # commit the file meta data git push origin master # sync the git repo and large file to the GitLab server ``` -Cloning the repository works the same as before. Git automatically detects the LFS-tracked files and clones them via HTTP. If you performed the git clone command with a SSH URL, you have to enter your GitLab credentials for HTTP authentication. +Cloning the repository works the same as before. Git automatically detects the +LFS-tracked files and clones them via HTTP. If you performed the git clone +command with a SSH URL, you have to enter your GitLab credentials for HTTP +authentication. ```bash git clone git@gitlab.example.com:group/project.git ``` -If you already cloned the repository and you want to get the latest LFS object that are on the remote repository, eg. from branch `master`: +If you already cloned the repository and you want to get the latest LFS object +that are on the remote repository, eg. from branch `master`: ```bash git lfs fetch master @@ -73,8 +86,8 @@ Check if you have permissions to push to the project or fetch from the project. * Project is not allowed to access the LFS object -LFS object you are trying to push to the project or fetch from the project is not available to the project anymore. -Probably the object was removed from the server. +LFS object you are trying to push to the project or fetch from the project is not +available to the project anymore. Probably the object was removed from the server. * Local git repository is using deprecated LFS API @@ -89,16 +102,26 @@ git lfs logs last If the status `error 501` is shown, it is because: -* Git LFS support is not enabled on the GitLab server. Check with your GitLab administrator why Git LFS is not enabled on the server. See [LFS administration documentation](lfs_administration.md) for instructions on how to enable LFS support. +* Git LFS support is not enabled on the GitLab server. Check with your GitLab + administrator why Git LFS is not enabled on the server. See + [LFS administration documentation](lfs_administration.md) for instructions + on how to enable LFS support. -* Git LFS client version is not supported by GitLab server. Check your Git LFS version with `git lfs version`. Check the Git config of the project for traces of deprecated API with `git lfs -l`. If `batch = false` is set in the config, remove the line and try to update your Git LFS client. Only version 1.0.1 and newer are supported. +* Git LFS client version is not supported by GitLab server. Check your Git LFS + version with `git lfs version`. Check the Git config of the project for traces + of deprecated API with `git lfs -l`. If `batch = false` is set in the config, + remove the line and try to update your Git LFS client. Only version 1.0.1 and + newer are supported. ### getsockopt: connection refused -If you push a LFS object to a project and you receive an error similar to: `Post /info/lfs/objects/batch: dial tcp IP: getsockopt: connection refused`, -the LFS client is trying to reach GitLab through HTTPS. However, your GitLab instance is being served on HTTP. +If you push a LFS object to a project and you receive an error similar to: +`Post /info/lfs/objects/batch: dial tcp IP: getsockopt: connection refused`, +the LFS client is trying to reach GitLab through HTTPS. However, your GitLab +instance is being served on HTTP. -This behaviour is caused by Git LFS using HTTPS connections by default when a `lfsurl` is not set in the Git config. +This behaviour is caused by Git LFS using HTTPS connections by default when a +`lfsurl` is not set in the Git config. To prevent this from happening, set the lfs url in project Git config: @@ -109,18 +132,24 @@ git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs/o ### Credentials are always required when pushing an object -Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing the LFS object on every push for every object, user HTTPS credentials are required. +Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing +the LFS object on every push for every object, user HTTPS credentials are required. -By default, Git has support for remembering the credentials for each repository you use. This is described in [Git credentials man pages](https://git-scm.com/docs/gitcredentials). +By default, Git has support for remembering the credentials for each repository +you use. This is described in [Git credentials man pages](https://git-scm.com/docs/gitcredentials). -For example, you can tell Git to remember the password for a period of time in which you expect to push the objects: +For example, you can tell Git to remember the password for a period of time in +which you expect to push the objects: ```bash git config --global credential.helper 'cache --timeout=3600' ``` -This will remember the credentials for an hour after which Git operations will require re-authentication. +This will remember the credentials for an hour after which Git operations will +require re-authentication. -If you are using OS X you can use `osxkeychain` to store and encrypt your credentials. For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases). +If you are using OS X you can use `osxkeychain` to store and encrypt your credentials. +For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases). -More details about various methods of storing the user credentials can be found on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). \ No newline at end of file +More details about various methods of storing the user credentials can be found +on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). \ No newline at end of file From fc90d9e5896cdcccedb697fd4536f126d10f3f8e Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 3 Mar 2016 17:59:47 +0100 Subject: [PATCH 014/255] 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 b64dbbd89c..6de7888888 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -7,6 +7,8 @@ class Projects::AvatarsController < Projects::ApplicationController @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 d9723acb1d..b6ff08262d 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -12,6 +12,8 @@ class Projects::RawController < Projects::ApplicationController 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 7f63a2e2cb..adb56e49c6 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -152,4 +152,29 @@ module BlobHelper '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 148eab692f..aba48f87be 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 @@ -544,9 +545,9 @@ class Project < ActiveRecord::Base 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 3c11b97921..18caddabd3 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))} From b0adc07b3ca30430549886c3b9906c24cc9d2c6a Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller Date: Thu, 3 Mar 2016 19:16:38 +0000 Subject: [PATCH 015/255] 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 0f2665a3bf..5a4b2f43ba 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. From e033b9a7f19223e4fbad6fa3c712c82f49e47244 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 4 Mar 2016 08:42:22 +0000 Subject: [PATCH 016/255] 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 c95ead22e6..3142afccd6 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 eb9c225df2..b151393aba 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 1c7de94acf..393998f15b 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 09740d8ea1..2b293e4770 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 From da6e38a889d4725a50fbe0bf8a3ca1d3e53d01c3 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 4 Mar 2016 10:44:04 +0000 Subject: [PATCH 017/255] 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 3142afccd6..eff8beb97e 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 2b293e4770..f675f092da 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 504654f90d..786a0cad97 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 1a360cd1eb..d9a8058efd 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -22,7 +22,7 @@ describe 'Comments', feature: true do 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 @@ describe 'Comments', feature: true do 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 @@ describe 'Comments', feature: true do 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 From b0d24ab1b52a78bf855a70886113b79707036029 Mon Sep 17 00:00:00 2001 From: evuez Date: Wed, 24 Feb 2016 13:14:37 +0100 Subject: [PATCH 018/255] 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 d61c5e74f3..0bbcd6b867 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,7 @@ v 8.6.0 (unreleased) - Fix avatar stretching by providing a cropping feature (Johann Pardanaud) - Don't load all of GitLab in mail_room - 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 - Update documentation to reflect Guest role not being enforced on internal projects - Allow search for logged out users diff --git a/doc/api/users.md b/doc/api/users.md index b7fc903825..82c57a2fd4 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 b021db8fa5..0779fb881a 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -23,6 +23,8 @@ module API end class UserFull < User + expose :last_sign_in_at + expose :confirmed_at expose :email expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at expose :identities, using: Entities::Identity diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index b82c5c7685..96e8c8c51f 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -47,6 +47,8 @@ describe API::API, api: true do expect(json_response.first.keys).to include 'identities' expect(json_response.first.keys).to include 'can_create_project' expect(json_response.first.keys).to include 'two_factor_enabled' + expect(json_response.first.keys).to include 'last_sign_in_at' + expect(json_response.first.keys).to include 'confirmed_at' end end end From b1264d86fa0772860b284893a3bf129bec35cb0d Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 4 Mar 2016 12:22:27 +0000 Subject: [PATCH 019/255] 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 dde864f518..b0c4668fc7 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -404,7 +404,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 From f95f242cfad4fb0032caecea47964d78c1f652bc Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 4 Mar 2016 13:52:29 +0000 Subject: [PATCH 020/255] 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 277c63914d..937fbbd34e 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 565bf088b4..3a189a9a1e 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 b0c4668fc7..8a420f9deb 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -417,7 +417,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 @@ -509,7 +509,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps 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 06e6944189..906b66a4a6 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 ae10c6069a..e59bfbea99 100644 --- a/features/steps/shared/issuable.rb +++ b/features/steps/shared/issuable.rb @@ -182,7 +182,7 @@ module SharedIssuable 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 eb6df61b8e..6870d364ac 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 From 97093a52984f1f4682b5d3432f7666f3c806c4ca Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 4 Mar 2016 15:17:12 +0100 Subject: [PATCH 021/255] 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 df08d3a6df..3388411859 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 From 5eeea4b7c69d29e8e8371b3c63f25938dcca7bb6 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 4 Mar 2016 15:20:30 +0100 Subject: [PATCH 022/255] 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 6c63122800..e708b2df54 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 From f943156b5e4cf5e00ea5373de55deb993c72662d Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 4 Mar 2016 15:45:58 +0100 Subject: [PATCH 023/255] 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 a09e91578b..4353ceaa75 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -211,4 +211,16 @@ module CommitsHelper 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 ce60fbdf03..5fe4d91883 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 e708b2df54..998251094a 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 From 4cb3a4a73eb9f650bf2f16a10030861b1b7b9997 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 4 Mar 2016 15:47:11 +0100 Subject: [PATCH 024/255] 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 4353ceaa75..f994c9e617 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -214,7 +214,6 @@ module CommitsHelper 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 From acb211215c30244612eae6219539ced4a071b0dc Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 4 Mar 2016 16:11:33 +0100 Subject: [PATCH 025/255] Add option to show archived projects on dahsboard Signed-off-by: Dmitriy Zaporozhets --- 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 0000000000..f63b703d10 --- /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 dc880b634e..fc51c3241a 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -1,18 +1,15 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController + include FilterProjects + before_action :event_filter def index - @projects = current_user.authorized_projects.sorted_by_activity.non_archived - @projects = @projects.sort(@sort = params[:sort]) + @projects = current_user.authorized_projects.sorted_by_activity + @projects = filter_projects(@projects) @projects = @projects.includes(:namespace) + @projects = @projects.sort(@sort = params[:sort]) + @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? - terms = params[:filter_projects] - - if terms.present? - @projects = @projects.search(terms) - end - - @projects = @projects.page(params[:page]).per(PER_PAGE) if terms.blank? @last_push = current_user.recent_push respond_to do |format| @@ -32,16 +29,11 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController def starred @projects = current_user.starred_projects.sorted_by_activity + @projects = filter_projects(@projects) @projects = @projects.includes(:namespace, :forked_from_project, :tags) @projects = @projects.sort(@sort = params[:sort]) + @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? - terms = params[:filter_projects] - - if terms.present? - @projects = @projects.search(terms) - end - - @projects = @projects.page(params[:page]).per(PER_PAGE) if terms.blank? @last_push = current_user.recent_push @groups = [] diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index a384f3004d..5b811db306 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -1,12 +1,12 @@ class Explore::ProjectsController < Explore::ApplicationController + include FilterProjects + def index @projects = ProjectsFinder.new.execute(current_user) @tags = @projects.tags_on(:tags) @projects = @projects.tagged_with(params[:tag]) if params[:tag].present? @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? - @projects = @projects.non_archived - @projects = @projects.search(params[:search]) if params[:search].present? - @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? + @projects = filter_projects(@projects) @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? @@ -22,8 +22,7 @@ class Explore::ProjectsController < Explore::ApplicationController def trending @projects = TrendingProjectsFinder.new.execute(current_user) - @projects = @projects.non_archived - @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? + @projects = filter_projects(@projects) @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? respond_to do |format| @@ -38,7 +37,7 @@ class Explore::ProjectsController < Explore::ApplicationController def starred @projects = ProjectsFinder.new.execute(current_user) - @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? + @projects = filter_projects(@projects) @projects = @projects.reorder('star_count DESC') @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? diff --git a/app/views/explore/projects/_dropdown.html.haml b/app/views/explore/projects/_dropdown.html.haml index a4b4cd8d6c..8859cf1eb1 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 From bebf34aa47235899b54c54995a8b6a13534f2eab Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 4 Mar 2016 16:42:37 +0100 Subject: [PATCH 026/255] Make projects sort dropdown shared so it can be reused for other pages Signed-off-by: Dmitriy Zaporozhets --- 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 3648757428..337b0aacbb 100644 --- a/app/helpers/explore_helper.rb +++ b/app/helpers/explore_helper.rb @@ -1,5 +1,5 @@ module ExploreHelper - def explore_projects_filter_path(options={}) + def filter_projects_path(options={}) exist_opts = { sort: params[:sort], scope: params[:scope], @@ -9,15 +9,7 @@ module ExploreHelper } options = exist_opts.merge(options) - - path = if explore_controller? - explore_projects_path - elsif current_action?(:starred) - starred_dashboard_projects_path - else - dashboard_projects_path - end - + path = request.path path << "?#{options.to_param}" path end diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 40f88261c1..9da3fcbd98 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 39e3e8e273..cd485da510 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 8859cf1eb1..3d721c40e2 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 From 2c431f89b1d172bdff326c93d58732a0d0a3f3bc Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 4 Mar 2016 16:42:58 +0100 Subject: [PATCH 027/255] Add projects list sort dropdown to group page Signed-off-by: Dmitriy Zaporozhets --- 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 ca5ce1e204..f05c29e997 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,4 +1,5 @@ class GroupsController < Groups::ApplicationController + include FilterProjects include IssuesAction include MergeRequestsAction @@ -41,7 +42,8 @@ class GroupsController < Groups::ApplicationController def show @last_push = current_user.recent_push if current_user @projects = @projects.includes(:namespace) - @projects = @projects.search(params[:filter_projects]) if params[:filter_projects].present? + @projects = filter_projects(@projects) + @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank? respond_to do |format| @@ -98,7 +100,7 @@ class GroupsController < Groups::ApplicationController end def load_projects - @projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity.non_archived + @projects ||= ProjectsFinder.new.execute(current_user, group: group).sorted_by_activity end # Dont allow unauthorized access to group diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index 794aa57b55..7cd8e9bea4 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 From ee0f5bb511802c6aefe725eed67b366601c9aad8 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 4 Mar 2016 16:56:56 +0100 Subject: [PATCH 028/255] Add test for archive toggle feature Signed-off-by: Dmitriy Zaporozhets --- 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 8692f61fe3..0803a6a02b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ v 8.6.0 (unreleased) - Allow search for logged out users - Don't show Issues/MRs from archived projects in Groups view - Increase the notes polling timeout over time (Roberto Dip) + - 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 69b3a77644..bed9282f1c 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 36e092f50c..6510f8d9b3 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 From 38984a59ac6c540b1cb97518d48386f5fc42490c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 4 Mar 2016 17:53:13 +0100 Subject: [PATCH 029/255] Show active sorting method for projects list Signed-off-by: Dmitriy Zaporozhets --- 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 f9026b887d..2f2d2721d6 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -16,6 +16,16 @@ module SortingHelper } end + def projects_sort_options_hash + { + sort_value_name => sort_title_name, + sort_value_recently_updated => sort_title_recently_updated, + sort_value_oldest_updated => sort_title_oldest_updated, + sort_value_recently_created => sort_title_recently_created, + sort_value_oldest_created => sort_title_oldest_created, + } + end + def sort_title_oldest_updated 'Oldest updated' end diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml index 3d721c40e2..89569906db 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 From 619321dc8d02aac16576d8407238acef0069f3ba Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 4 Mar 2016 19:24:47 +0100 Subject: [PATCH 030/255] Fix test Signed-off-by: Dmitriy Zaporozhets --- 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 7df6b6f09b..092e18d1b8 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" From 62067c5b0f9bc7764a88b9bc618d21cae4f90875 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 4 Mar 2016 20:12:00 +0100 Subject: [PATCH 031/255] Align projects sort checkbox Signed-off-by: Dmitriy Zaporozhets --- .../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 d4878b333f..d2c7cdc3e9 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 89569906db..daaac092dc 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 From a056dfa9a077def4c3ffb958d3f86f7c9d7c2096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Mon, 22 Feb 2016 19:08:00 -0500 Subject: [PATCH 032/255] Refactor GlobalMilestone queries. Make methods return ActiveRecord Relations instead of Arrays. --- app/models/concerns/issuable.rb | 1 + app/models/global_milestone.rb | 26 +++++++++---------- app/models/merge_request.rb | 1 - app/models/project.rb | 1 + .../dashboard/milestones/_milestone.html.haml | 2 +- app/views/dashboard/milestones/show.html.haml | 2 +- .../groups/milestones/_milestone.html.haml | 2 +- app/views/groups/milestones/show.html.haml | 2 +- 8 files changed, 19 insertions(+), 18 deletions(-) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 286d665586..a3c4a3d277 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -29,6 +29,7 @@ module Issuable scope :assigned, -> { where("assignee_id IS NOT NULL") } scope :unassigned, -> { where("assignee_id IS NULL") } scope :of_projects, ->(ids) { where(project_id: ids) } + scope :of_milestones, ->(ids) { where(milestone_id: ids) } scope :opened, -> { with_state(:opened, :reopened) } scope :only_opened, -> { with_state(:opened) } scope :only_reopened, -> { with_state(:reopened) } diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index 7ee276255a..e4dd90b631 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -28,27 +28,27 @@ class GlobalMilestone end def projects - milestones.map { |milestone| milestone.project } + @projects ||= Project.for_milestones(milestones.map(&:id)) end - def issue_count - milestones.map { |milestone| milestone.issues.count }.sum + def issues_count + issues.count end def merge_requests_count - milestones.map { |milestone| milestone.merge_requests.count }.sum + merge_requests.count end def open_items_count - milestones.map { |milestone| milestone.open_items_count }.sum + opened_issues.count + opened_merge_requests.count end def closed_items_count - milestones.map { |milestone| milestone.closed_items_count }.sum + closed_issues.count + closed_merge_requests.count end def total_items_count - milestones.map { |milestone| milestone.total_items_count }.sum + issues_count + merge_requests_count end def percent_complete @@ -76,11 +76,11 @@ class GlobalMilestone end def issues - @issues ||= milestones.map(&:issues).flatten.group_by(&:state) + @issues ||= Issue.of_milestones(milestones.map(&:id)) end def merge_requests - @merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state) + @merge_requests ||= MergeRequest.of_milestones(milestones.map(&:id)) end def participants @@ -88,19 +88,19 @@ class GlobalMilestone end def opened_issues - issues.values_at("opened", "reopened").compact.flatten + issues.opened end def closed_issues - issues['closed'] + issues.closed end def opened_merge_requests - merge_requests.values_at("opened", "reopened").compact.flatten + merge_requests.opened end def closed_merge_requests - merge_requests.values_at("closed", "merged", "locked").compact.flatten + merge_requests.with_states(:closed, :merged, :locked) end def complete? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 025b522cf6..f575494e2b 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -139,7 +139,6 @@ class MergeRequest < ActiveRecord::Base scope :of_projects, ->(ids) { where(target_project_id: ids) } scope :opened, -> { with_states(:opened, :reopened) } scope :merged, -> { with_state(:merged) } - scope :closed, -> { with_state(:closed) } scope :closed_and_merged, -> { with_states(:closed, :merged) } scope :join_project, -> { joins(:target_project) } diff --git a/app/models/project.rb b/app/models/project.rb index 148eab692f..3a28d5d7fe 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -215,6 +215,7 @@ class Project < ActiveRecord::Base scope :public_only, -> { where(visibility_level: Project::PUBLIC) } scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) } scope :non_archived, -> { where(archived: false) } + scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids) } state_machine :import_status, initial: :none do event :import_start do diff --git a/app/views/dashboard/milestones/_milestone.html.haml b/app/views/dashboard/milestones/_milestone.html.haml index 7c882a3270..ea6c304d7d 100644 --- a/app/views/dashboard/milestones/_milestone.html.haml +++ b/app/views/dashboard/milestones/_milestone.html.haml @@ -8,7 +8,7 @@ .row .col-sm-6 = link_to issues_dashboard_path(milestone_title: milestone.title) do - = pluralize milestone.issue_count, 'Issue' + = pluralize milestone.issues_count, 'Issue' · = link_to merge_requests_dashboard_path(milestone_title: milestone.title) do = pluralize milestone.merge_requests_count, 'Merge Request' diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml index 3810267577..bbe0b0905c 100644 --- a/app/views/dashboard/milestones/show.html.haml +++ b/app/views/dashboard/milestones/show.html.haml @@ -52,7 +52,7 @@ %li.active = link_to '#tab-issues', 'data-toggle' => 'tab' do Issues - %span.badge= @milestone.issue_count + %span.badge= @milestone.issues_count %li = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do Merge Requests diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml index a20bf75bc3..50558d7dce 100644 --- a/app/views/groups/milestones/_milestone.html.haml +++ b/app/views/groups/milestones/_milestone.html.haml @@ -8,7 +8,7 @@ .row .col-sm-6 = link_to issues_group_path(@group, milestone_title: milestone.title) do - = pluralize milestone.issue_count, 'Issue' + = pluralize milestone.issues_count, 'Issue' · = link_to merge_requests_group_path(@group, milestone_title: milestone.title) do = pluralize milestone.merge_requests_count, 'Merge Request' diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index 1233da8552..405df1d343 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -58,7 +58,7 @@ %li.active = link_to '#tab-issues', 'data-toggle' => 'tab' do Issues - %span.badge= @milestone.issue_count + %span.badge= @milestone.issues_count %li = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do Merge Requests From 70028d36bee51a9ed86d20fe5d6c895772cc476a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Mon, 22 Feb 2016 20:39:36 -0500 Subject: [PATCH 033/255] Recator Issues Tab into a custom partial. --- app/views/groups/milestones/show.html.haml | 15 ++------------- app/views/projects/milestones/show.html.haml | 9 +-------- .../milestones/_issue.html.haml | 5 +++-- .../milestones/_issues.html.haml | 2 +- app/views/shared/milestones/_issues_tab.html.haml | 7 +++++++ 5 files changed, 14 insertions(+), 24 deletions(-) rename app/views/{projects => shared}/milestones/_issue.html.haml (63%) rename app/views/{projects => shared}/milestones/_issues.html.haml (80%) create mode 100644 app/views/shared/milestones/_issues_tab.html.haml diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index 405df1d343..0114e68b19 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -68,20 +68,9 @@ Participants %span.badge= @milestone.participants.count -.tab-content +.tab-content.milestone-content .tab-pane.active#tab-issues - .gray-content-block.middle-block - .pull-right - = link_to 'Browse Issues', issues_group_path(@group, milestone_title: @milestone.title), class: "btn btn-grouped" - - .oneline - All issues in this milestone - - .row.prepend-top-default - .col-md-6 - = render 'issues', title: "Open", issues: @milestone.opened_issues - .col-md-6 - = render 'issues', title: "Closed", issues: @milestone.closed_issues + = render 'shared/milestones/issues_tab', unassigned: @milestone.opened_issues.unassigned, assigned: @milestone.opened_issues.assigned, closed: @milestone.closed_issues .tab-pane#tab-merge-requests .gray-content-block.middle-block diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 2cae1ac4e2..2322946894 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -94,14 +94,7 @@ .tab-content.milestone-content .tab-pane.active#tab-issues - .row.prepend-top-default - .col-md-4 - = render('issues', title: 'Unstarted Issues (open and unassigned)', issues: @issues.opened.unassigned, id: 'unassigned') - .col-md-4 - = render('issues', title: 'Ongoing Issues (open and assigned)', issues: @issues.opened.assigned, id: 'ongoing') - .col-md-4 - = render('issues', title: 'Completed Issues (closed)', issues: @issues.closed, id: 'closed') - + = render 'shared/milestones/issues_tab', unassigned: @issues.opened.unassigned, assigned: @issues.opened.assigned, closed: @issues.closed .tab-pane#tab-merge-requests .row.prepend-top-default .col-md-3 diff --git a/app/views/projects/milestones/_issue.html.haml b/app/views/shared/milestones/_issue.html.haml similarity index 63% rename from app/views/projects/milestones/_issue.html.haml rename to app/views/shared/milestones/_issue.html.haml index ca51b8c745..07880404c9 100644 --- a/app/views/projects/milestones/_issue.html.haml +++ b/app/views/shared/milestones/_issue.html.haml @@ -1,8 +1,9 @@ +- project = issue.project %li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid, 'data-url' => issue_path(issue) } %span - = link_to_gfm issue.title, [@project.namespace.becomes(Namespace), @project, issue], title: issue.title + = link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title .issue-detail - = link_to [@project.namespace.becomes(Namespace), @project, issue] do + = link_to [project.namespace.becomes(Namespace), project, issue] do %span.issue-number ##{issue.iid} - issue.labels.each do |label| = render_colored_label(label) diff --git a/app/views/projects/milestones/_issues.html.haml b/app/views/shared/milestones/_issues.html.haml similarity index 80% rename from app/views/projects/milestones/_issues.html.haml rename to app/views/shared/milestones/_issues.html.haml index 6f8a341e47..e1b5c2eb66 100644 --- a/app/views/projects/milestones/_issues.html.haml +++ b/app/views/shared/milestones/_issues.html.haml @@ -4,4 +4,4 @@ .pull-right= issues.size %ul{ class: "well-list issues-sortable-list", id: "issues-list-#{id}", "data-state" => id } - issues.sort_by(&:position).each do |issue| - = render 'issue', issue: issue + = render 'shared/milestones/issue', issue: issue diff --git a/app/views/shared/milestones/_issues_tab.html.haml b/app/views/shared/milestones/_issues_tab.html.haml new file mode 100644 index 0000000000..3e98a809ad --- /dev/null +++ b/app/views/shared/milestones/_issues_tab.html.haml @@ -0,0 +1,7 @@ +.row.prepend-top-default + .col-md-4 + = render('shared/milestones/issues', title: 'Unstarted Issues (open and unassigned)', issues: unassigned, id: 'unassigned') + .col-md-4 + = render('shared/milestones/issues', title: 'Ongoing Issues (open and assigned)', issues: assigned, id: 'ongoing') + .col-md-4 + = render('shared/milestones/issues', title: 'Completed Issues (closed)', issues: closed, id: 'closed') From 37d92d0b7b91c678ce71393e73aa94860051fd5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 23 Feb 2016 11:53:59 -0500 Subject: [PATCH 034/255] Refactor Merge Requests tab into a custom partial --- app/views/groups/milestones/show.html.haml | 13 +------------ app/views/projects/milestones/show.html.haml | 15 +-------------- .../milestones/_merge_request.html.haml | 6 ++++-- .../milestones/_merge_requests.html.haml | 2 +- .../shared/milestones/_merge_requests_tab.haml | 13 +++++++++++++ 5 files changed, 20 insertions(+), 29 deletions(-) rename app/views/{projects => shared}/milestones/_merge_request.html.haml (58%) rename app/views/{projects => shared}/milestones/_merge_requests.html.haml (73%) create mode 100644 app/views/shared/milestones/_merge_requests_tab.haml diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index 0114e68b19..d9899fbd43 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -73,18 +73,7 @@ = render 'shared/milestones/issues_tab', unassigned: @milestone.opened_issues.unassigned, assigned: @milestone.opened_issues.assigned, closed: @milestone.closed_issues .tab-pane#tab-merge-requests - .gray-content-block.middle-block - .pull-right - = link_to 'Browse Merge Requests', merge_requests_group_path(@group, milestone_title: @milestone.title), class: "btn btn-grouped" - - .oneline - All merge requests in this milestone - - .row.prepend-top-default - .col-md-6 - = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests - .col-md-6 - = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests + = render 'shared/milestones/merge_requests_tab', unassigned: @milestone.opened_merge_requests.unassigned, assigned: @milestone.opened_merge_requests.assigned, closed: @milestone.closed_merge_requests, merged: @milestone.merge_requests.merged .tab-pane#tab-participants .gray-content-block.middle-block diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 2322946894..02f330cce2 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -96,20 +96,7 @@ .tab-pane.active#tab-issues = render 'shared/milestones/issues_tab', unassigned: @issues.opened.unassigned, assigned: @issues.opened.assigned, closed: @issues.closed .tab-pane#tab-merge-requests - .row.prepend-top-default - .col-md-3 - = render('merge_requests', title: 'Work in progress (open and unassigned)', merge_requests: @merge_requests.opened.unassigned, id: 'unassigned') - .col-md-3 - = render('merge_requests', title: 'Waiting for merge (open and assigned)', merge_requests: @merge_requests.opened.assigned, id: 'ongoing') - .col-md-3 - = render('merge_requests', title: 'Rejected (closed)', merge_requests: @merge_requests.closed, id: 'closed') - .col-md-3 - .panel.panel-primary - .panel-heading Merged - %ul.well-list - - @merge_requests.merged.each do |merge_request| - = render 'merge_request', merge_request: merge_request - + = render 'shared/milestones/merge_requests_tab', unassigned: @merge_requests.opened.unassigned, assigned: @merge_requests.opened.assigned, closed: @merge_requests.closed, merged: @merge_requests.merged .tab-pane#tab-participants %ul.bordered-list - @users.each do |user| diff --git a/app/views/projects/milestones/_merge_request.html.haml b/app/views/shared/milestones/_merge_request.html.haml similarity index 58% rename from app/views/projects/milestones/_merge_request.html.haml rename to app/views/shared/milestones/_merge_request.html.haml index a1033607c5..a58dadb4f8 100644 --- a/app/views/projects/milestones/_merge_request.html.haml +++ b/app/views/shared/milestones/_merge_request.html.haml @@ -1,8 +1,10 @@ +- project = @project || merge_request.project + %li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid, 'data-url' => merge_request_path(merge_request) } %span.str-truncated - = link_to [@project.namespace.becomes(Namespace), @project, merge_request] do + = link_to [project.namespace.becomes(Namespace), project, merge_request] do %span.cgray ##{merge_request.iid} - = link_to_gfm merge_request.title, [@project.namespace.becomes(Namespace), @project, merge_request], title: merge_request.title + = link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title .pull-right.assignee-icon - if merge_request.assignee = image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16", alt: '' diff --git a/app/views/projects/milestones/_merge_requests.html.haml b/app/views/shared/milestones/_merge_requests.html.haml similarity index 73% rename from app/views/projects/milestones/_merge_requests.html.haml rename to app/views/shared/milestones/_merge_requests.html.haml index 9a5a02af21..c8df6c2e28 100644 --- a/app/views/projects/milestones/_merge_requests.html.haml +++ b/app/views/shared/milestones/_merge_requests.html.haml @@ -2,4 +2,4 @@ .panel-heading= title %ul{ class: "well-list merge_requests-sortable-list", id: "merge_requests-list-#{id}", "data-state" => id } - merge_requests.sort_by(&:position).each do |merge_request| - = render 'merge_request', merge_request: merge_request + = render 'shared/milestones/merge_request', merge_request: merge_request diff --git a/app/views/shared/milestones/_merge_requests_tab.haml b/app/views/shared/milestones/_merge_requests_tab.haml new file mode 100644 index 0000000000..5797aeb829 --- /dev/null +++ b/app/views/shared/milestones/_merge_requests_tab.haml @@ -0,0 +1,13 @@ +.row.prepend-top-default + .col-md-3 + = render('shared/milestones/merge_requests', title: 'Work in progress (open and unassigned)', merge_requests: unassigned, id: 'unassigned') + .col-md-3 + = render('shared/milestones/merge_requests', title: 'Waiting for merge (open and assigned)', merge_requests: assigned, id: 'ongoing') + .col-md-3 + = render('shared/milestones/merge_requests', title: 'Rejected (closed)', merge_requests: closed, id: 'closed') + .col-md-3 + .panel.panel-primary + .panel-heading Merged + %ul.well-list + - merged.each do |merge_request| + = render 'shared/milestones/merge_request', merge_request: merge_request From e805becfe838b6ac351a15a01d58e355a0b1763b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 23 Feb 2016 12:15:19 -0500 Subject: [PATCH 035/255] Eager load Issues/MRs project for Milestone. With this change we avoid doing N+1 queries when viewing Milestone's Issues/MRs from a Group context. --- app/models/global_milestone.rb | 4 ++-- app/views/groups/milestones/show.html.haml | 2 +- app/views/shared/milestones/_issue.html.haml | 4 +++- app/views/shared/milestones/_merge_request.html.haml | 1 + 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index e4dd90b631..40193a6f05 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -76,11 +76,11 @@ class GlobalMilestone end def issues - @issues ||= Issue.of_milestones(milestones.map(&:id)) + @issues ||= Issue.of_milestones(milestones.map(&:id)).includes(:project) end def merge_requests - @merge_requests ||= MergeRequest.of_milestones(milestones.map(&:id)) + @merge_requests ||= MergeRequest.of_milestones(milestones.map(&:id)).includes(:target_project) end def participants diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index d9899fbd43..c6016179ba 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -73,7 +73,7 @@ = render 'shared/milestones/issues_tab', unassigned: @milestone.opened_issues.unassigned, assigned: @milestone.opened_issues.assigned, closed: @milestone.closed_issues .tab-pane#tab-merge-requests - = render 'shared/milestones/merge_requests_tab', unassigned: @milestone.opened_merge_requests.unassigned, assigned: @milestone.opened_merge_requests.assigned, closed: @milestone.closed_merge_requests, merged: @milestone.merge_requests.merged + = render 'shared/milestones/merge_requests_tab', unassigned: @milestone.opened_merge_requests.unassigned, assigned: @milestone.opened_merge_requests.assigned, closed: @milestone.merge_requests.closed, merged: @milestone.merge_requests.merged .tab-pane#tab-participants .gray-content-block.middle-block diff --git a/app/views/shared/milestones/_issue.html.haml b/app/views/shared/milestones/_issue.html.haml index 07880404c9..be55c5b6fe 100644 --- a/app/views/shared/milestones/_issue.html.haml +++ b/app/views/shared/milestones/_issue.html.haml @@ -1,4 +1,6 @@ -- project = issue.project +-# @project is present when viewing Project's milestone +- project = @project || issue.project + %li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid, 'data-url' => issue_path(issue) } %span = link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title diff --git a/app/views/shared/milestones/_merge_request.html.haml b/app/views/shared/milestones/_merge_request.html.haml index a58dadb4f8..66e2a2955d 100644 --- a/app/views/shared/milestones/_merge_request.html.haml +++ b/app/views/shared/milestones/_merge_request.html.haml @@ -1,3 +1,4 @@ +-# @project is present when viewing Project's milestone - project = @project || merge_request.project %li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid, 'data-url' => merge_request_path(merge_request) } From 834b5d49ecb486065e3ecbf8b36becb416eff366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 23 Feb 2016 12:45:16 -0500 Subject: [PATCH 036/255] Refactor Merge Requests tab into a custom partial --- app/views/groups/milestones/show.html.haml | 13 +------------ app/views/projects/milestones/show.html.haml | 10 +--------- .../shared/milestones/_participants_tab.html.haml | 8 ++++++++ 3 files changed, 10 insertions(+), 21 deletions(-) create mode 100644 app/views/shared/milestones/_participants_tab.html.haml diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index c6016179ba..0a15f49ff1 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -76,15 +76,4 @@ = render 'shared/milestones/merge_requests_tab', unassigned: @milestone.opened_merge_requests.unassigned, assigned: @milestone.opened_merge_requests.assigned, closed: @milestone.merge_requests.closed, merged: @milestone.merge_requests.merged .tab-pane#tab-participants - .gray-content-block.middle-block - .oneline - All participants to this milestone - - %ul.bordered-list - - @milestone.participants.each do |user| - %li - = link_to user, title: user.name, class: "darken" do - = image_tag avatar_icon(user, 32), class: "avatar s32" - %strong= truncate(user.name, lenght: 40) - %br - %small.cgray= user.username + = render 'shared/milestones/participants_tab', users: @milestone.participants diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 02f330cce2..6ca66885eb 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -98,15 +98,7 @@ .tab-pane#tab-merge-requests = render 'shared/milestones/merge_requests_tab', unassigned: @merge_requests.opened.unassigned, assigned: @merge_requests.opened.assigned, closed: @merge_requests.closed, merged: @merge_requests.merged .tab-pane#tab-participants - %ul.bordered-list - - @users.each do |user| - %li - = link_to user, title: user.name, class: "darken" do - = image_tag avatar_icon(user, 32), class: "avatar s32" - %strong= truncate(user.name, lenght: 40) - %br - %small.cgray= user.username - + = render 'shared/milestones/participants_tab', users: @users .tab-pane#tab-labels %ul.bordered-list.manage-labels-list - @labels.each do |label| diff --git a/app/views/shared/milestones/_participants_tab.html.haml b/app/views/shared/milestones/_participants_tab.html.haml new file mode 100644 index 0000000000..67ae85ac27 --- /dev/null +++ b/app/views/shared/milestones/_participants_tab.html.haml @@ -0,0 +1,8 @@ +%ul.bordered-list + - users.each do |user| + %li + = link_to user, title: user.name, class: "darken" do + = image_tag avatar_icon(user, 32), class: "avatar s32" + %strong= truncate(user.name, lenght: 40) + %br + %small.cgray= user.username From ed4808555877c668366d98a5408937712ad10d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 23 Feb 2016 16:22:36 -0500 Subject: [PATCH 037/255] Refactor Merge Requests tab into a custom partial * Add Labels tab to Groups * Add decorator for label so it's aware of Milestones. --- app/decorators/label_with_milestone.rb | 19 +++++++++++++++++++ app/models/global_milestone.rb | 6 ++++++ app/views/groups/milestones/show.html.haml | 8 ++++++-- app/views/projects/milestones/show.html.haml | 14 +------------- .../shared/milestones/_labels_tab.html.haml | 15 +++++++++++++++ config/application.rb | 1 + 6 files changed, 48 insertions(+), 15 deletions(-) create mode 100644 app/decorators/label_with_milestone.rb create mode 100644 app/views/shared/milestones/_labels_tab.html.haml diff --git a/app/decorators/label_with_milestone.rb b/app/decorators/label_with_milestone.rb new file mode 100644 index 0000000000..a70a4e2f50 --- /dev/null +++ b/app/decorators/label_with_milestone.rb @@ -0,0 +1,19 @@ +class LabelWithMilestone + attr_reader :milestone + + def initialize(label, milestone) + @label, @milestone = label, milestone + end + + def method_missing(meth, *args) + if @label.respond_to?(meth) + @label.send(meth, *args) + else + super + end + end + + def respond_to?(meth) + @label.respond_to?(meth) + end +end diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index 40193a6f05..e13aaf1673 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -87,6 +87,12 @@ class GlobalMilestone @participants ||= milestones.map(&:participants).flatten.compact.uniq end + def labels + @labels ||= milestones.map do |ms| + ms.labels.map { |label| LabelWithMilestone.new(label, ms) } + end.flatten.sort_by!(&:title) + end + def opened_issues issues.opened end diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index 0a15f49ff1..7ffa3f2d51 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -67,13 +67,17 @@ = link_to '#tab-participants', 'data-toggle' => 'tab' do Participants %span.badge= @milestone.participants.count + %li + = link_to '#tab-labels', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do + Labels + %span.badge= @milestone.labels.count .tab-content.milestone-content .tab-pane.active#tab-issues = render 'shared/milestones/issues_tab', unassigned: @milestone.opened_issues.unassigned, assigned: @milestone.opened_issues.assigned, closed: @milestone.closed_issues - .tab-pane#tab-merge-requests = render 'shared/milestones/merge_requests_tab', unassigned: @milestone.opened_merge_requests.unassigned, assigned: @milestone.opened_merge_requests.assigned, closed: @milestone.merge_requests.closed, merged: @milestone.merge_requests.merged - .tab-pane#tab-participants = render 'shared/milestones/participants_tab', users: @milestone.participants + .tab-pane#tab-labels + = render 'shared/milestones/labels_tab', labels: @milestone.labels diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 6ca66885eb..4aa1a53e87 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -100,16 +100,4 @@ .tab-pane#tab-participants = render 'shared/milestones/participants_tab', users: @users .tab-pane#tab-labels - %ul.bordered-list.manage-labels-list - - @labels.each do |label| - %li - = render_colored_label(label) - - args = [@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title, label_name: label.title] - - options = args.extract_options! - - %span.issues-count - = link_to namespace_project_issues_path(*args, options.merge(state: 'opened')) do - = pluralize label.open_issues_count, 'open issue' - %span.issues-count - = link_to namespace_project_issues_path(*args, options.merge(state: 'closed')) do - = pluralize label.closed_issues_count, 'closed issue' + = render 'shared/milestones/labels_tab', labels: @labels diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml new file mode 100644 index 0000000000..bb6ce0afb4 --- /dev/null +++ b/app/views/shared/milestones/_labels_tab.html.haml @@ -0,0 +1,15 @@ +%ul.bordered-list.manage-labels-list + - labels.each do |label| + - milestone = @milestone.is_a?(Milestone) ? @milestone : label.milestone + + %li + = render_colored_label(label) + - args = [milestone.project.namespace, milestone.project, milestone_title: milestone.title, label_name: label.title] + - options = args.extract_options! + + %span.issues-count + = link_to namespace_project_issues_path(*args, options.merge(state: 'opened')) do + = pluralize label.open_issues_count, 'open issue' + %span.issues-count + = link_to namespace_project_issues_path(*args, options.merge(state: 'closed')) do + = pluralize label.closed_issues_count, 'closed issue' diff --git a/config/application.rb b/config/application.rb index 28684a3e57..fee8637a4c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -15,6 +15,7 @@ module Gitlab # Custom directories with classes and modules you want to be autoloadable. config.autoload_paths.push(*%W(#{config.root}/lib + #{config.root}/app/decorators #{config.root}/app/models/hooks #{config.root}/app/models/concerns #{config.root}/app/models/project_services From 32f8fc1f9a493dca41f5b67a15bb7ff37e5d78c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 23 Feb 2016 16:35:36 -0500 Subject: [PATCH 038/255] Refactor Milestone view for Dashboard. --- .../dashboard/milestones/_issue.html.haml | 10 ---- .../dashboard/milestones/_issues.html.haml | 6 --- .../milestones/_merge_request.html.haml | 10 ---- .../milestones/_merge_requests.html.haml | 6 --- app/views/dashboard/milestones/show.html.haml | 48 ++++--------------- 5 files changed, 10 insertions(+), 70 deletions(-) delete mode 100644 app/views/dashboard/milestones/_issue.html.haml delete mode 100644 app/views/dashboard/milestones/_issues.html.haml delete mode 100644 app/views/dashboard/milestones/_merge_request.html.haml delete mode 100644 app/views/dashboard/milestones/_merge_requests.html.haml diff --git a/app/views/dashboard/milestones/_issue.html.haml b/app/views/dashboard/milestones/_issue.html.haml deleted file mode 100644 index 1408ebdd5d..0000000000 --- a/app/views/dashboard/milestones/_issue.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid } - %span.milestone-row - - project = issue.project - %strong #{project.name_with_namespace} · - = link_to [project.namespace.becomes(Namespace), project, issue] do - %span.cgray ##{issue.iid} - = link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title - .pull-right.assignee-icon - - if issue.assignee - = image_tag avatar_icon(issue.assignee, 16), class: "avatar s16" diff --git a/app/views/dashboard/milestones/_issues.html.haml b/app/views/dashboard/milestones/_issues.html.haml deleted file mode 100644 index 9f350b772b..0000000000 --- a/app/views/dashboard/milestones/_issues.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -.panel.panel-default - .panel-heading= title - %ul{ class: "well-list issues-sortable-list" } - - if issues - - issues.each do |issue| - = render 'issue', issue: issue diff --git a/app/views/dashboard/milestones/_merge_request.html.haml b/app/views/dashboard/milestones/_merge_request.html.haml deleted file mode 100644 index 77c46de030..0000000000 --- a/app/views/dashboard/milestones/_merge_request.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid } - %span.milestone-row - - project = merge_request.project - %strong #{project.name_with_namespace} · - = link_to [project.namespace.becomes(Namespace), project, merge_request] do - %span.cgray ##{merge_request.iid} - = link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title - .pull-right.assignee-icon - - if merge_request.assignee - = image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16" diff --git a/app/views/dashboard/milestones/_merge_requests.html.haml b/app/views/dashboard/milestones/_merge_requests.html.haml deleted file mode 100644 index 50057e2c63..0000000000 --- a/app/views/dashboard/milestones/_merge_requests.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -.panel.panel-default - .panel-heading= title - %ul{ class: "well-list merge_requests-sortable-list" } - - if merge_requests - - merge_requests.each do |merge_request| - = render 'merge_request', merge_request: merge_request diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml index bbe0b0905c..3e9e120440 100644 --- a/app/views/dashboard/milestones/show.html.haml +++ b/app/views/dashboard/milestones/show.html.haml @@ -61,45 +61,17 @@ = link_to '#tab-participants', 'data-toggle' => 'tab' do Participants %span.badge= @milestone.participants.count + %li + = link_to '#tab-labels', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do + Labels + %span.badge= @milestone.labels.count -.tab-content +.tab-content.milestone-content .tab-pane.active#tab-issues - .gray-content-block.middle-block - .pull-right - = link_to 'Browse Issues', issues_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped" - - .oneline - All issues in this milestone - - .row.prepend-top-default - .col-md-6 - = render 'issues', title: "Open", issues: @milestone.opened_issues - .col-md-6 - = render 'issues', title: "Closed", issues: @milestone.closed_issues - + = render 'shared/milestones/issues_tab', unassigned: @milestone.opened_issues.unassigned, assigned: @milestone.opened_issues.assigned, closed: @milestone.closed_issues .tab-pane#tab-merge-requests - .gray-content-block.middle-block - .pull-right - = link_to 'Browse Merge Requests', merge_requests_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped" - - .oneline - All merge requests in this milestone - - .row.prepend-top-default - .col-md-6 - = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests - .col-md-6 - = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests - + = render 'shared/milestones/merge_requests_tab', unassigned: @milestone.opened_merge_requests.unassigned, assigned: @milestone.opened_merge_requests.assigned, closed: @milestone.merge_requests.closed, merged: @milestone.merge_requests.merged .tab-pane#tab-participants - .gray-content-block.middle-block - .oneline - All participants to this milestone - %ul.bordered-list - - @milestone.participants.each do |user| - %li - = link_to user, title: user.name, class: "darken" do - = image_tag avatar_icon(user, 32), class: "avatar s32" - %strong= truncate(user.name, lenght: 40) - %br - %small.cgray= user.username + = render 'shared/milestones/participants_tab', users: @milestone.participants + .tab-pane#tab-labels + = render 'shared/milestones/labels_tab', labels: @milestone.labels From e0a18829ee7f1d681500002a2b6bda122b64b4c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 23 Feb 2016 19:31:59 -0500 Subject: [PATCH 039/255] Show project name for Issues tab in Group and Dashboard context. --- app/views/dashboard/milestones/show.html.haml | 6 +++--- app/views/groups/milestones/show.html.haml | 6 +++--- app/views/shared/milestones/_issue.html.haml | 4 ++++ app/views/shared/milestones/_issues.html.haml | 2 +- app/views/shared/milestones/_issues_tab.html.haml | 9 ++++++--- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml index 3e9e120440..18cf6fcbfd 100644 --- a/app/views/dashboard/milestones/show.html.haml +++ b/app/views/dashboard/milestones/show.html.haml @@ -68,10 +68,10 @@ .tab-content.milestone-content .tab-pane.active#tab-issues - = render 'shared/milestones/issues_tab', unassigned: @milestone.opened_issues.unassigned, assigned: @milestone.opened_issues.assigned, closed: @milestone.closed_issues + = render 'shared/milestones/issues_tab', unassigned: @milestone.opened_issues.unassigned, assigned: @milestone.opened_issues.assigned, closed: @milestone.closed_issues, show_full_project_name: true .tab-pane#tab-merge-requests - = render 'shared/milestones/merge_requests_tab', unassigned: @milestone.opened_merge_requests.unassigned, assigned: @milestone.opened_merge_requests.assigned, closed: @milestone.merge_requests.closed, merged: @milestone.merge_requests.merged + = render 'shared/milestones/merge_requests_tab', unassigned: @milestone.opened_merge_requests.unassigned, assigned: @milestone.opened_merge_requests.assigned, closed: @milestone.merge_requests.closed, merged: @milestone.merge_requests.merged, show_full_project_name: true .tab-pane#tab-participants = render 'shared/milestones/participants_tab', users: @milestone.participants .tab-pane#tab-labels - = render 'shared/milestones/labels_tab', labels: @milestone.labels + = render 'shared/milestones/labels_tab', labels: @milestone.labels, show_full_project_name: true diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index 7ffa3f2d51..021425cad4 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -74,10 +74,10 @@ .tab-content.milestone-content .tab-pane.active#tab-issues - = render 'shared/milestones/issues_tab', unassigned: @milestone.opened_issues.unassigned, assigned: @milestone.opened_issues.assigned, closed: @milestone.closed_issues + = render 'shared/milestones/issues_tab', unassigned: @milestone.opened_issues.unassigned, assigned: @milestone.opened_issues.assigned, closed: @milestone.closed_issues, show_project_name: true .tab-pane#tab-merge-requests - = render 'shared/milestones/merge_requests_tab', unassigned: @milestone.opened_merge_requests.unassigned, assigned: @milestone.opened_merge_requests.assigned, closed: @milestone.merge_requests.closed, merged: @milestone.merge_requests.merged + = render 'shared/milestones/merge_requests_tab', unassigned: @milestone.opened_merge_requests.unassigned, assigned: @milestone.opened_merge_requests.assigned, closed: @milestone.merge_requests.closed, merged: @milestone.merge_requests.merged, show_project_name: true .tab-pane#tab-participants = render 'shared/milestones/participants_tab', users: @milestone.participants .tab-pane#tab-labels - = render 'shared/milestones/labels_tab', labels: @milestone.labels + = render 'shared/milestones/labels_tab', labels: @milestone.labels, show_project_name: true diff --git a/app/views/shared/milestones/_issue.html.haml b/app/views/shared/milestones/_issue.html.haml index be55c5b6fe..dbef742ebe 100644 --- a/app/views/shared/milestones/_issue.html.haml +++ b/app/views/shared/milestones/_issue.html.haml @@ -3,6 +3,10 @@ %li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid, 'data-url' => issue_path(issue) } %span + - if show_project_name + %strong #{project.name} · + - elsif show_full_project_name + %strong #{project.name_with_namespace} · = link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title .issue-detail = link_to [project.namespace.becomes(Namespace), project, issue] do diff --git a/app/views/shared/milestones/_issues.html.haml b/app/views/shared/milestones/_issues.html.haml index e1b5c2eb66..04053eb854 100644 --- a/app/views/shared/milestones/_issues.html.haml +++ b/app/views/shared/milestones/_issues.html.haml @@ -4,4 +4,4 @@ .pull-right= issues.size %ul{ class: "well-list issues-sortable-list", id: "issues-list-#{id}", "data-state" => id } - issues.sort_by(&:position).each do |issue| - = render 'shared/milestones/issue', issue: issue + = render 'shared/milestones/issue', issue: issue, show_project_name: show_project_name, show_full_project_name: show_full_project_name diff --git a/app/views/shared/milestones/_issues_tab.html.haml b/app/views/shared/milestones/_issues_tab.html.haml index 3e98a809ad..277cd81677 100644 --- a/app/views/shared/milestones/_issues_tab.html.haml +++ b/app/views/shared/milestones/_issues_tab.html.haml @@ -1,7 +1,10 @@ +- args = { show_project_name: local_assigns.fetch(:show_project_name, false), + show_full_project_name: local_assigns.fetch(:show_full_project_name, false) } + .row.prepend-top-default .col-md-4 - = render('shared/milestones/issues', title: 'Unstarted Issues (open and unassigned)', issues: unassigned, id: 'unassigned') + = render 'shared/milestones/issues', args.merge({ title: 'Unstarted Issues (open and unassigned)', issues: unassigned, id: 'unassigned' }) .col-md-4 - = render('shared/milestones/issues', title: 'Ongoing Issues (open and assigned)', issues: assigned, id: 'ongoing') + = render 'shared/milestones/issues', args.merge({ title: 'Ongoing Issues (open and assigned)', issues: assigned, id: 'ongoing' }) .col-md-4 - = render('shared/milestones/issues', title: 'Completed Issues (closed)', issues: closed, id: 'closed') + = render 'shared/milestones/issues', args.merge({ title: 'Completed Issues (closed)', issues: closed, id: 'closed' }) From 7cc102a0e6568fdfa08e63ba3a0b31ced2c30a7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 23 Feb 2016 20:12:10 -0500 Subject: [PATCH 040/255] Make Merge Requests tab have the same look as Issues. --- app/assets/stylesheets/pages/milestone.scss | 8 ++++---- app/views/groups/milestones/_issue.html.haml | 10 ---------- app/views/groups/milestones/_issues.html.haml | 6 ------ .../groups/milestones/_merge_request.html.haml | 10 ---------- .../groups/milestones/_merge_requests.html.haml | 6 ------ .../shared/milestones/_merge_request.html.haml | 14 ++++++++++---- .../shared/milestones/_merge_requests.html.haml | 7 +++++-- .../shared/milestones/_merge_requests_tab.haml | 15 +++++++-------- 8 files changed, 26 insertions(+), 50 deletions(-) delete mode 100644 app/views/groups/milestones/_issue.html.haml delete mode 100644 app/views/groups/milestones/_issues.html.haml delete mode 100644 app/views/groups/milestones/_merge_request.html.haml delete mode 100644 app/views/groups/milestones/_merge_requests.html.haml diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index d24adbf67e..2c2a8e34d5 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -19,7 +19,7 @@ li.milestone { width: 105px; } - .issue-row { + .issue-row, .mr-row { .color-label { border-radius: 2px; padding: 3px !important; @@ -44,11 +44,11 @@ li.milestone { } } -.issues-sortable-list { - .issue-detail { +.issues-sortable-list, .merge_requests-sortable-list { + .issue-detail, .merge-request-detail { display: block; - .issue-number{ + .issue-number, .merge-request-number { color: rgba(0,0,0,0.44); margin-right: 5px; } diff --git a/app/views/groups/milestones/_issue.html.haml b/app/views/groups/milestones/_issue.html.haml deleted file mode 100644 index 9b85d83d6d..0000000000 --- a/app/views/groups/milestones/_issue.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid } - %span.milestone-row - - project = issue.project - %strong #{project.name} · - = link_to [project.namespace.becomes(Namespace), project, issue] do - %span.cgray ##{issue.iid} - = link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title - .pull-right.assignee-icon - - if issue.assignee - = image_tag avatar_icon(issue.assignee, 16), class: "avatar s16", alt: '' diff --git a/app/views/groups/milestones/_issues.html.haml b/app/views/groups/milestones/_issues.html.haml deleted file mode 100644 index 9f350b772b..0000000000 --- a/app/views/groups/milestones/_issues.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -.panel.panel-default - .panel-heading= title - %ul{ class: "well-list issues-sortable-list" } - - if issues - - issues.each do |issue| - = render 'issue', issue: issue diff --git a/app/views/groups/milestones/_merge_request.html.haml b/app/views/groups/milestones/_merge_request.html.haml deleted file mode 100644 index e3aa4aad19..0000000000 --- a/app/views/groups/milestones/_merge_request.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid } - %span.milestone-row - - project = merge_request.project - %strong #{project.name} · - = link_to [project.namespace.becomes(Namespace), project, merge_request] do - %span.cgray ##{merge_request.iid} - = link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title - .pull-right.assignee-icon - - if merge_request.assignee - = image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16", alt: '' diff --git a/app/views/groups/milestones/_merge_requests.html.haml b/app/views/groups/milestones/_merge_requests.html.haml deleted file mode 100644 index 50057e2c63..0000000000 --- a/app/views/groups/milestones/_merge_requests.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -.panel.panel-default - .panel-heading= title - %ul{ class: "well-list merge_requests-sortable-list" } - - if merge_requests - - merge_requests.each do |merge_request| - = render 'merge_request', merge_request: merge_request diff --git a/app/views/shared/milestones/_merge_request.html.haml b/app/views/shared/milestones/_merge_request.html.haml index 66e2a2955d..7b6c660646 100644 --- a/app/views/shared/milestones/_merge_request.html.haml +++ b/app/views/shared/milestones/_merge_request.html.haml @@ -2,10 +2,16 @@ - project = @project || merge_request.project %li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid, 'data-url' => merge_request_path(merge_request) } - %span.str-truncated - = link_to [project.namespace.becomes(Namespace), project, merge_request] do - %span.cgray ##{merge_request.iid} + %span + - if show_project_name + %strong #{project.name} · + - elsif show_full_project_name + %strong #{project.name_with_namespace} · = link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title - .pull-right.assignee-icon + .merge-request-detail + = link_to [project.namespace.becomes(Namespace), project, merge_request] do + %span.merge-request-number ##{merge_request.iid} + - merge_request.labels.each do |label| + = render_colored_label(label) - if merge_request.assignee = image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16", alt: '' diff --git a/app/views/shared/milestones/_merge_requests.html.haml b/app/views/shared/milestones/_merge_requests.html.haml index c8df6c2e28..71991516e9 100644 --- a/app/views/shared/milestones/_merge_requests.html.haml +++ b/app/views/shared/milestones/_merge_requests.html.haml @@ -1,5 +1,8 @@ -.panel.panel-default +- primary = local_assigns.fetch(:primary, false) +- panel_class = primary ? 'panel-primary' : 'panel-default' + +.panel{ class: panel_class } .panel-heading= title %ul{ class: "well-list merge_requests-sortable-list", id: "merge_requests-list-#{id}", "data-state" => id } - merge_requests.sort_by(&:position).each do |merge_request| - = render 'shared/milestones/merge_request', merge_request: merge_request + = render 'shared/milestones/merge_request', merge_request: merge_request, show_project_name: show_project_name, show_full_project_name: show_full_project_name diff --git a/app/views/shared/milestones/_merge_requests_tab.haml b/app/views/shared/milestones/_merge_requests_tab.haml index 5797aeb829..d912e60796 100644 --- a/app/views/shared/milestones/_merge_requests_tab.haml +++ b/app/views/shared/milestones/_merge_requests_tab.haml @@ -1,13 +1,12 @@ +- args = { show_project_name: local_assigns.fetch(:show_project_name, false), + show_full_project_name: local_assigns.fetch(:show_full_project_name, false) } + .row.prepend-top-default .col-md-3 - = render('shared/milestones/merge_requests', title: 'Work in progress (open and unassigned)', merge_requests: unassigned, id: 'unassigned') + = render 'shared/milestones/merge_requests', args.merge({ title: 'Work in progress (open and unassigned)', merge_requests: unassigned, id: 'unassigned' }) .col-md-3 - = render('shared/milestones/merge_requests', title: 'Waiting for merge (open and assigned)', merge_requests: assigned, id: 'ongoing') + = render 'shared/milestones/merge_requests', args.merge({ title: 'Waiting for merge (open and assigned)', merge_requests: assigned, id: 'ongoing' }) .col-md-3 - = render('shared/milestones/merge_requests', title: 'Rejected (closed)', merge_requests: closed, id: 'closed') + = render 'shared/milestones/merge_requests', args.merge({ title: 'Rejected (closed)', merge_requests: closed, id: 'closed' }) .col-md-3 - .panel.panel-primary - .panel-heading Merged - %ul.well-list - - merged.each do |merge_request| - = render 'shared/milestones/merge_request', merge_request: merge_request + = render 'shared/milestones/merge_requests', args.merge({ title: 'Merged', merge_requests: merged, id: 'merged', primary: true }) From b6e5de2cfbc396ae4f59de2c2bb56432e705d695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 23 Feb 2016 21:17:14 -0500 Subject: [PATCH 041/255] Use the same partial when rendering Issues or Merge Requests. --- app/assets/stylesheets/pages/milestone.scss | 6 +++--- app/views/shared/milestones/_issue.html.haml | 17 ----------------- app/views/shared/milestones/_issues.html.haml | 7 ------- .../shared/milestones/_issues_tab.html.haml | 6 +++--- .../shared/milestones/_merge_request.html.haml | 17 ----------------- .../shared/milestones/_merge_requests.html.haml | 8 -------- .../shared/milestones/_merge_requests_tab.haml | 8 ++++---- 7 files changed, 10 insertions(+), 59 deletions(-) delete mode 100644 app/views/shared/milestones/_issue.html.haml delete mode 100644 app/views/shared/milestones/_issues.html.haml delete mode 100644 app/views/shared/milestones/_merge_request.html.haml delete mode 100644 app/views/shared/milestones/_merge_requests.html.haml diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 2c2a8e34d5..300c2fa7e1 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -19,7 +19,7 @@ li.milestone { width: 105px; } - .issue-row, .mr-row { + .issue-row, .merge_request-row { .color-label { border-radius: 2px; padding: 3px !important; @@ -45,10 +45,10 @@ li.milestone { } .issues-sortable-list, .merge_requests-sortable-list { - .issue-detail, .merge-request-detail { + .issue-detail, .merge_request-detail { display: block; - .issue-number, .merge-request-number { + .issue-number, .merge_request-number { color: rgba(0,0,0,0.44); margin-right: 5px; } diff --git a/app/views/shared/milestones/_issue.html.haml b/app/views/shared/milestones/_issue.html.haml deleted file mode 100644 index dbef742ebe..0000000000 --- a/app/views/shared/milestones/_issue.html.haml +++ /dev/null @@ -1,17 +0,0 @@ --# @project is present when viewing Project's milestone -- project = @project || issue.project - -%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid, 'data-url' => issue_path(issue) } - %span - - if show_project_name - %strong #{project.name} · - - elsif show_full_project_name - %strong #{project.name_with_namespace} · - = link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title - .issue-detail - = link_to [project.namespace.becomes(Namespace), project, issue] do - %span.issue-number ##{issue.iid} - - issue.labels.each do |label| - = render_colored_label(label) - - if issue.assignee - = image_tag avatar_icon(issue.assignee, 16), class: "avatar s24", alt: '' diff --git a/app/views/shared/milestones/_issues.html.haml b/app/views/shared/milestones/_issues.html.haml deleted file mode 100644 index 04053eb854..0000000000 --- a/app/views/shared/milestones/_issues.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -.panel.panel-default - .panel-heading - = title - .pull-right= issues.size - %ul{ class: "well-list issues-sortable-list", id: "issues-list-#{id}", "data-state" => id } - - issues.sort_by(&:position).each do |issue| - = render 'shared/milestones/issue', issue: issue, show_project_name: show_project_name, show_full_project_name: show_full_project_name diff --git a/app/views/shared/milestones/_issues_tab.html.haml b/app/views/shared/milestones/_issues_tab.html.haml index 277cd81677..44a221114b 100644 --- a/app/views/shared/milestones/_issues_tab.html.haml +++ b/app/views/shared/milestones/_issues_tab.html.haml @@ -3,8 +3,8 @@ .row.prepend-top-default .col-md-4 - = render 'shared/milestones/issues', args.merge({ title: 'Unstarted Issues (open and unassigned)', issues: unassigned, id: 'unassigned' }) + = render 'shared/milestones/records', args.merge({ title: 'Unstarted Issues (open and unassigned)', records: unassigned, id: 'unassigned', show_counter: true }) .col-md-4 - = render 'shared/milestones/issues', args.merge({ title: 'Ongoing Issues (open and assigned)', issues: assigned, id: 'ongoing' }) + = render 'shared/milestones/records', args.merge({ title: 'Ongoing Issues (open and assigned)', records: assigned, id: 'ongoing', show_counter: true }) .col-md-4 - = render 'shared/milestones/issues', args.merge({ title: 'Completed Issues (closed)', issues: closed, id: 'closed' }) + = render 'shared/milestones/records', args.merge({ title: 'Completed Issues (closed)', records: closed, id: 'closed', show_counter: true }) diff --git a/app/views/shared/milestones/_merge_request.html.haml b/app/views/shared/milestones/_merge_request.html.haml deleted file mode 100644 index 7b6c660646..0000000000 --- a/app/views/shared/milestones/_merge_request.html.haml +++ /dev/null @@ -1,17 +0,0 @@ --# @project is present when viewing Project's milestone -- project = @project || merge_request.project - -%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid, 'data-url' => merge_request_path(merge_request) } - %span - - if show_project_name - %strong #{project.name} · - - elsif show_full_project_name - %strong #{project.name_with_namespace} · - = link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title - .merge-request-detail - = link_to [project.namespace.becomes(Namespace), project, merge_request] do - %span.merge-request-number ##{merge_request.iid} - - merge_request.labels.each do |label| - = render_colored_label(label) - - if merge_request.assignee - = image_tag avatar_icon(merge_request.assignee, 16), class: "avatar s16", alt: '' diff --git a/app/views/shared/milestones/_merge_requests.html.haml b/app/views/shared/milestones/_merge_requests.html.haml deleted file mode 100644 index 71991516e9..0000000000 --- a/app/views/shared/milestones/_merge_requests.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -- primary = local_assigns.fetch(:primary, false) -- panel_class = primary ? 'panel-primary' : 'panel-default' - -.panel{ class: panel_class } - .panel-heading= title - %ul{ class: "well-list merge_requests-sortable-list", id: "merge_requests-list-#{id}", "data-state" => id } - - merge_requests.sort_by(&:position).each do |merge_request| - = render 'shared/milestones/merge_request', merge_request: merge_request, show_project_name: show_project_name, show_full_project_name: show_full_project_name diff --git a/app/views/shared/milestones/_merge_requests_tab.haml b/app/views/shared/milestones/_merge_requests_tab.haml index d912e60796..15b2876c32 100644 --- a/app/views/shared/milestones/_merge_requests_tab.haml +++ b/app/views/shared/milestones/_merge_requests_tab.haml @@ -3,10 +3,10 @@ .row.prepend-top-default .col-md-3 - = render 'shared/milestones/merge_requests', args.merge({ title: 'Work in progress (open and unassigned)', merge_requests: unassigned, id: 'unassigned' }) + = render 'shared/milestones/records', args.merge({ title: 'Work in progress (open and unassigned)', records: unassigned, id: 'unassigned' }) .col-md-3 - = render 'shared/milestones/merge_requests', args.merge({ title: 'Waiting for merge (open and assigned)', merge_requests: assigned, id: 'ongoing' }) + = render 'shared/milestones/records', args.merge({ title: 'Waiting for merge (open and assigned)', records: assigned, id: 'ongoing' }) .col-md-3 - = render 'shared/milestones/merge_requests', args.merge({ title: 'Rejected (closed)', merge_requests: closed, id: 'closed' }) + = render 'shared/milestones/records', args.merge({ title: 'Rejected (closed)', records: closed, id: 'closed' }) .col-md-3 - = render 'shared/milestones/merge_requests', args.merge({ title: 'Merged', merge_requests: merged, id: 'merged', primary: true }) + = render 'shared/milestones/records', args.merge({ title: 'Merged', records: merged, id: 'merged', primary: true }) From 3c157b6bf7fa97cba769ecacd11cdcdba43adf4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 23 Feb 2016 22:21:04 -0500 Subject: [PATCH 042/255] Show Project name on Labels tab for Group and Dashboard context. --- app/views/shared/milestones/_labels_tab.html.haml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml index bb6ce0afb4..88e8e4f900 100644 --- a/app/views/shared/milestones/_labels_tab.html.haml +++ b/app/views/shared/milestones/_labels_tab.html.haml @@ -1,12 +1,19 @@ +- show_project_name = local_assigns.fetch(:show_project_name, false) +- show_full_project_name = local_assigns.fetch(:show_full_project_name, false) + %ul.bordered-list.manage-labels-list - labels.each do |label| - milestone = @milestone.is_a?(Milestone) ? @milestone : label.milestone %li = render_colored_label(label) + - if show_project_name + %strong · #{milestone.project.name} + - elsif show_full_project_name + %strong · #{milestone.project.name_with_namespace} + - args = [milestone.project.namespace, milestone.project, milestone_title: milestone.title, label_name: label.title] - options = args.extract_options! - %span.issues-count = link_to namespace_project_issues_path(*args, options.merge(state: 'opened')) do = pluralize label.open_issues_count, 'open issue' From 96058605c39ed2a7f6fdbdbbc3da05fdbe170681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 23 Feb 2016 23:02:32 -0500 Subject: [PATCH 043/255] Show some stats about Milestone according to the new UI. --- app/views/dashboard/milestones/show.html.haml | 18 +++++++++++++----- app/views/groups/milestones/show.html.haml | 18 +++++++++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml index 18cf6fcbfd..88edc025c6 100644 --- a/app/views/dashboard/milestones/show.html.haml +++ b/app/views/dashboard/milestones/show.html.haml @@ -41,11 +41,19 @@ = milestone.expires_at .context - %p.lead - Progress: - #{@milestone.closed_items_count} closed - – - #{@milestone.open_items_count} open + .milestone-summary + %h4 Progress + %strong= @milestone.issues.size + issues: + %span.milestone-stat + %strong= @milestone.opened_issues.size + open and + %strong= @milestone.closed_issues.size + closed + %span.milestone-stat + %strong== #{@milestone.percent_complete}% + complete + = milestone_progress_bar(@milestone) %ul.nav-links.no-top.no-bottom diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index 021425cad4..bd61e50a5b 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -47,11 +47,19 @@ = milestone.expires_at .context - %p.lead - Progress: - #{@milestone.closed_items_count} closed - – - #{@milestone.open_items_count} open + .milestone-summary + %h4 Progress + %strong= @milestone.issues.size + issues: + %span.milestone-stat + %strong= @milestone.opened_issues.size + open and + %strong= @milestone.closed_issues.size + closed + %span.milestone-stat + %strong== #{@milestone.percent_complete}% + complete + = milestone_progress_bar(@milestone) %ul.nav-links.no-top.no-bottom From 3f2d82485ed84247d88cde6423c9df517c8962b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 24 Feb 2016 14:05:28 -0500 Subject: [PATCH 044/255] Add missing partials! --- app/views/shared/milestones/_record.html.haml | 17 +++++++++++++++++ app/views/shared/milestones/_records.html.haml | 14 ++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 app/views/shared/milestones/_record.html.haml create mode 100644 app/views/shared/milestones/_records.html.haml diff --git a/app/views/shared/milestones/_record.html.haml b/app/views/shared/milestones/_record.html.haml new file mode 100644 index 0000000000..a753dd78a6 --- /dev/null +++ b/app/views/shared/milestones/_record.html.haml @@ -0,0 +1,17 @@ +-# @project is present when viewing Project's milestone +- project = @project || record.project + +%li{ id: dom_id(record, 'sortable'), class: "#{dom_class(record)}-row", 'data-iid' => record.iid, 'data-url' => polymorphic_path(record) } + %span + - if show_project_name + %strong #{project.name} · + - elsif show_full_project_name + %strong #{project.name_with_namespace} · + = link_to_gfm record.title, [project.namespace.becomes(Namespace), project, record], title: record.title + %div{class: "#{dom_class(record)}-detail"} + = link_to [project.namespace.becomes(Namespace), project, record] do + %span{ class: "#{dom_class(record)}-number" } ##{record.iid} + - record.labels.each do |label| + = render_colored_label(label) + - if record.assignee + = image_tag avatar_icon(record.assignee, 16), class: "avatar s16", alt: '' diff --git a/app/views/shared/milestones/_records.html.haml b/app/views/shared/milestones/_records.html.haml new file mode 100644 index 0000000000..29fd6aaac9 --- /dev/null +++ b/app/views/shared/milestones/_records.html.haml @@ -0,0 +1,14 @@ +- show_counter = local_assigns.fetch(:show_counter, false) +- primary = local_assigns.fetch(:primary, false) +- panel_class = primary ? 'panel-primary' : 'panel-default' + +.panel{ class: panel_class } + .panel-heading + = title + - if show_counter + .pull-right= records.size + + - class_prefix = dom_class(records).pluralize + %ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id } + - records.sort_by(&:position).each do |record| + = render 'shared/milestones/record', record: record, show_project_name: show_project_name, show_full_project_name: show_full_project_name From baa782ac9a29ba8fe162287511cbc9e4810fc4ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 24 Feb 2016 15:37:40 -0500 Subject: [PATCH 045/255] Add some spinach specs. --- features/group/milestones.feature | 18 ++++++++++ features/steps/group/milestones.rb | 55 ++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/features/group/milestones.feature b/features/group/milestones.feature index 62ea66a783..a097e07583 100644 --- a/features/group/milestones.feature +++ b/features/group/milestones.feature @@ -28,3 +28,21 @@ Feature: Group Milestones And I fill milestone name When I press create mileston button Then milestone in each project should be created + + Scenario: I should see Issues listed with labels + Given Group has projects with milestones + When I visit group "Owned" page + And I click on group milestones + And I click on one group milestone + Then I should see the "bug" label + And I should see the "feature" label + And I should see the project name in the Issue row + + Scenario: I should see the Labels tab + Given Group has projects with milestones + When I visit group "Owned" page + And I click on group milestones + And I click on one group milestone + And I click on the "Labels" tab + Then I should see the list of labels + And I should see the project name in the Label row diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb index 2363ad797f..ce4e52181c 100644 --- a/features/steps/group/milestones.rb +++ b/features/steps/group/milestones.rb @@ -24,6 +24,9 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps end step 'I click on one group milestone' do + milestones = Milestone.where(title: 'GL-113') + @global_milestone = GlobalMilestone.new('GL-113', milestones) + click_link 'GL-113' end @@ -33,7 +36,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps step 'I should see group milestone with all issues and MRs assigned to that milestone' do expect(page).to have_content('Milestone GL-113') - expect(page).to have_content('Progress: 0 closed – 3 open') + expect(page).to have_content('3 issues: 3 open and 0 closed') issue = Milestone.find_by(name: 'GL-113').issues.first expect(page).to have_link(issue.title, href: namespace_project_issue_path(issue.project.namespace, issue.project, issue)) end @@ -60,6 +63,47 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps end end + step 'I should see the "bug" label' do + page.within('#tab-issues') do + expect(page).to have_content 'bug' + end + end + + step 'I should see the "feature" label' do + page.within('#tab-issues') do + expect(page).to have_content 'bug' + end + end + + step 'I should see the project name in the Issue row' do + page.within('#tab-issues') do + @global_milestone.projects.each do |project| + expect(page).to have_content project.name + end + end + end + + step 'I click on the "Labels" tab' do + page.within('.nav-links') do + page.find(:xpath, "//a[@href='#tab-labels']").click + end + end + + step 'I should see the list of labels' do + page.within('#tab-labels') do + expect(page).to have_content 'bug' + expect(page).to have_content 'feature' + end + end + + step 'I should see the project name in the Label row' do + page.within('#tab-labels') do + @global_milestone.projects.each do |project| + expect(page).to have_content project.name + end + end + end + private def group_milestone @@ -68,6 +112,10 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps %w(gitlabhq gitlab-ci cookbook-gitlab).each do |path| project = create :project, path: path, group: group milestone = create :milestone, title: "Version 7.2", project: project + + create(:label, project: project, title: 'bug') + create(:label, project: project, title: 'feature') + create :issue, project: project, assignee: current_user, @@ -80,11 +128,14 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps due_date: '2114-08-20', description: 'Lorem Ipsum is simply dummy text' - create :issue, + issue = create :issue, project: project, assignee: current_user, author: current_user, milestone: milestone + + issue.labels << project.labels.find_by(title: 'bug') + issue.labels << project.labels.find_by(title: 'feature') end end end From c91554de09cb2b19e1403fdf50f691004e6befdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Wed, 24 Feb 2016 17:45:49 -0500 Subject: [PATCH 046/255] Add link with filter by milestone for labels and avatar. Closes #13628 --- app/views/shared/milestones/_record.html.haml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/views/shared/milestones/_record.html.haml b/app/views/shared/milestones/_record.html.haml index a753dd78a6..f82f2132bb 100644 --- a/app/views/shared/milestones/_record.html.haml +++ b/app/views/shared/milestones/_record.html.haml @@ -1,5 +1,7 @@ -# @project is present when viewing Project's milestone - project = @project || record.project +- record_type = record.class.table_name +- base_url_args = [project.namespace.becomes(Namespace), project, record_type] %li{ id: dom_id(record, 'sortable'), class: "#{dom_class(record)}-row", 'data-iid' => record.iid, 'data-url' => polymorphic_path(record) } %span @@ -11,7 +13,11 @@ %div{class: "#{dom_class(record)}-detail"} = link_to [project.namespace.becomes(Namespace), project, record] do %span{ class: "#{dom_class(record)}-number" } ##{record.iid} + - record.labels.each do |label| - = render_colored_label(label) + %a{ href: polymorphic_path(base_url_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) }< + = render_colored_label(label) + - if record.assignee - = image_tag avatar_icon(record.assignee, 16), class: "avatar s16", alt: '' + %a{ href: polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: record.assignee_id, state: 'all' }) } + = image_tag(avatar_icon(record.assignee, 16), class: "avatar s16", alt: '') From 7a311c23494b26d5212cac98318273fc9d2c69d1 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Sat, 5 Mar 2016 11:51:56 +0000 Subject: [PATCH 047/255] 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 d2c7cdc3e9..3dc524ccca 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 daaac092dc..e7e04621ff 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 From 836d5930332797192094ce4a3c8083e96f7e8c53 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sat, 5 Mar 2016 17:17:49 -0500 Subject: [PATCH 048/255] 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 07b5759443..a41172816b 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -4,7 +4,7 @@ class SnippetsFinder case filter when :all then - snippets(current_user).fresh.non_expired + snippets(current_user).fresh when :by_user then by_user(current_user, params[:user], params[:scope]) when :by_project @@ -27,7 +27,7 @@ class SnippetsFinder end def by_user(current_user, user, scope) - snippets = user.snippets.fresh.non_expired + snippets = user.snippets.fresh return snippets.are_public unless current_user @@ -48,7 +48,7 @@ class SnippetsFinder end def by_project(current_user, project) - snippets = project.snippets.fresh.non_expired + snippets = project.snippets.fresh if current_user if project.team.member?(current_user.id) diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 41ae404899..0a5a8eb5ae 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 9cee3b70cb..452f3913ee 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 9e2c1b0e18..1f7d85a5f3 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 f876be7a4c..dd3925c7a7 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -10,7 +10,6 @@ # created_at :datetime # updated_at :datetime # file_name :string(255) -# expires_at :datetime # type :string(255) # visibility_level :integer default(0), not null # @@ -46,8 +45,6 @@ class Snippet < ActiveRecord::Base scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) } scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) } scope :fresh, -> { order("created_at DESC") } - scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) } - scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) } participant :author, :notes @@ -111,10 +108,6 @@ class Snippet < ActiveRecord::Base nil end - def expired? - expires_at && expires_at < Time.current - end - def visibility_level_field visibility_level end diff --git a/db/migrate/20160305220806_remove_expires_at_from_snippets.rb b/db/migrate/20160305220806_remove_expires_at_from_snippets.rb new file mode 100644 index 0000000000..fc12b5b09e --- /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 71d9257a31..2d6b9b5a4c 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 @@ ActiveRecord::Schema.define(version: 20160222153918) do t.datetime "created_at" t.datetime "updated_at" t.string "file_name" - t.datetime "expires_at" t.string "type" t.integer "visibility_level", default: 0, null: false end @@ -785,7 +784,6 @@ ActiveRecord::Schema.define(version: 20160222153918) do add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree add_index "snippets", ["created_at", "id"], name: "index_snippets_on_created_at_and_id", using: :btree add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree - add_index "snippets", ["expires_at"], name: "index_snippets_on_expires_at", using: :btree add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree add_index "snippets", ["updated_at"], name: "index_snippets_on_updated_at", using: :btree add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree diff --git a/doc/api/notes.md b/doc/api/notes.md index d4d63e825a..85d4f0bafa 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 a7acf37b5b..fb802102e3 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 b82306bd1d..e2b53c45ab 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 b021db8fa5..aa3d8fe843 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -141,7 +141,7 @@ module API class ProjectSnippet < Grape::Entity expose :id, :title, :file_name expose :author, using: Entities::UserBasic - expose :expires_at, :updated_at, :created_at + expose :updated_at, :created_at end class ProjectEntity < Grape::Entity diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb index cc92eb0bd9..e0feb606f7 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 eb2dbbdc5a..7e5b5499ae 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 # From 95b06a62c0db5f8c285a1d24fa1994e10c70ff27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Sun, 6 Mar 2016 23:07:19 -0500 Subject: [PATCH 049/255] Updates from last code review. --- CHANGELOG | 1 + app/assets/javascripts/dispatcher.js.coffee | 2 +- app/assets/javascripts/milestone.js.coffee | 7 +- app/assets/stylesheets/pages/milestone.scss | 14 +-- .../projects/milestones_controller.rb | 4 - app/decorators/label_with_milestone.rb | 19 ---- app/finders/issuable_finder.rb | 6 +- app/helpers/milestones_helper.rb | 26 ++++++ app/models/concerns/issuable.rb | 2 + app/models/concerns/milestoneish.rb | 25 +++++ app/models/global_label.rb | 7 +- app/models/global_milestone.rb | 53 +---------- app/models/merge_request.rb | 1 - app/models/milestone.rb | 27 +----- app/models/project.rb | 2 +- .../dashboard/milestones/_milestone.html.haml | 31 ++----- app/views/dashboard/milestones/show.html.haml | 86 +---------------- .../groups/milestones/_milestone.html.haml | 34 +------ app/views/groups/milestones/show.html.haml | 93 +------------------ .../projects/milestones/_milestone.html.haml | 36 +------ app/views/projects/milestones/show.html.haml | 59 +----------- .../shared/milestones/_issuable.html.haml | 25 +++++ ...records.html.haml => _issuables.html.haml} | 10 +- .../shared/milestones/_issues_tab.html.haml | 6 +- .../shared/milestones/_labels_tab.html.haml | 30 +++--- .../milestones/_merge_requests_tab.haml | 8 +- .../shared/milestones/_milestone.html.haml | 45 +++++++++ app/views/shared/milestones/_record.html.haml | 23 ----- .../shared/milestones/_summary.html.haml | 28 ++++++ app/views/shared/milestones/_tabs.html.haml | 30 ++++++ app/views/shared/milestones/_top.html.haml | 58 ++++++++++++ config/application.rb | 1 - features/group/milestones.feature | 1 - features/steps/group/milestones.rb | 8 -- features/steps/project/issues/milestones.rb | 2 +- spec/models/milestone_spec.rb | 3 +- 36 files changed, 317 insertions(+), 496 deletions(-) delete mode 100644 app/decorators/label_with_milestone.rb create mode 100644 app/models/concerns/milestoneish.rb create mode 100644 app/views/shared/milestones/_issuable.html.haml rename app/views/shared/milestones/{_records.html.haml => _issuables.html.haml} (52%) create mode 100644 app/views/shared/milestones/_milestone.html.haml delete mode 100644 app/views/shared/milestones/_record.html.haml create mode 100644 app/views/shared/milestones/_summary.html.haml create mode 100644 app/views/shared/milestones/_tabs.html.haml create mode 100644 app/views/shared/milestones/_top.html.haml diff --git a/CHANGELOG b/CHANGELOG index 8692f61fe3..bba032036e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ v 8.6.0 (unreleased) - Allow search for logged out users - 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 v 8.5.4 - Do not cache requests for badges (including builds badge) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index d7feb5d5c8..54b28f2dd8 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -23,7 +23,7 @@ class Dispatcher new Issue() shortcut_handler = new ShortcutsIssuable() new ZenMode() - when 'projects:milestones:show' + when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show' new Milestone() when 'projects:milestones:new', 'projects:milestones:edit' new ZenMode() diff --git a/app/assets/javascripts/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee index e6d8518bec..0037a3a21c 100644 --- a/app/assets/javascripts/milestone.js.coffee +++ b/app/assets/javascripts/milestone.js.coffee @@ -69,7 +69,7 @@ class @Milestone @bindIssuesSorting() @bindMergeRequestSorting() - @bindTabsSwitching + @bindTabsSwitching() bindIssuesSorting: -> $("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable( @@ -104,7 +104,7 @@ class @Milestone ).disableSelection() - bindMergeRequestSorting: -> + bindTabsSwitching: -> $('a[data-toggle="tab"]').on 'show.bs.tab', (e) -> currentTabClass = $(e.target).data('show') previousTabClass = $(e.relatedTarget).data('show') @@ -112,7 +112,8 @@ class @Milestone $(previousTabClass).hide() $(currentTabClass).removeClass('hidden') $(currentTabClass).show() - + + bindMergeRequestSorting: -> $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable( connectWith: ".merge_requests-sortable-list", dropOnEmpty: true, diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 300c2fa7e1..d0e72a4422 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -19,10 +19,11 @@ li.milestone { width: 105px; } - .issue-row, .merge_request-row { + .issuable-row { .color-label { border-radius: 2px; padding: 3px !important; + margin-right: 7px; } // Issue title @@ -45,19 +46,14 @@ li.milestone { } .issues-sortable-list, .merge_requests-sortable-list { - .issue-detail, .merge_request-detail { + .issuable-detail { display: block; + margin-top: 7px; - .issue-number, .merge_request-number { + .issuable-number { color: rgba(0,0,0,0.44); margin-right: 5px; } - .color-label { - padding: 6px 10px; - margin-right: 7px; - margin-top: 10px; - } - .avatar { float: none; } diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 21f30f278c..da46731d94 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -32,10 +32,6 @@ class Projects::MilestonesController < Projects::ApplicationController end def show - @issues = @milestone.issues - @users = @milestone.participants.uniq - @merge_requests = @milestone.merge_requests - @labels = @milestone.labels end def create diff --git a/app/decorators/label_with_milestone.rb b/app/decorators/label_with_milestone.rb deleted file mode 100644 index a70a4e2f50..0000000000 --- a/app/decorators/label_with_milestone.rb +++ /dev/null @@ -1,19 +0,0 @@ -class LabelWithMilestone - attr_reader :milestone - - def initialize(label, milestone) - @label, @milestone = label, milestone - end - - def method_missing(meth, *args) - if @label.respond_to?(meth) - @label.send(meth, *args) - else - super - end - end - - def respond_to?(meth) - @label.respond_to?(meth) - end -end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index f7240edd61..c88a420b41 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -263,11 +263,9 @@ class IssuableFinder def by_label(items) if labels? if filter_by_no_label? - items = items. - joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{klass.name}' AND label_links.target_id = #{klass.table_name}.id"). - where(label_links: { id: nil }) + items = items.without_label else - items = items.joins(:labels).where(labels: { title: label_names }) + items = items.with_label(label_names) if projects items = items.where(labels: { project_id: projects }) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 7de81d8dfd..e3e7daa49c 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -9,6 +9,32 @@ module MilestonesHelper end end + def milestones_label_path(opts = {}) + if @project + namespace_project_issues_path(@project.namespace, @project, opts) + elsif @group + issues_group_path(@group, opts) + else + issues_dashboard_path(opts) + end + end + + def milestones_browse_issuables_path(milestone, type:) + opts = { milestone_title: milestone.title } + + if @project + polymorphic_path([@project.namespace.becomes(Namespace), @project, type], opts) + elsif @group + polymorphic_url([type, @group], opts) + else + polymorphic_url([type, :dashboard], opts) + end + end + + def milestone_issues_by_label_count(milestone, label, state:) + milestone.issues.with_label(label.title).send(state).size + end + def milestone_progress_bar(milestone) options = { class: 'progress-bar progress-bar-success', diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index a3c4a3d277..27b97944e3 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -36,6 +36,8 @@ module Issuable scope :closed, -> { with_state(:closed) } scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') } scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') } + scope :with_label, ->(title) { joins(:labels).where(labels: { title: title }) } + scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } scope :join_project, -> { joins(:project) } scope :references_project, -> { references(:project) } diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb new file mode 100644 index 0000000000..d67df7c1d9 --- /dev/null +++ b/app/models/concerns/milestoneish.rb @@ -0,0 +1,25 @@ +module Milestoneish + def closed_items_count + issues.closed.size + merge_requests.closed_and_merged.size + end + + def total_items_count + issues.size + merge_requests.size + end + + def complete? + total_items_count == closed_items_count + end + + def percent_complete + ((closed_items_count * 100) / total_items_count).abs + rescue ZeroDivisionError + 0 + end + + def remaining_days + return 0 if !due_date || expired? + + (due_date - Date.today).to_i + end +end diff --git a/app/models/global_label.rb b/app/models/global_label.rb index 0171f7d54b..ddd4bad5c2 100644 --- a/app/models/global_label.rb +++ b/app/models/global_label.rb @@ -2,16 +2,19 @@ class GlobalLabel attr_accessor :title, :labels alias_attribute :name, :title + delegate :color, :description, to: :@first_label + def self.build_collection(labels) labels = labels.group_by(&:title) - labels.map do |title, label| - new(title, label) + labels.map do |title, labels| + new(title, labels) end end def initialize(title, labels) @title = title @labels = labels + @first_label = labels.find { |lbl| lbl.description.present? } || labels.first end end diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index e13aaf1673..97bd79af08 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -1,4 +1,6 @@ class GlobalMilestone + include Milestoneish + attr_accessor :title, :milestones alias_attribute :name, :title @@ -31,32 +33,6 @@ class GlobalMilestone @projects ||= Project.for_milestones(milestones.map(&:id)) end - def issues_count - issues.count - end - - def merge_requests_count - merge_requests.count - end - - def open_items_count - opened_issues.count + opened_merge_requests.count - end - - def closed_items_count - closed_issues.count + closed_merge_requests.count - end - - def total_items_count - issues_count + merge_requests_count - end - - def percent_complete - ((closed_items_count * 100) / total_items_count).abs - rescue ZeroDivisionError - 0 - end - def state state = milestones.map { |milestone| milestone.state } @@ -88,29 +64,8 @@ class GlobalMilestone end def labels - @labels ||= milestones.map do |ms| - ms.labels.map { |label| LabelWithMilestone.new(label, ms) } - end.flatten.sort_by!(&:title) - end - - def opened_issues - issues.opened - end - - def closed_issues - issues.closed - end - - def opened_merge_requests - merge_requests.opened - end - - def closed_merge_requests - merge_requests.with_states(:closed, :merged, :locked) - end - - def complete? - total_items_count == closed_items_count + @labels ||= GlobalLabel.build_collection(milestones.map(&:labels).flatten) + .sort_by!(&:title) end def due_date diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index f575494e2b..0c1a47b3f6 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -137,7 +137,6 @@ class MergeRequest < ActiveRecord::Base scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) } scope :of_projects, ->(ids) { where(target_project_id: ids) } - scope :opened, -> { with_states(:opened, :reopened) } scope :merged, -> { with_state(:merged) } scope :closed_and_merged, -> { with_states(:closed, :merged) } diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 7dc2f909b2..e3969f32dd 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -24,12 +24,13 @@ class Milestone < ActiveRecord::Base include Sortable include Referable include StripAttribute + include Milestoneish belongs_to :project has_many :issues has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues has_many :merge_requests - has_many :participants, through: :issues, source: :assignee + has_many :participants, -> { distinct.reorder('users.name') }, through: :issues, source: :assignee scope :active, -> { with_state(:active) } scope :closed, -> { with_state(:closed) } @@ -92,30 +93,6 @@ class Milestone < ActiveRecord::Base end end - def open_items_count - self.issues.opened.count + self.merge_requests.opened.count - end - - def closed_items_count - self.issues.closed.count + self.merge_requests.closed_and_merged.count - end - - def total_items_count - self.issues.count + self.merge_requests.count - end - - def percent_complete - ((closed_items_count * 100) / total_items_count).abs - rescue ZeroDivisionError - 0 - end - - def remaining_days - return 0 if !due_date || expired? - - (due_date - Date.today).to_i - end - def expires_at if due_date if due_date.past? diff --git a/app/models/project.rb b/app/models/project.rb index 3a28d5d7fe..3235a1cee5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -215,7 +215,7 @@ class Project < ActiveRecord::Base scope :public_only, -> { where(visibility_level: Project::PUBLIC) } scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) } scope :non_archived, -> { where(archived: false) } - scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids) } + scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct } state_machine :import_status, initial: :none do event :import_start do diff --git a/app/views/dashboard/milestones/_milestone.html.haml b/app/views/dashboard/milestones/_milestone.html.haml index ea6c304d7d..6173ca6ab9 100644 --- a/app/views/dashboard/milestones/_milestone.html.haml +++ b/app/views/dashboard/milestones/_milestone.html.haml @@ -1,25 +1,6 @@ -%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) } - .row - .col-sm-6 - %strong - = link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title) - .col-sm-6 - .pull-right.light #{milestone.percent_complete}% complete - .row - .col-sm-6 - = link_to issues_dashboard_path(milestone_title: milestone.title) do - = pluralize milestone.issues_count, 'Issue' - · - = link_to merge_requests_dashboard_path(milestone_title: milestone.title) do - = pluralize milestone.merge_requests_count, 'Merge Request' - .col-sm-6 - = milestone_progress_bar(milestone) - .row - .col-sm-6 - .expiration - = render 'shared/milestone_expired', milestone: milestone - .projects - - milestone.milestones.each do |milestone| - = link_to milestone_path(milestone) do - %span.label.label-gray - = milestone.project.name_with_namespace += render 'shared/milestones/milestone', + milestone_path: dashboard_milestone_path(milestone.safe_title, title: milestone.title), + issues_path: issues_dashboard_path(milestone_title: milestone.title), + merge_requests_path: merge_requests_dashboard_path(milestone_title: milestone.title), + milestone: milestone, + dashboard: true diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml index 88edc025c6..60c84a2642 100644 --- a/app/views/dashboard/milestones/show.html.haml +++ b/app/views/dashboard/milestones/show.html.haml @@ -1,85 +1,5 @@ -- page_title @milestone.title, "Milestones" - header_title "Milestones", dashboard_milestones_path -.detail-page-header - .status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" } - - if @milestone.closed? - Closed - - else - Open - %span.identifier - Milestone #{@milestone.title} - -.detail-page-description.gray-content-block.second-block - %h2.title - = markdown escape_once(@milestone.title), pipeline: :single_line - -- if @milestone.complete? && @milestone.active? - .alert.alert-success.prepend-top-default - %span All issues for this milestone are closed. Navigate to the project to close the milestone. - -.table-holder - %table.table - %thead - %tr - %th Project - %th Open issues - %th State - %th Due date - - @milestone.milestones.each do |milestone| - %tr - %td - = link_to "#{milestone.project.name_with_namespace}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) - %td - = milestone.issues.opened.count - %td - - if milestone.closed? - Closed - - else - Open - %td - = milestone.expires_at - -.context - .milestone-summary - %h4 Progress - %strong= @milestone.issues.size - issues: - %span.milestone-stat - %strong= @milestone.opened_issues.size - open and - %strong= @milestone.closed_issues.size - closed - %span.milestone-stat - %strong== #{@milestone.percent_complete}% - complete - - = milestone_progress_bar(@milestone) - -%ul.nav-links.no-top.no-bottom - %li.active - = link_to '#tab-issues', 'data-toggle' => 'tab' do - Issues - %span.badge= @milestone.issues_count - %li - = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do - Merge Requests - %span.badge= @milestone.merge_requests_count - %li - = link_to '#tab-participants', 'data-toggle' => 'tab' do - Participants - %span.badge= @milestone.participants.count - %li - = link_to '#tab-labels', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do - Labels - %span.badge= @milestone.labels.count - -.tab-content.milestone-content - .tab-pane.active#tab-issues - = render 'shared/milestones/issues_tab', unassigned: @milestone.opened_issues.unassigned, assigned: @milestone.opened_issues.assigned, closed: @milestone.closed_issues, show_full_project_name: true - .tab-pane#tab-merge-requests - = render 'shared/milestones/merge_requests_tab', unassigned: @milestone.opened_merge_requests.unassigned, assigned: @milestone.opened_merge_requests.assigned, closed: @milestone.merge_requests.closed, merged: @milestone.merge_requests.merged, show_full_project_name: true - .tab-pane#tab-participants - = render 'shared/milestones/participants_tab', users: @milestone.participants - .tab-pane#tab-labels - = render 'shared/milestones/labels_tab', labels: @milestone.labels, show_full_project_name: true += render 'shared/milestones/top', milestone: @milestone += render 'shared/milestones/summary', milestone: @milestone += render 'shared/milestones/tabs', milestone: @milestone, show_full_project_name: true diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml index 50558d7dce..4c4e0a2672 100644 --- a/app/views/groups/milestones/_milestone.html.haml +++ b/app/views/groups/milestones/_milestone.html.haml @@ -1,29 +1,5 @@ -%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) } - .row - .col-sm-6 - %strong - = link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title) - .col-sm-6 - .pull-right.light #{milestone.percent_complete}% complete - .row - .col-sm-6 - = link_to issues_group_path(@group, milestone_title: milestone.title) do - = pluralize milestone.issues_count, 'Issue' - · - = link_to merge_requests_group_path(@group, milestone_title: milestone.title) do - = pluralize milestone.merge_requests_count, 'Merge Request' - .col-sm-6 - = milestone_progress_bar(milestone) - .row - .col-sm-6 - %div - - milestone.milestones.each do |milestone| - = link_to milestone_path(milestone) do - %span.label.label-gray - = milestone.project.name - .col-sm-6 - - if can?(current_user, :admin_milestones, @group) - - if milestone.closed? - = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen" - - else - = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-xs btn-close" += render 'shared/milestones/milestone', + milestone_path: group_milestone_path(@group, milestone.safe_title, title: milestone.title), + issues_path: issues_group_path(@group, milestone_title: milestone.title), + merge_requests_path: merge_requests_group_path(@group, milestone_title: milestone.title), + milestone: milestone diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index bd61e50a5b..fb6f0da28f 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -1,91 +1,4 @@ -- page_title @milestone.title, "Milestones" = render "header_title" - -.detail-page-header - .status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" } - - if @milestone.closed? - Closed - - else - Open - %span.identifier - Milestone #{@milestone.title} - .pull-right - - if can?(current_user, :admin_milestones, @group) - - if @milestone.active? - = link_to 'Close Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close" - - else - = link_to 'Reopen Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" - -.detail-page-description.gray-content-block.second-block - %h2.title - = markdown escape_once(@milestone.title), pipeline: :single_line - -- if @milestone.complete? && @milestone.active? - .alert.alert-success.prepend-top-default - %span All issues for this milestone are closed. You may close the milestone now. - -.table-holder - %table.table - %thead - %tr - %th Project - %th Open issues - %th State - %th Due date - - @milestone.milestones.each do |milestone| - %tr - %td - = link_to "#{milestone.project.name}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) - %td - = milestone.issues.opened.count - %td - - if milestone.closed? - Closed - - else - Open - %td - = milestone.expires_at - -.context - .milestone-summary - %h4 Progress - %strong= @milestone.issues.size - issues: - %span.milestone-stat - %strong= @milestone.opened_issues.size - open and - %strong= @milestone.closed_issues.size - closed - %span.milestone-stat - %strong== #{@milestone.percent_complete}% - complete - - = milestone_progress_bar(@milestone) - -%ul.nav-links.no-top.no-bottom - %li.active - = link_to '#tab-issues', 'data-toggle' => 'tab' do - Issues - %span.badge= @milestone.issues_count - %li - = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do - Merge Requests - %span.badge= @milestone.merge_requests_count - %li - = link_to '#tab-participants', 'data-toggle' => 'tab' do - Participants - %span.badge= @milestone.participants.count - %li - = link_to '#tab-labels', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do - Labels - %span.badge= @milestone.labels.count - -.tab-content.milestone-content - .tab-pane.active#tab-issues - = render 'shared/milestones/issues_tab', unassigned: @milestone.opened_issues.unassigned, assigned: @milestone.opened_issues.assigned, closed: @milestone.closed_issues, show_project_name: true - .tab-pane#tab-merge-requests - = render 'shared/milestones/merge_requests_tab', unassigned: @milestone.opened_merge_requests.unassigned, assigned: @milestone.opened_merge_requests.assigned, closed: @milestone.merge_requests.closed, merged: @milestone.merge_requests.merged, show_project_name: true - .tab-pane#tab-participants - = render 'shared/milestones/participants_tab', users: @milestone.participants - .tab-pane#tab-labels - = render 'shared/milestones/labels_tab', labels: @milestone.labels, show_project_name: true += render 'shared/milestones/top', milestone: @milestone, group: @group += render 'shared/milestones/summary', milestone: @milestone += render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml index 67d95ab036..77b566db6b 100644 --- a/app/views/projects/milestones/_milestone.html.haml +++ b/app/views/projects/milestones/_milestone.html.haml @@ -1,31 +1,5 @@ -%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone) } - .row - .col-sm-6 - %strong - = link_to_gfm truncate(milestone.title, length: 100), namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) - - .col-sm-6 - .pull-right.light #{milestone.percent_complete}% complete - .row - .col-sm-6 - = link_to namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do - = pluralize milestone.issues.count, 'Issue' - · - = link_to namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do - = pluralize milestone.merge_requests.count, 'Merge Request' - .col-sm-6 - = milestone_progress_bar(milestone) - - .row - .col-sm-6 - = render 'shared/milestone_expired', milestone: milestone - .col-sm-6 - - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? - = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs" do - = icon('pencil-square-o') - Edit - \ - = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close" - = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do - = icon('trash-o') - Delete += render 'shared/milestones/milestone', + milestone_path: namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), + issues_path: namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title), + merge_requests_path: namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title), + milestone: milestone diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 4aa1a53e87..b4597043a2 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -42,62 +42,9 @@ = preserve do = markdown @milestone.description -- if @milestone.issues.any? && @milestone.can_be_closed? +- if @milestone.complete? && @milestone.active? .alert.alert-success.prepend-top-default %span All issues for this milestone are closed. You may close milestone now. -.context.prepend-top-default - .milestone-summary - %h4 Progress - %strong= @milestone.issues.count - issues: - %span.milestone-stat - %strong= @milestone.open_items_count - open and - %strong= @milestone.closed_items_count - closed - %span.milestone-stat - %strong== #{@milestone.percent_complete}% - complete - %span.milestone-stat - %span.remaining-days= milestone_remaining_days(@milestone) - %span.pull-right.tab-issues-buttons - - if can?(current_user, :create_issue, @project) - = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do - %i.fa.fa-plus - New Issue - - if can?(current_user, :read_issue, @project) - = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped" - %span.pull-right.tab-merge-requests-buttons.hidden - - if can?(current_user, :read_merge_request, @project) - = link_to 'Browse Merge Requests', namespace_project_merge_requests_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped" - - = milestone_progress_bar(@milestone) - -%ul.nav-links.no-top.no-bottom - %li.active - = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do - Issues - %span.badge= @issues.count - %li - = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do - Merge Requests - %span.badge= @merge_requests.count - %li - = link_to '#tab-participants', 'data-toggle' => 'tab' do - Participants - %span.badge= @users.count - %li - = link_to '#tab-labels', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do - Labels - %span.badge= @labels.count - -.tab-content.milestone-content - .tab-pane.active#tab-issues - = render 'shared/milestones/issues_tab', unassigned: @issues.opened.unassigned, assigned: @issues.opened.assigned, closed: @issues.closed - .tab-pane#tab-merge-requests - = render 'shared/milestones/merge_requests_tab', unassigned: @merge_requests.opened.unassigned, assigned: @merge_requests.opened.assigned, closed: @merge_requests.closed, merged: @merge_requests.merged - .tab-pane#tab-participants - = render 'shared/milestones/participants_tab', users: @users - .tab-pane#tab-labels - = render 'shared/milestones/labels_tab', labels: @labels += render 'shared/milestones/summary', milestone: @milestone, project: @project += render 'shared/milestones/tabs', milestone: @milestone diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml new file mode 100644 index 0000000000..f7c6fc14ad --- /dev/null +++ b/app/views/shared/milestones/_issuable.html.haml @@ -0,0 +1,25 @@ +-# @project is present when viewing Project's milestone +- project = @project || issuable.project +- assignee = issuable.assignee +- issuable_type = issuable.class.table_name +- base_url_args = [project.namespace.becomes(Namespace), project, issuable_type] + +%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row", 'data-iid' => issuable.iid, 'data-url' => polymorphic_path(issuable) } + %span + - if show_project_name + %strong #{project.name} · + - elsif show_full_project_name + %strong #{project.name_with_namespace} · + = link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title + %div{class: 'issuable-detail'} + = link_to [project.namespace.becomes(Namespace), project, issuable] do + %span{ class: 'issuable-number' }>= issuable.to_reference + + - issuable.labels.each do |label| + = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do + - render_colored_label(label) + + - if assignee + = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }), + class: 'has_tooltip', data: { 'original-title' => "Assigned to #{sanitize(assignee.name)}", container: 'body' } do + - image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '') diff --git a/app/views/shared/milestones/_records.html.haml b/app/views/shared/milestones/_issuables.html.haml similarity index 52% rename from app/views/shared/milestones/_records.html.haml rename to app/views/shared/milestones/_issuables.html.haml index 29fd6aaac9..8619939dde 100644 --- a/app/views/shared/milestones/_records.html.haml +++ b/app/views/shared/milestones/_issuables.html.haml @@ -6,9 +6,11 @@ .panel-heading = title - if show_counter - .pull-right= records.size + .pull-right= issuables.size - - class_prefix = dom_class(records).pluralize + - class_prefix = dom_class(issuables).pluralize %ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id } - - records.sort_by(&:position).each do |record| - = render 'shared/milestones/record', record: record, show_project_name: show_project_name, show_full_project_name: show_full_project_name + = render partial: 'shared/milestones/issuable', + collection: issuables.sort_by(&:position), + as: :issuable, + locals: { show_project_name: show_project_name, show_full_project_name: show_full_project_name } diff --git a/app/views/shared/milestones/_issues_tab.html.haml b/app/views/shared/milestones/_issues_tab.html.haml index 44a221114b..a8db7f8a55 100644 --- a/app/views/shared/milestones/_issues_tab.html.haml +++ b/app/views/shared/milestones/_issues_tab.html.haml @@ -3,8 +3,8 @@ .row.prepend-top-default .col-md-4 - = render 'shared/milestones/records', args.merge({ title: 'Unstarted Issues (open and unassigned)', records: unassigned, id: 'unassigned', show_counter: true }) + = render 'shared/milestones/issuables', args.merge(title: 'Unstarted Issues (open and unassigned)', issuables: issues.opened.unassigned, id: 'unassigned', show_counter: true) .col-md-4 - = render 'shared/milestones/records', args.merge({ title: 'Ongoing Issues (open and assigned)', records: assigned, id: 'ongoing', show_counter: true }) + = render 'shared/milestones/issuables', args.merge(title: 'Ongoing Issues (open and assigned)', issuables: issues.opened.assigned, id: 'ongoing', show_counter: true) .col-md-4 - = render 'shared/milestones/records', args.merge({ title: 'Completed Issues (closed)', records: closed, id: 'closed', show_counter: true }) + = render 'shared/milestones/issuables', args.merge(title: 'Completed Issues (closed)', issuables: issues.closed, id: 'closed', show_counter: true) diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml index 88e8e4f900..ba27bafd1b 100644 --- a/app/views/shared/milestones/_labels_tab.html.haml +++ b/app/views/shared/milestones/_labels_tab.html.haml @@ -1,22 +1,18 @@ -- show_project_name = local_assigns.fetch(:show_project_name, false) -- show_full_project_name = local_assigns.fetch(:show_full_project_name, false) - %ul.bordered-list.manage-labels-list - labels.each do |label| - - milestone = @milestone.is_a?(Milestone) ? @milestone : label.milestone + - options = { milestone_title: @milestone.title, label_name: label.title } %li - = render_colored_label(label) - - if show_project_name - %strong · #{milestone.project.name} - - elsif show_full_project_name - %strong · #{milestone.project.name_with_namespace} + %span.label-row + = link_to milestones_label_path(options) do + - render_colored_label(label) + %span.prepend-left-10 + = markdown(label.description, pipeline: :single_line) - - args = [milestone.project.namespace, milestone.project, milestone_title: milestone.title, label_name: label.title] - - options = args.extract_options! - %span.issues-count - = link_to namespace_project_issues_path(*args, options.merge(state: 'opened')) do - = pluralize label.open_issues_count, 'open issue' - %span.issues-count - = link_to namespace_project_issues_path(*args, options.merge(state: 'closed')) do - = pluralize label.closed_issues_count, 'closed issue' + .pull-right + %strong.issues-count + = link_to milestones_label_path(options.merge(state: 'opened')) do + - pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue' + %strong.issues-count + = link_to milestones_label_path(options.merge(state: 'closed')) do + - pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue' diff --git a/app/views/shared/milestones/_merge_requests_tab.haml b/app/views/shared/milestones/_merge_requests_tab.haml index 15b2876c32..c29d8ee673 100644 --- a/app/views/shared/milestones/_merge_requests_tab.haml +++ b/app/views/shared/milestones/_merge_requests_tab.haml @@ -3,10 +3,10 @@ .row.prepend-top-default .col-md-3 - = render 'shared/milestones/records', args.merge({ title: 'Work in progress (open and unassigned)', records: unassigned, id: 'unassigned' }) + = render 'shared/milestones/issuables', args.merge(title: 'Work in progress (open and unassigned)', issuables: merge_requests.opened.unassigned, id: 'unassigned') .col-md-3 - = render 'shared/milestones/records', args.merge({ title: 'Waiting for merge (open and assigned)', records: assigned, id: 'ongoing' }) + = render 'shared/milestones/issuables', args.merge(title: 'Waiting for merge (open and assigned)', issuables: merge_requests.opened.assigned, id: 'ongoing') .col-md-3 - = render 'shared/milestones/records', args.merge({ title: 'Rejected (closed)', records: closed, id: 'closed' }) + = render 'shared/milestones/issuables', args.merge(title: 'Rejected (closed)', issuables: merge_requests.closed, id: 'closed') .col-md-3 - = render 'shared/milestones/records', args.merge({ title: 'Merged', records: merged, id: 'merged', primary: true }) + = render 'shared/milestones/issuables', args.merge(title: 'Merged', issuables: merge_requests.merged, id: 'merged', primary: true) diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml new file mode 100644 index 0000000000..f01138af3f --- /dev/null +++ b/app/views/shared/milestones/_milestone.html.haml @@ -0,0 +1,45 @@ +- dashboard = local_assigns[:dashboard] +- custom_dom_id = dom_id(@project ? milestone : milestone.milestones.first) + +%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id } + .row + .col-sm-6 + %strong= link_to_gfm truncate(milestone.title, length: 100), milestone_path + .col-sm-6 + .pull-right.light #{milestone.percent_complete}% complete + .row + .col-sm-6 + = link_to pluralize(milestone.issues.size, 'Issue'), issues_path + · + = link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path + .col-sm-6= milestone_progress_bar(milestone) + - if milestone.is_a?(GlobalMilestone) + .row + .col-sm-6 + .expiration= render('shared/milestone_expired', milestone: milestone) + .projects + - milestone.milestones.each do |milestone| + = link_to milestone_path(milestone) do + %span.label.label-gray + = dashboard ? milestone.project.name_with_namespace : milestone.project.name + - if @group + .col-sm-6 + - if can?(current_user, :admin_milestones, @group) + - if milestone.closed? + = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen" + - else + = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-xs btn-close" + + - if @project + .row + .col-sm-6= render('shared/milestone_expired', milestone: milestone) + .col-sm-6 + - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? + = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs" do + = icon('pencil-square-o') + Edit + \ + = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close" + = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do + = icon('trash-o') + Delete diff --git a/app/views/shared/milestones/_record.html.haml b/app/views/shared/milestones/_record.html.haml deleted file mode 100644 index f82f2132bb..0000000000 --- a/app/views/shared/milestones/_record.html.haml +++ /dev/null @@ -1,23 +0,0 @@ --# @project is present when viewing Project's milestone -- project = @project || record.project -- record_type = record.class.table_name -- base_url_args = [project.namespace.becomes(Namespace), project, record_type] - -%li{ id: dom_id(record, 'sortable'), class: "#{dom_class(record)}-row", 'data-iid' => record.iid, 'data-url' => polymorphic_path(record) } - %span - - if show_project_name - %strong #{project.name} · - - elsif show_full_project_name - %strong #{project.name_with_namespace} · - = link_to_gfm record.title, [project.namespace.becomes(Namespace), project, record], title: record.title - %div{class: "#{dom_class(record)}-detail"} - = link_to [project.namespace.becomes(Namespace), project, record] do - %span{ class: "#{dom_class(record)}-number" } ##{record.iid} - - - record.labels.each do |label| - %a{ href: polymorphic_path(base_url_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) }< - = render_colored_label(label) - - - if record.assignee - %a{ href: polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: record.assignee_id, state: 'all' }) } - = image_tag(avatar_icon(record.assignee, 16), class: "avatar s16", alt: '') diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml new file mode 100644 index 0000000000..59d4ae29f7 --- /dev/null +++ b/app/views/shared/milestones/_summary.html.haml @@ -0,0 +1,28 @@ +- project = local_assigns[:project] + +.context.prepend-top-default + .milestone-summary + %h4 Progress + %strong= milestone.issues.size + issues: + %span.milestone-stat + %strong= milestone.issues.opened.size + open and + %strong= milestone.issues.closed.size + closed + %span.milestone-stat + %strong== #{milestone.percent_complete}% + complete + + %span.milestone-stat + %span.remaining-days= milestone_remaining_days(milestone) + %span.pull-right.tab-issues-buttons + - if project && can?(current_user, :create_issue, project) + = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn btn-grouped", title: "New Issue" do + %i.fa.fa-plus + New Issue + = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn btn-grouped" + %span.pull-right.tab-merge-requests-buttons.hidden + = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn btn-grouped" + + = milestone_progress_bar(milestone) diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml new file mode 100644 index 0000000000..57d7ee85a3 --- /dev/null +++ b/app/views/shared/milestones/_tabs.html.haml @@ -0,0 +1,30 @@ +%ul.nav-links.no-top.no-bottom + %li.active + = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do + Issues + %span.badge= milestone.issues.size + %li + = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do + Merge Requests + %span.badge= milestone.merge_requests.size + %li + = link_to '#tab-participants', 'data-toggle' => 'tab' do + Participants + %span.badge= milestone.participants.count + %li + = link_to '#tab-labels', 'data-toggle' => 'tab' do + Labels + %span.badge= milestone.labels.count + +- show_project_name = local_assigns.fetch(:show_project_name, false) +- show_full_project_name = local_assigns.fetch(:show_full_project_name, false) + +.tab-content.milestone-content + .tab-pane.active#tab-issues + = render 'shared/milestones/issues_tab', issues: milestone.issues, show_project_name: show_project_name, show_full_project_name: show_full_project_name + .tab-pane#tab-merge-requests + = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name + .tab-pane#tab-participants + = render 'shared/milestones/participants_tab', users: milestone.participants + .tab-pane#tab-labels + = render 'shared/milestones/labels_tab', labels: milestone.labels diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml new file mode 100644 index 0000000000..4cf1d948b5 --- /dev/null +++ b/app/views/shared/milestones/_top.html.haml @@ -0,0 +1,58 @@ +- page_title milestone.title, "Milestones" + +- group = local_assigns[:group] + +.detail-page-header + .status-box{ class: "status-box-#{milestone.closed? ? 'closed' : 'open'}" } + - if milestone.closed? + Closed + - elsif milestone.expired? + Expired + - else + Open + %span.identifier + Milestone #{milestone.title} + - if milestone.expires_at + %span.creator + · + = milestone.expires_at + - if group + .pull-right + - if can?(current_user, :admin_milestones, group) + - if milestone.active? + = link_to 'Close Milestone', group_milestone_path(group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close" + - else + = link_to 'Reopen Milestone', group_milestone_path(group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" + +.detail-page-description.gray-content-block.second-block + %h2.title + = markdown escape_once(milestone.title), pipeline: :single_line + +- if milestone.complete? && milestone.active? + .alert.alert-success.prepend-top-default + - close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.' + %span All issues for this milestone are closed. #{close_msg} + +.table-holder + %table.table + %thead + %tr + %th Project + %th Open issues + %th State + %th Due date + - milestone.milestones.each do |ms| + %tr + %td + - project_name = group ? ms.project.name : ms.project.name_with_namespace + = link_to project_name, namespace_project_milestone_path(ms.project.namespace, ms.project, ms) + %td + = ms.issues.opened.count + %td + - if ms.closed? + Closed + - else + Open + %td + = ms.expires_at + diff --git a/config/application.rb b/config/application.rb index fee8637a4c..28684a3e57 100644 --- a/config/application.rb +++ b/config/application.rb @@ -15,7 +15,6 @@ module Gitlab # Custom directories with classes and modules you want to be autoloadable. config.autoload_paths.push(*%W(#{config.root}/lib - #{config.root}/app/decorators #{config.root}/app/models/hooks #{config.root}/app/models/concerns #{config.root}/app/models/project_services diff --git a/features/group/milestones.feature b/features/group/milestones.feature index a097e07583..d6c05df984 100644 --- a/features/group/milestones.feature +++ b/features/group/milestones.feature @@ -45,4 +45,3 @@ Feature: Group Milestones And I click on one group milestone And I click on the "Labels" tab Then I should see the list of labels - And I should see the project name in the Label row diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb index ce4e52181c..a167d25983 100644 --- a/features/steps/group/milestones.rb +++ b/features/steps/group/milestones.rb @@ -96,14 +96,6 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps end end - step 'I should see the project name in the Label row' do - page.within('#tab-labels') do - @global_milestone.projects.each do |project| - expect(page).to have_content project.name - end - end - end - private def group_milestone diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb index e2eda51149..4faa0f4707 100644 --- a/features/steps/project/issues/milestones.rb +++ b/features/steps/project/issues/milestones.rb @@ -59,7 +59,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps end step 'I should see 3 issues' do - expect(page).to have_selector('#tab-issues li.issue-row', count: 4) + expect(page).to have_selector('#tab-issues li.issuable-row', count: 4) end step 'I click link to remove milestone' do diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 1b1380ce4e..28f13100d1 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -60,7 +60,7 @@ describe Milestone, models: true do end it "should recover from dividing by zero" do - expect(milestone.issues).to receive(:count).and_return(0) + expect(milestone.issues).to receive(:size).and_return(0) expect(milestone.percent_complete).to eq(0) end end @@ -114,7 +114,6 @@ describe Milestone, models: true do end it { expect(milestone.closed_items_count).to eq(1) } - it { expect(milestone.open_items_count).to eq(2) } it { expect(milestone.total_items_count).to eq(3) } it { expect(milestone.is_empty?).to be_falsey } end From 823633325833a6449be9351cec7d78788274d57f Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 7 Mar 2016 10:41:49 +0100 Subject: [PATCH 050/255] 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 5fe4d91883..62e5f34677 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 From d51877cac821d17bd79c9c0efbd8e15b7f4c09a3 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 7 Mar 2016 10:42:27 +0100 Subject: [PATCH 051/255] 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 62e5f34677..c0988eca6d 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 998251094a..787fe443ac 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. From b9168b0759f36c4c1c19404fb3ab73a06fcdbeb3 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 7 Mar 2016 09:44:55 +0000 Subject: [PATCH 052/255] 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 0000000000..e110eece16 --- /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 d7feb5d5c8..f5e5c7c75c 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 3c2997c1d5..75f298019e 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 8eec78a557..be7cc0f256 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 From a215e2ee8ddaefbfef16669ad0bd8ccd6853e163 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 7 Mar 2016 14:11:38 +0100 Subject: [PATCH 053/255] 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 aba48f87be..148eab692f 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 @@ -545,9 +544,9 @@ class Project < ActiveRecord::Base 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 From 41bc9c463c187396e47b4a942965de6ecddca5a1 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 7 Mar 2016 14:27:53 +0100 Subject: [PATCH 054/255] 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 6de7888888..a6bebc46b0 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -7,8 +7,9 @@ class Projects::AvatarsController < Projects::ApplicationController @blob = @repository.blob_at_branch('master', @project.avatar_in_git) if @blob headers['X-Content-Type-Options'] = 'nosniff' - 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 b6ff08262d..10de0e6053 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -12,8 +12,8 @@ class Projects::RawController < Projects::ApplicationController 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 adb56e49c6..e5c0ed4b7b 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -153,7 +153,10 @@ module BlobHelper 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 @@ module BlobHelper 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 8ee9f3006b..72e6c5fa3f 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 From af24645d5d25642f462dea5bacd81908b852c493 Mon Sep 17 00:00:00 2001 From: Florent Baldino Date: Mon, 7 Mar 2016 15:35:26 +0100 Subject: [PATCH 055/255] 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 a355b62260..e939b632d0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ v 8.6.0 (unreleased) - Allow search for logged out users - Don't show Issues/MRs from archived projects in Groups view - Increase the notes polling timeout over time (Roberto Dip) + - Add shortcut to toggle markdown preview (Florent Baldino) 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 98fc8f1734..3aa61de2f5 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 9c7c2474aa..100e3aac53 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 82d2d4aabe..e6e3bfbea7 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 From a19b4e752918209a40cddfdf04cb2b7833017939 Mon Sep 17 00:00:00 2001 From: Christian Mehlmauer Date: Mon, 7 Mar 2016 15:39:12 +0100 Subject: [PATCH 056/255] 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 c8083a4547..d323131ab7 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 ce7ae0d285..f446ed0a35 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 ``` From a284d307838adf44080c43e4f553fe60b3495ffe Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 7 Mar 2016 16:49:46 +0100 Subject: [PATCH 057/255] 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 e5c0ed4b7b..0f77b3b299 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -157,27 +157,20 @@ module BlobHelper 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 From 591bf92ce7ee4c8fab9f7d9491be8b9872cf8dc8 Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Mon, 22 Feb 2016 23:59:13 +0000 Subject: [PATCH 058/255] 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 a8dc00ed81..a8a833e641 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,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 3f483847ef..46e51a4bf6 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -76,7 +76,7 @@ module Gitlab project.issues.create!( description: body, title: issue["title"], - state: %w(resolved invalid duplicate wontfix).include?(issue["status"]) ? 'closed' : 'opened', + state: %w(resolved invalid duplicate wontfix closed).include?(issue["status"]) ? 'closed' : 'opened', author_id: gl_user_id(project, reporter) ) end diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb new file mode 100644 index 0000000000..c413132abe --- /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 From f05c22295dbaa7eef769a232545ead6bb1d6c08e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 7 Mar 2016 15:03:55 -0500 Subject: [PATCH 059/255] 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 c66ef3cffa..62b2e55b53 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 22c86e4ae8..cc5e16c37d 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) From 56c4f22c80b0555d5d7cfbc7f443122a8b70072c Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 7 Mar 2016 15:24:27 -0500 Subject: [PATCH 060/255] 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 89af58dcef..ff21c7d1b8 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 a69089f00c..f8d9fe1854 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 565bf088b4..d9842ccf95 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 8bf423cc64..c19b15bc9e 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 eb6df61b8e..444d6726f9 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 583937ca74..33085dac4e 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -24,7 +24,7 @@ require 'spec_helper' describe Note, models: true do describe 'associations' do it { is_expected.to belong_to(:project) } - it { is_expected.to belong_to(:noteable) } + it { is_expected.to belong_to(:noteable).touch(true) } it { is_expected.to belong_to(:author).class_name('User') } it { is_expected.to have_many(:todos).dependent(:destroy) } From 73535b80b246248e932e6a88c462bc827d510e24 Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Sun, 28 Feb 2016 17:27:22 +0000 Subject: [PATCH 061/255] adds language names to projects list github style --- app/models/project.rb | 9 +++++++++ app/views/shared/projects/_project.html.haml | 3 +++ 2 files changed, 12 insertions(+) diff --git a/app/models/project.rb b/app/models/project.rb index 3235a1cee5..2fdd4bfbbc 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -945,4 +945,13 @@ class Project < ActiveRecord::Base def wiki @wiki ||= ProjectWiki.new(self, self.owner) end + + def main_language + if !empty_repo? + languages = Linguist::Repository.new( + @repository.rugged, + @repository.rugged.head.target_id).languages + return languages.key(languages.values.max) + end + end end diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 99e48e86e3..97cfb76cdb 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -28,6 +28,9 @@ = project.name .controls + - if project.main_language + %span + = project.main_language - if ci_commit %span = render_ci_status(ci_commit) From fe934a2da2be153d7346c90671103a5423b13833 Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Sun, 28 Feb 2016 21:29:27 +0000 Subject: [PATCH 062/255] adds cache to languages list --- app/models/project.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 2fdd4bfbbc..3a41812f3c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -948,10 +948,10 @@ class Project < ActiveRecord::Base def main_language if !empty_repo? - languages = Linguist::Repository.new( - @repository.rugged, - @repository.rugged.head.target_id).languages - return languages.key(languages.values.max) + Rails.cache.fetch([self, "language"]) do + languages = Linguist::Repository.new(@repository.rugged, @repository.rugged.head.target_id).languages + languages.key(languages.values.max) + end end end end From 85de3f69558af88ad0af8716ee6fb7197b2d28df Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Mon, 29 Feb 2016 10:15:53 +0000 Subject: [PATCH 063/255] changes if ! to unless and now asks for language instead of languages --- app/models/project.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 3a41812f3c..92aef8a60f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -947,10 +947,11 @@ class Project < ActiveRecord::Base end def main_language - if !empty_repo? + unless empty_repo? Rails.cache.fetch([self, "language"]) do - languages = Linguist::Repository.new(@repository.rugged, @repository.rugged.head.target_id).languages - languages.key(languages.values.max) + Linguist::Repository.new( + @repository.rugged, + @repository.rugged.head.target_id).language end end end From d9e646a796795de43b23a27b74289f44255810e3 Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Mon, 29 Feb 2016 16:50:59 +0000 Subject: [PATCH 064/255] moves method to repository model --- app/models/project.rb | 8 +------- app/models/repository.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 92aef8a60f..3b565ef809 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -947,12 +947,6 @@ class Project < ActiveRecord::Base end def main_language - unless empty_repo? - Rails.cache.fetch([self, "language"]) do - Linguist::Repository.new( - @repository.rugged, - @repository.rugged.head.target_id).language - end - end + @main_language = repository.main_language end end diff --git a/app/models/repository.rb b/app/models/repository.rb index c135ab61f6..f911611aca 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -812,6 +812,18 @@ class Repository raw_repository.ls_files(actual_ref) end + def main_language + return @main_language unless @main_language.nil? + + unless empty? + @main_language = cache.fetch(:main_language) do + Linguist::Repository.new( + rugged, + rugged.head.target_id).language + end + end + end + private def cache From a14b98f858cadf60c67db4cbd23aa10ae0d5213b Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Tue, 1 Mar 2016 01:55:36 +0000 Subject: [PATCH 065/255] implements project languages saving them onto the database --- app/models/.project.rb.swo | Bin 0 -> 49152 bytes app/models/project.rb | 12 ++++++++++ ...9193553_add_main_language_to_repository.rb | 21 ++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 app/models/.project.rb.swo create mode 100644 db/migrate/20160229193553_add_main_language_to_repository.rb diff --git a/app/models/.project.rb.swo b/app/models/.project.rb.swo new file mode 100644 index 0000000000000000000000000000000000000000..5d264ccfaaa42e0a6f93323aa8baf0a4e1fa7cf3 GIT binary patch literal 49152 zcmeI537A}0b?1w)*?_TGJ_!4h8oR5VR%vIcSi9S{uq?@~#W+F~ud7~nm%29BtCG~V zgbf&Dwh%JG_5`y!fY~O&2@DJlJAolEOt1-S1`GiL1k4te@MUIx=iGbWyYF?ixpAzFbEnoE+<95>)Y66mpK}U@r~LfxgHzi!|ISA{_bK%1;Y__Di%?>q?DwHEj4%o|{QUKh7=f5U`T->1%?zDQea4dAq9pM_zz2gUgtrDYbpKj*cx1EzweX# zzQ6r_hW$R2dw)Os`@xI6ch9%m_hByn19H!A%e{Y~{e8XtzMn0iE2k^ht@izmx%X$< z=Qr8!C)*0U_z$wrue0C(E%*Mu_PG;iloq%3JfVQq`;5@LkbKj zFr>hc0z(Q6DezlKfv{F6e2x4~z@XTVEA4Qv8W0Ka-@q40Td z2WWxw!MD#U6y6A~1y2R%fKNZ9P`DjD4{Qa$Mi6i}cqizA_241kTQK1N1^gkH1Eb)Z zFzp`(uLFm`qruN$^nU_=3_bqo)+z`MZP!P|h)`^}&Mwt+{1Un5NTB=`h)1CZbE3G6418q=j_SZ`GtVQZ!z&P2hc z!2Me&xM1C=(~j%Cc6Ywi?a$0awbE=9)=IswI}`OP_1ah|iL+>wahsY8Nk+Ts{jK`S~|)S&h0V1<`))a>-;w*=>%7igiYvlKY01;I#AosFt2`N6s#ToYNE{xGR%q}6FOSya`c%D9@6A^l(b1?u z74wg5RLYu5I3Fz1Q#rLR5=#L zl&ls_2mMYh>_rvLrjjodVZ!NN)UB9ML3ydrBR0K!j=g~P&^-JWu&GtsjefILEH}cq zR|%`V`ca{1*c-Pk-;F3?jR#}4%rTuXA2=y$Rf7lRMFNeGXpe4^%_``%wKK(GGYY~u zFo{T>QeNm3_8Yy*(Xi21QWeYbY}lnt-F~YTb>m9Z3a1)TZ7jGZsP?n zCb1=}Wr7V(B0nq^ZHIio!O4Z)>TLaJRC8@Xr~|t%-M#0E-Ibk}?cFoEzj9!57jwzz zY_HcTuU_46&9&OcS|u7}REN*;9&=Z4v8ub3%R96)ToN@pQCD8qTh&It76tp;b5Uzs zpE(OWB=rViGgWU+ec|D9bq%kaUz!EUY8n3 zE^aF8Nw-PU^g=8$d7XYalF^uCb6ROs>uf=isL4N$rc#-DvqMjOd9>g1TICeYvS!RnHg5O3)wI1G6^rXOZuEoo<~v@XgxE>2nElj(BAlvkk1}ic^fN(imq_a2=Cd>*{crI$y=dF6 zefbWWpE_+~u2Ivp-9X6`!!~)p+epLmU%s#lR(=z@EW02XrqaxAIdzMMO>prF4H$5C z+39t|>Kq#=*J1O;$p%YG?gujuUA0#-1EB1@GB>_kY+T|_SdEgLG@2uy)JhiaT#Y2m zY1Jyb;3lb^RvP|x#MI3c)y|sfBUk0cscW}gNtDwnhwky(Y17zLe3F?Bs%!(3Eaaa| zXGyn;i-)O2akX3T$X?J7luSpRu*ZbH;N$lKX+de^1%VcJao5*!L0!et$8M1s4?5j? zGwjX>bJ4smZTn?G*Zj)7Qw{o9Uu@bG?1TSmhQZDj`}wBsZwtczZ-U1?6CPOj|Av)P z+yjsQ74T*70q|n*0`Por1Y7{dz`gMO-vD0#F9+9xK4^j&FbysP8$b!H0*?bfLN@Rn z@ILTz@B(lWoB$y}Rk^SZtObt;zeaZOFW_h3i{Q=R_26~j6`%u-fI7GWTmsGj|A=hi z%iv4kzk)vpZw0r5mxCkVnP4y210DqK2kr~Lfehju;LYGo;91~7;QPoSUIT6hH-Y2e zAlM7`fHmMT;1S^A;QPoTJ_7Cp?+3Smmx339>%n2L8Qeu1dVGBkUmkI6*%C^eh zEjV#vljCi#;CD$Rry?7*axz2&QP+vcR7yj#t|&12NQx49l`?rNY_=KB&(C%dCcUJr zDAF$x=o!gpT$KOiQMcVpgs1sE{aDG$Cbkq+URvt4D|IO`+@}#?_PYJZ$uUieTWlfo zX;o|?Y*J%>xy`1!)P_GtbgfLDO+_A6?4H{^=Y^w@%9*naI|5yi`DsCe^Mj|XvFT(& zRS>5)>#cq-iZ>NCe5N#+u)*r36Y;BJB4_#bQU{qILLEn%To}74-r^^dRxRPZQ_*^+ z{S$$E_9^>5?K7@5L}F60H4}lsmgMyaJHy~Y`eDPw6hgCcrO^Xn!$e-JQOR&5*uUq(JwY*??@x!Vv2t(`fslv=$LK3@l#^T( zr{K7bh25Gk`E0DMgg(A-4dg1CzN{b-c-|D&u5((_v{z=v2Du>In08ej7~9MxB5G6 zx{!R*z(LTMjw|gcwW)QSL2$tZDylDNV0Y4ynF`WaacP>iuOufL>6;T8KRGIK-J4?# zilmdx5?vaZsZ1aw1M5hkg^8(D>wYX`M@IHjM4C0mlo}*Y;*w$IcB*8-KORjq_KG9BlT`rv0?)4Bl;kYx<~8P$TqMoh#%A(lth_P?nL`Em%*v2kg24~oNHyKvaX0kx!1bl?#GS!LWZ1aZhmjf%4kZ*W|3MKjmXli%m|6- z;c^ztS*}g9-L22aLY6s!@zX`>gh9O-z4UOYRar#@W6J-35PtlD@bbd{!}jXum;C+- z_%Zl6cq_OaTn`R`KLERc#oq%3JfVQq`;5@Lkj$s zQvh|ZsL@8xvx0=7tRjYX7cIYbH>%IHDjcqtgHih!7JPI(P=z%W5b)*zLZ4B%@sVIM zYUsgwEc|!a>vfU#RMG84?pba(&`w?`TUZ#7;{i0&jOOZ8H$;k{sa4Ud3@KqaGo$h` zR1(Vk5Pe|O<>GDee*0!mt0dZC)n28F1gXMNKU!y2!bV^xf{|}I|GyLd|Cd1ce~zK` z^AGU$9|gC7CYS_I0#5`I_6HJv_!&}QNP!^*h7=f5U`T->1%?zDQea4dAq9pM7*gQ3 zkphAJaDMLn`ybn^+G$;=X*I0AXCMx%KNX1xqawD@h&9CeIu}Iv|Hso-cVcTU{C{ZM z?X~dvuK|a_vp@iT3eW!&Ao~7)0qWo&*bY{Le@0LLZQy1g`utU}3Va_u{C9(SunIf? z{7)1O{uDe9Tn?TJeuQrQ--0{93=kgx=YgM}StxuC{2jO*yan6}t^lH6|4xJ!;s>A% z?m!Rza&RB;S#0@l1Q&pxW8?5g;1Ku&Py}ZIvF-mnxEs6|{0Vp&cqzCF>;V^pi@@)J zGr@g80sH{_ezE)i1o#{90x$u#fX$!;9t^|>z!$+6z{kN$!1KZLKpkuXXMiuz2A>80 z14w(kADrqVeOT0`R8^(Zk7q?nl!|w-8&U<5y#8f5Z{ppEje1M4@P+7ySp}nVdFSr^ z6O+5Q?GiOMwD*iw(V$41fY-#B?oG$}AdN zEDbTCw~=FpaIKGOoM){VOmzv0DQkIJwZ$}5_r#XKe`eFo6Ur;UNrw`eLAxD!1q!<7uND)+SoKhWbX* z#I#Vz%j}5-cndOH63EkjJdRQ<*4SZ#dP)wq6mb=;Wqnd{mX_nes?E3>FeB4FEF1l| zG4&>3BbjlM?S<`DV?L9>!l7-)U1?Bs)!dE;<+A*Q%Vjg5GpQVDLlXMvs?BClClwq` z6zIgbam6MbyE5)e{)8M}+u0K$G!mOc9dVKgn8ao@U8$iue_onm0`* zxssi2+N6QTO5F(e0a3S@_QP1aOt5X=4l$iH=8?&x(F}dlOk@^djB6%Il1XfNYmLD# zoY=Qxu|!l|9QP=>bn*)vGl}sP(iE#c9ro8-_B(Z-&R~&3>~S ztXs3DG*@r5r{=M$FyNBGn)ZI!Cb>0fL#`vCK zAJAh7U!#RE^(o_(!0ZsrdLX;;Ai$DclAsQ~HZF1)%I9M56VHm4av-ij^p8vzeyJ&t zT~vslc~R-N>iAV@agJEQx}q$Hh?ovZ*N_FrGa&ctmyadkmX776CnH46RjTA-weuZ? zrL4v~i`9Q;dKwQJQER3*o9zu7H&V;MxWJggqtslImQc!k%A#HFOwwg7#SKbKmz0$1 z{5>SOTc0b3GhHAphqawyZO`gKC&?CEZsr@K`xt*3<-{4wBwjs^yJ^#$({|+%des`z zwmSS2=c!UMnfI2CE)9w4MM4>gC3RX;D&cg(^Z@*kWC)#UeB0cbj3i|~OB7$)8kP3n zwD@WC{|Cj_epg6GCvE0g)er79Yt}Hz26r2;3nu6EIr8O3{77(6ER@o7dGY!(qnqVs z)SP0&F^(k@Bgw3!BsN_?o^;bRuo*xaQZ4pe`C4^`yXm5txYSt=|2rJpGjxq3ERN-; z#N14bVuJF~uv?d-dnibd;q))+zM_2uL8%w<=|5A46qTL2i}8>;hkU}Tm|O9 z5ikihfc4=1;C|pP+W9l!)8GT(wcr+TGq?eWU&0!wf?Z$((7vJlB4u~el`2`b>8-+q zFIW+I?wI5Z(-dH$H!9qcXwFE?7B;2I7iUbP^681?7d;xA8z=f25Xo4`q)fImixbgB z{+;1o_nOV%4*0w13Q4|Of)$b39-SKudDLE4jczC{OT0SYE7}UF!4k|sz2R(f7P9$7 zb7XMe0qR1|>S(7H_S88`#u)O5nVfvm$QM!IQ$^J|V1beE09_<&kSsHuay2ChV+1S< ztos(qAY8rJJXVZE!&>Z>vtb>PP9e^4Nx>V*ZjgBo6SK6*MW9Lh7gCz07FC!Hivu)c zCMfMa8xjt7Wi~_!$_r;U3SwkeoUV6~Gnt6Gm5Ff|h5#se;)Kp7Dwk9Siv|sQCx0tb?b>{r)~G&% z-^OHAm?Wfv$}9zj9g}j)XG|HyiWe@}v)eHmF+&@&FG$JqA@qrr1xeqo)gMo^QK_rnm82k(A608vA2!J3_m1ME}GnPnRtwY#d9g_o7gq6 zW51X7c(7y7f!+Iy=a0d*2BvB@bV3Y@zH;a8-4m0+Gxmra!M-0+*O|jcl$N zM`2B@w8aOjA(#<0z}GX-T+ZT5GYl>X&u^E&i|41^|r)lwAr4MGEK;pPTJY|{AEX|P@P^u&&oeeSk6^n8ziIue!wvMaf z#IuAXVBcd4&ikUwmAR4#?_|X|bt6?x2Gz&zpD45U^R)fnKsV=&d-bTbkhV01ATxGY(y#AH6kL&GSpHkq2( zFTrQ^8uKDEl5r~B$ezgynQOLR2{&?Vwr=#3={9btGrP-9Yekj_vU6MEH-)2QHdrE< zQ_4XOIz@~}-d=HZwKeTddWLY*sn~W<|H$_VCvzuVIY?5&^D%2`J-Tbp6%&(ALS;mS zW5KSSmrev_Z2=Z!pCikkJwb62JdTFfC&byftFJ)HhUMbm!yzH7>s zA0w$uAwRmdFfUI<0PY0$FJWYehk~vx#CHFnR2C3WcB1IASsa@U(&e@As1;j z8_x*95IE`jy+H6ZR(j;vAjKo0`iMm~>OaW}t-M!m6C)Iqk{Pcf`JQBpq$gr= z(w)su^+dtUU^@JSoZ!hR&b~_~E;RQ4?4t{R3!f+azq>#0|G?wl3&j85-QX@D@_<(W z@dbDzh`|A{2kZt<1&;;~1LuH$MJDh$a0|E*h;P6-5P=$)1P=vrUx3I8L{4x%crq9P zj|2|}_Xqa_UqwdnG4KKK*Wk~=Gr=Ud9IOM60_T9U!M(^0?f~xqZwEJn8^QB|=pD?0 z39toh2CKjd@Hp^TAkGl(L5}d(;IF{T!1ds{;3TMki@`Rq6+9Vy6`8`{gU^Exfj5E| zf)n65;F(|&Tn^R)xzpfb;2iK%X1*MM0t12%#u zfo~vpcs+O>xB-;FFOe;LAG`~^3A`HoC-8FcGH@L12N!@9paAYgt|0P=d%)Mhhk@ut z+yGt(V(@ez_aLkTE5HwtFZ>huj>s8+$RyqZ-VFW(90l9J(}Bn&)`2Gi>3g4&{`c?u z$OzeLjuJS!-5PDQXWFGsYi3jyq;$hm!eP%8W3giFYl@>y6b&%q!QDy{L#A^b za(cp|hk14ND)06|?i=1xJaVLNOWer>smakQ%QK>8w$tJWI;xp1mUSkgG3^0}26Ivk zTN63kkkRh)%8K1(x6}!{aBw0_5ic`NnBse!$uP=W*-F7im!XqZA=tKMPIsUg4#IX_PI)-=TWMCL3Jj}BTensxU3W=2V6k-DDFVABdG>)3 z5*Bek4I-&qq0i~tdhJGbvT`$9l@nEAV4OR@^awFCQ6=O;HY52s zS6hg8^l|{aR2c;bo108uNbw3n=&h;L*pDg`9`9x74G!SfaK$1)O#H>FNb+vTx^In; z(!BD+KaW(sqnzEhG?L8MF02&c`6_UNXsQ}0%Fl_O}Yn-VDbGNorc z80YS;M-D2wSGaU75JIHOgiq+a9)}P&bwX}wm<=S3>lY-Ud-g%W{gBFWC~Xn}by=Jy zBKAg(Y4{5@5tiKp_Q^Z+l`_v#12ej9R7tC7{?9Cz$D*kUiU^MN<~Ebju|sPQ+oIXG z>LyK;kRXccI{2k@LZWTyeL5v{vP3O}ge$me6S=;p!{sQ;TeE;AtXbOhQhuAcpO}6D#|{6f&%Z-u)mNdkat2ki>0c>n+7to}AU@_suF-+p=8(!t zzwrJ@>z{N%;}yYPG))oiAT;QSY{(R@hPzX?M_suyMT&?#n*0b>?^*GM-Iu!bWwN%D zgw1LTqDkFdnG>QpBzU^}RCP7C{qz)Yd;*%0WX|PiW$54$gnzlKNw%~`JY|1(xEbac zyfn=oZUhfhtsS>X@Mw_n~LmHb}(|)hU~A| zd8jI{tE3I#9%ojs0kKsvHy>9Y4v47^B^M?lDobq4bE$A7LgS#a6yt?)VHMH>nyK6* zs|RSJ|9?Jwmz)C&|9_L^!QKh~E;#*^Mz9)) zzyC*p2LSQ+|4s05@E72A@J8?kFbAFl{x>}Pt>7l`D)3@(0*G(_8E`4s0XBgPz`wzt zzZbj&h`zuJz+v!o@L+I%Ain&+2VedP@M>@ntOE}Lq95={@DU*Q42oUBO`r$nK%QTJ zJkR9b!4HA=f;+&wz&yABJQ#cz9{e8g6(D^2ZJ+}#0~dpfzy|OH@JR4Y`13D;yTIqb zhrsnfbP7&_E~tPj!85@{;E~{=;4JVfc=a!XyTM)HL*N$h3UCy3KpTkN!v?S(oCCfK zzkUa}9lRdA4*Vgw5xfY@f>9uReE`k?--cKJI`|s+@8EslFTow))!+tj9k><YAbA z)}#`a;Es$cMp*a~N9m$CZ20iOG<7NXl|o38q%Y6Y3EBTo&r34R@6t*=m7A*R{=%_= ze&T2ci!bCAoHPa}OxVPa@qpSG9Wz{aYnl9nSZ6%qnw0cFSK5z$%XzhSP@WOjEiYRo zj`fgB3eE#Ab2wgoHIGaP5oUS0IM2F_7ifmV2-Y~5rB?LQV(I6c-7*U)TgkYl_3 zyWTje!y{9DM^lLIc~urFAeh_PQcXgSsDZrEO|sVr3(I9|ps^R`NnXdJDx2IQ?tZ$_ zQ8wezG5u!1nQnNQIO?6WHzmrAs?+2*>)3LqdNHmF6?!J2#5K2lX3q%*fWs>Qq&f z08U2dil~u(!}3|Sg1(Wm`^nJLGvna*0!}yin*>GZlmxNVl6QF~hgT^bg2A6911uvA zNm1e77}pDf)HUj8Rh7M%tvD7$fHZsRD^r%f|+*) zgz-lT^S-PJ=zObQaN;ssw31Xo!TuxtNJeEgyqx!o)7bn!EV=#g^g_E#Eh|y+(k_Mklxn4 z>(PYFBf;+Im}_YrpQ=oO3RD|Y8!L8U8!Ep=rRu`%${>5C)Jrs|uGCN_RxKXqN$}x) zM46oG%S5i~4!Qoj2$8d^fD)3f(h5d1FGi&}((&bEq+|v8*96rEUJy|wU@Adh!TGAM zvqM!y^`d#4DU6+`q)OFxO{N8Pg)ijbmA$${Z6u@tOc(;WsmOh!O=mB|J#8tGJs@M( zp#up7-sESB*`P4(>>vo80P$6%*>x4d>Q=zb(T4M8+`ts zf!n~Vzy#O=HUqH*7y(ZJj|UF|UxmkiHF!1u&(!7eLP4pAF6iXM!`pFV2KF2VVmp1j5U|7(`$T zcp~^2{Q1YgYr(Z(8tevgu6`DHAh-_@Cy0LsZ!UN7y$Boumx6P_58%1)1TO@;!A5X4 z_-AmtkuYk3Es0iP_>Wj6d?$!5{pHFC@W8^LkQ$(vasja9PNpvHm{p*s>k)2`kV zD>=y&b76}))j6j%XVdAo?28pX*-jN*Y^7WS42qj7uNlV+HlzGwW2@?C%4($NY=++^ zS`ci_J~Mk4o8aJ<;H6ug@{w~)xD%JiI^j_~%bREO_e-1IJf>SX+0Z4MO-H9};8a82 z71f!f$n9!HLwwOSD+l|zldHs?Tc%@Jz2f31N)ARnzN7~S#kE{sZuCo4L(FPKx?JT< z&7OiK94Sm|#6{hrPbPvnB>qrt#Oy9)AEa+Y3}#EiW~mxdH{B%RQI|uLu!UNToTTv5!8G@vH_6mSV+SUA%LTY{n$hT^km$0M z>wo1=GB$S2j>$)B7uz^7ryy+Hba!mZ8?Rose$~1)YuB$?yXNU@H>}&ZcD;#rep)e0 z6uV_`)zISDP%Z>pQ96cfiDl4@a1WjCz5Lg@Q`MLI#5O?GD3C{)Ds43+Zr}F6?D26Bf<88H@{IC`D8~>IrrNoGGI>R2IPetvkRY} zO%}SvaUm0y#&h?eQbiYldAqVW)%;LPkuKd=mTmVEajr|jlCA6xK;3gs1y`t(+*s}2 zO9f2X&68^iW(5$D21BIR%?Kthwi|NeQXt+hE^V+|kmF`VrjnL3Ahp845S<$#wBas& zyD{=fCsWS)!aBo$;<(tv!u99@Hd^M+1dguS;xt;ADm!TGo>zuWZORQa5uvMUtqcgH zvg(qCV`Wie7fU_j_l`wyLqrh;_~}&Hoq3wUWV4Zxr~I&%I6wdj=P#OS1hJY--FBlP zmlB$Kq_UfI(#>C8HUc~DbC4VdOVPx~jc5bK3;{L03FA3eEUDe3UZN=>RA$K(z$7NL za3u(gJ>B^jxX>J;0E0%a=o!tR{_j#^c5t#|A^cX4v_@l1>mjf9>G8uQBH!~_=!~pO zOq4APT{i8Z`vZwi+KVu(tgnl_k*UUf1}+@t)#-A_F3HT!sd-ezvTjm!Aa3n+k|fC%-yCh}D%cO&b&4NN1iuJ?Z3ma0z|?)br!r7y7b8(G zK@XHmHoe}+O2@sJNpq{An7r2Yf$`u5Nx@WEmCw?rHW-#zRnCB?`?72j*)1EY-e;C! zij;~SWCR)UktWOVI0YdWp`r@72hq;#YIu(tYL+_N7t^b z<~B}IVw7_X%PwTz4PYTugFy-|MKSL)nGipB=B3wO|0)GOlcrV?85RMJT}8 z-^+RaRbVT4IQTlez1aC*2Sh(V0K)5k1pF~L0Dc#I1OELUa09py>;S($6aRqVKZ7g5 zeS!G-yBs_L$T|DF!SjHetv?2Q0zUjWxDb379{Wyk1K1571O6VK`5oXW*bIc9{t^7~ zz2JplC3p+`@6*8lf*wMXd7;bymyhq;!Se&LJm(Gvt>XwJx^$EDj*6Uj2R{$%(FcuS z?xgI`;JU;*`Ejc`tGKyqGI=w=tHh*Ufh)&{sb>n4vX{*A>R^)EYk2C@sEBH>QP|84 z9OCg}d%xb0t2s_V5vE>YO-vGE8fSI1*d=iNwZ5}52BY?)OKVzFbe7F)vR_^$cNopP zQx+;i%v|=%%186qr+P}KuD(oZQX6qm=bR0h6SpO6v(h%yPSusVkYl=)E;ZwyB)tt7 z;-r1ddz|!;Ooayp7+C8}Ech8wTN~UCnUFMOx66#}7fRQ>io8k6%8tIB3Z$PG5z70iiN zb}k_4+zK@?^-OW}WOY2qdli2)fTf*B`!NhkQs z`I)8Z0BYuI!9*e|pr-%48LgH6_W$4@93ymV-2e zqm|qcZ;O&i!GBK*OE-cmUs}U_=~8EX>Tk}DoKwwDx=vb_NC$v39B>LNOGd8Z0HfVB zmM*6q8QCQ2+PLpFV|>ZX{QKvZ3@l1DNI<&FPF;M!R5=}9c7&<0In_p)dzq!zP1ziW z&dZJwH;~ONJM>J{Y{9Gdo6C+7; Date: Tue, 1 Mar 2016 01:58:02 +0000 Subject: [PATCH 066/255] removes unused methods that I forgot --- app/models/project.rb | 4 ---- app/models/repository.rb | 12 ------------ 2 files changed, 16 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 4d2ff11cc1..3d3fd6f76a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -957,8 +957,4 @@ class Project < ActiveRecord::Base def wiki @wiki ||= ProjectWiki.new(self, self.owner) end - - def main_language - @main_language = repository.main_language - end end diff --git a/app/models/repository.rb b/app/models/repository.rb index f911611aca..c135ab61f6 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -812,18 +812,6 @@ class Repository raw_repository.ls_files(actual_ref) end - def main_language - return @main_language unless @main_language.nil? - - unless empty? - @main_language = cache.fetch(:main_language) do - Linguist::Repository.new( - rugged, - rugged.head.target_id).language - end - end - end - private def cache From c2c5572e22d8e03b111859f3cf143181edd8c20f Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Tue, 1 Mar 2016 11:42:47 +0000 Subject: [PATCH 067/255] adds swp and swo to gitignore and improves migration for project main_language --- app/models/.project.rb.swo | Bin 49152 -> 0 bytes app/models/project.rb | 18 ++++++++++------ app/models/repository.rb | 6 ++++++ ...9193553_add_main_language_to_repository.rb | 20 ++---------------- 4 files changed, 20 insertions(+), 24 deletions(-) delete mode 100644 app/models/.project.rb.swo diff --git a/app/models/.project.rb.swo b/app/models/.project.rb.swo deleted file mode 100644 index 5d264ccfaaa42e0a6f93323aa8baf0a4e1fa7cf3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49152 zcmeI537A}0b?1w)*?_TGJ_!4h8oR5VR%vIcSi9S{uq?@~#W+F~ud7~nm%29BtCG~V zgbf&Dwh%JG_5`y!fY~O&2@DJlJAolEOt1-S1`GiL1k4te@MUIx=iGbWyYF?ixpAzFbEnoE+<95>)Y66mpK}U@r~LfxgHzi!|ISA{_bK%1;Y__Di%?>q?DwHEj4%o|{QUKh7=f5U`T->1%?zDQea4dAq9pM_zz2gUgtrDYbpKj*cx1EzweX# zzQ6r_hW$R2dw)Os`@xI6ch9%m_hByn19H!A%e{Y~{e8XtzMn0iE2k^ht@izmx%X$< z=Qr8!C)*0U_z$wrue0C(E%*Mu_PG;iloq%3JfVQq`;5@LkbKj zFr>hc0z(Q6DezlKfv{F6e2x4~z@XTVEA4Qv8W0Ka-@q40Td z2WWxw!MD#U6y6A~1y2R%fKNZ9P`DjD4{Qa$Mi6i}cqizA_241kTQK1N1^gkH1Eb)Z zFzp`(uLFm`qruN$^nU_=3_bqo)+z`MZP!P|h)`^}&Mwt+{1Un5NTB=`h)1CZbE3G6418q=j_SZ`GtVQZ!z&P2hc z!2Me&xM1C=(~j%Cc6Ywi?a$0awbE=9)=IswI}`OP_1ah|iL+>wahsY8Nk+Ts{jK`S~|)S&h0V1<`))a>-;w*=>%7igiYvlKY01;I#AosFt2`N6s#ToYNE{xGR%q}6FOSya`c%D9@6A^l(b1?u z74wg5RLYu5I3Fz1Q#rLR5=#L zl&ls_2mMYh>_rvLrjjodVZ!NN)UB9ML3ydrBR0K!j=g~P&^-JWu&GtsjefILEH}cq zR|%`V`ca{1*c-Pk-;F3?jR#}4%rTuXA2=y$Rf7lRMFNeGXpe4^%_``%wKK(GGYY~u zFo{T>QeNm3_8Yy*(Xi21QWeYbY}lnt-F~YTb>m9Z3a1)TZ7jGZsP?n zCb1=}Wr7V(B0nq^ZHIio!O4Z)>TLaJRC8@Xr~|t%-M#0E-Ibk}?cFoEzj9!57jwzz zY_HcTuU_46&9&OcS|u7}REN*;9&=Z4v8ub3%R96)ToN@pQCD8qTh&It76tp;b5Uzs zpE(OWB=rViGgWU+ec|D9bq%kaUz!EUY8n3 zE^aF8Nw-PU^g=8$d7XYalF^uCb6ROs>uf=isL4N$rc#-DvqMjOd9>g1TICeYvS!RnHg5O3)wI1G6^rXOZuEoo<~v@XgxE>2nElj(BAlvkk1}ic^fN(imq_a2=Cd>*{crI$y=dF6 zefbWWpE_+~u2Ivp-9X6`!!~)p+epLmU%s#lR(=z@EW02XrqaxAIdzMMO>prF4H$5C z+39t|>Kq#=*J1O;$p%YG?gujuUA0#-1EB1@GB>_kY+T|_SdEgLG@2uy)JhiaT#Y2m zY1Jyb;3lb^RvP|x#MI3c)y|sfBUk0cscW}gNtDwnhwky(Y17zLe3F?Bs%!(3Eaaa| zXGyn;i-)O2akX3T$X?J7luSpRu*ZbH;N$lKX+de^1%VcJao5*!L0!et$8M1s4?5j? zGwjX>bJ4smZTn?G*Zj)7Qw{o9Uu@bG?1TSmhQZDj`}wBsZwtczZ-U1?6CPOj|Av)P z+yjsQ74T*70q|n*0`Por1Y7{dz`gMO-vD0#F9+9xK4^j&FbysP8$b!H0*?bfLN@Rn z@ILTz@B(lWoB$y}Rk^SZtObt;zeaZOFW_h3i{Q=R_26~j6`%u-fI7GWTmsGj|A=hi z%iv4kzk)vpZw0r5mxCkVnP4y210DqK2kr~Lfehju;LYGo;91~7;QPoSUIT6hH-Y2e zAlM7`fHmMT;1S^A;QPoTJ_7Cp?+3Smmx339>%n2L8Qeu1dVGBkUmkI6*%C^eh zEjV#vljCi#;CD$Rry?7*axz2&QP+vcR7yj#t|&12NQx49l`?rNY_=KB&(C%dCcUJr zDAF$x=o!gpT$KOiQMcVpgs1sE{aDG$Cbkq+URvt4D|IO`+@}#?_PYJZ$uUieTWlfo zX;o|?Y*J%>xy`1!)P_GtbgfLDO+_A6?4H{^=Y^w@%9*naI|5yi`DsCe^Mj|XvFT(& zRS>5)>#cq-iZ>NCe5N#+u)*r36Y;BJB4_#bQU{qILLEn%To}74-r^^dRxRPZQ_*^+ z{S$$E_9^>5?K7@5L}F60H4}lsmgMyaJHy~Y`eDPw6hgCcrO^Xn!$e-JQOR&5*uUq(JwY*??@x!Vv2t(`fslv=$LK3@l#^T( zr{K7bh25Gk`E0DMgg(A-4dg1CzN{b-c-|D&u5((_v{z=v2Du>In08ej7~9MxB5G6 zx{!R*z(LTMjw|gcwW)QSL2$tZDylDNV0Y4ynF`WaacP>iuOufL>6;T8KRGIK-J4?# zilmdx5?vaZsZ1aw1M5hkg^8(D>wYX`M@IHjM4C0mlo}*Y;*w$IcB*8-KORjq_KG9BlT`rv0?)4Bl;kYx<~8P$TqMoh#%A(lth_P?nL`Em%*v2kg24~oNHyKvaX0kx!1bl?#GS!LWZ1aZhmjf%4kZ*W|3MKjmXli%m|6- z;c^ztS*}g9-L22aLY6s!@zX`>gh9O-z4UOYRar#@W6J-35PtlD@bbd{!}jXum;C+- z_%Zl6cq_OaTn`R`KLERc#oq%3JfVQq`;5@Lkj$s zQvh|ZsL@8xvx0=7tRjYX7cIYbH>%IHDjcqtgHih!7JPI(P=z%W5b)*zLZ4B%@sVIM zYUsgwEc|!a>vfU#RMG84?pba(&`w?`TUZ#7;{i0&jOOZ8H$;k{sa4Ud3@KqaGo$h` zR1(Vk5Pe|O<>GDee*0!mt0dZC)n28F1gXMNKU!y2!bV^xf{|}I|GyLd|Cd1ce~zK` z^AGU$9|gC7CYS_I0#5`I_6HJv_!&}QNP!^*h7=f5U`T->1%?zDQea4dAq9pM7*gQ3 zkphAJaDMLn`ybn^+G$;=X*I0AXCMx%KNX1xqawD@h&9CeIu}Iv|Hso-cVcTU{C{ZM z?X~dvuK|a_vp@iT3eW!&Ao~7)0qWo&*bY{Le@0LLZQy1g`utU}3Va_u{C9(SunIf? z{7)1O{uDe9Tn?TJeuQrQ--0{93=kgx=YgM}StxuC{2jO*yan6}t^lH6|4xJ!;s>A% z?m!Rza&RB;S#0@l1Q&pxW8?5g;1Ku&Py}ZIvF-mnxEs6|{0Vp&cqzCF>;V^pi@@)J zGr@g80sH{_ezE)i1o#{90x$u#fX$!;9t^|>z!$+6z{kN$!1KZLKpkuXXMiuz2A>80 z14w(kADrqVeOT0`R8^(Zk7q?nl!|w-8&U<5y#8f5Z{ppEje1M4@P+7ySp}nVdFSr^ z6O+5Q?GiOMwD*iw(V$41fY-#B?oG$}AdN zEDbTCw~=FpaIKGOoM){VOmzv0DQkIJwZ$}5_r#XKe`eFo6Ur;UNrw`eLAxD!1q!<7uND)+SoKhWbX* z#I#Vz%j}5-cndOH63EkjJdRQ<*4SZ#dP)wq6mb=;Wqnd{mX_nes?E3>FeB4FEF1l| zG4&>3BbjlM?S<`DV?L9>!l7-)U1?Bs)!dE;<+A*Q%Vjg5GpQVDLlXMvs?BClClwq` z6zIgbam6MbyE5)e{)8M}+u0K$G!mOc9dVKgn8ao@U8$iue_onm0`* zxssi2+N6QTO5F(e0a3S@_QP1aOt5X=4l$iH=8?&x(F}dlOk@^djB6%Il1XfNYmLD# zoY=Qxu|!l|9QP=>bn*)vGl}sP(iE#c9ro8-_B(Z-&R~&3>~S ztXs3DG*@r5r{=M$FyNBGn)ZI!Cb>0fL#`vCK zAJAh7U!#RE^(o_(!0ZsrdLX;;Ai$DclAsQ~HZF1)%I9M56VHm4av-ij^p8vzeyJ&t zT~vslc~R-N>iAV@agJEQx}q$Hh?ovZ*N_FrGa&ctmyadkmX776CnH46RjTA-weuZ? zrL4v~i`9Q;dKwQJQER3*o9zu7H&V;MxWJggqtslImQc!k%A#HFOwwg7#SKbKmz0$1 z{5>SOTc0b3GhHAphqawyZO`gKC&?CEZsr@K`xt*3<-{4wBwjs^yJ^#$({|+%des`z zwmSS2=c!UMnfI2CE)9w4MM4>gC3RX;D&cg(^Z@*kWC)#UeB0cbj3i|~OB7$)8kP3n zwD@WC{|Cj_epg6GCvE0g)er79Yt}Hz26r2;3nu6EIr8O3{77(6ER@o7dGY!(qnqVs z)SP0&F^(k@Bgw3!BsN_?o^;bRuo*xaQZ4pe`C4^`yXm5txYSt=|2rJpGjxq3ERN-; z#N14bVuJF~uv?d-dnibd;q))+zM_2uL8%w<=|5A46qTL2i}8>;hkU}Tm|O9 z5ikihfc4=1;C|pP+W9l!)8GT(wcr+TGq?eWU&0!wf?Z$((7vJlB4u~el`2`b>8-+q zFIW+I?wI5Z(-dH$H!9qcXwFE?7B;2I7iUbP^681?7d;xA8z=f25Xo4`q)fImixbgB z{+;1o_nOV%4*0w13Q4|Of)$b39-SKudDLE4jczC{OT0SYE7}UF!4k|sz2R(f7P9$7 zb7XMe0qR1|>S(7H_S88`#u)O5nVfvm$QM!IQ$^J|V1beE09_<&kSsHuay2ChV+1S< ztos(qAY8rJJXVZE!&>Z>vtb>PP9e^4Nx>V*ZjgBo6SK6*MW9Lh7gCz07FC!Hivu)c zCMfMa8xjt7Wi~_!$_r;U3SwkeoUV6~Gnt6Gm5Ff|h5#se;)Kp7Dwk9Siv|sQCx0tb?b>{r)~G&% z-^OHAm?Wfv$}9zj9g}j)XG|HyiWe@}v)eHmF+&@&FG$JqA@qrr1xeqo)gMo^QK_rnm82k(A608vA2!J3_m1ME}GnPnRtwY#d9g_o7gq6 zW51X7c(7y7f!+Iy=a0d*2BvB@bV3Y@zH;a8-4m0+Gxmra!M-0+*O|jcl$N zM`2B@w8aOjA(#<0z}GX-T+ZT5GYl>X&u^E&i|41^|r)lwAr4MGEK;pPTJY|{AEX|P@P^u&&oeeSk6^n8ziIue!wvMaf z#IuAXVBcd4&ikUwmAR4#?_|X|bt6?x2Gz&zpD45U^R)fnKsV=&d-bTbkhV01ATxGY(y#AH6kL&GSpHkq2( zFTrQ^8uKDEl5r~B$ezgynQOLR2{&?Vwr=#3={9btGrP-9Yekj_vU6MEH-)2QHdrE< zQ_4XOIz@~}-d=HZwKeTddWLY*sn~W<|H$_VCvzuVIY?5&^D%2`J-Tbp6%&(ALS;mS zW5KSSmrev_Z2=Z!pCikkJwb62JdTFfC&byftFJ)HhUMbm!yzH7>s zA0w$uAwRmdFfUI<0PY0$FJWYehk~vx#CHFnR2C3WcB1IASsa@U(&e@As1;j z8_x*95IE`jy+H6ZR(j;vAjKo0`iMm~>OaW}t-M!m6C)Iqk{Pcf`JQBpq$gr= z(w)su^+dtUU^@JSoZ!hR&b~_~E;RQ4?4t{R3!f+azq>#0|G?wl3&j85-QX@D@_<(W z@dbDzh`|A{2kZt<1&;;~1LuH$MJDh$a0|E*h;P6-5P=$)1P=vrUx3I8L{4x%crq9P zj|2|}_Xqa_UqwdnG4KKK*Wk~=Gr=Ud9IOM60_T9U!M(^0?f~xqZwEJn8^QB|=pD?0 z39toh2CKjd@Hp^TAkGl(L5}d(;IF{T!1ds{;3TMki@`Rq6+9Vy6`8`{gU^Exfj5E| zf)n65;F(|&Tn^R)xzpfb;2iK%X1*MM0t12%#u zfo~vpcs+O>xB-;FFOe;LAG`~^3A`HoC-8FcGH@L12N!@9paAYgt|0P=d%)Mhhk@ut z+yGt(V(@ez_aLkTE5HwtFZ>huj>s8+$RyqZ-VFW(90l9J(}Bn&)`2Gi>3g4&{`c?u z$OzeLjuJS!-5PDQXWFGsYi3jyq;$hm!eP%8W3giFYl@>y6b&%q!QDy{L#A^b za(cp|hk14ND)06|?i=1xJaVLNOWer>smakQ%QK>8w$tJWI;xp1mUSkgG3^0}26Ivk zTN63kkkRh)%8K1(x6}!{aBw0_5ic`NnBse!$uP=W*-F7im!XqZA=tKMPIsUg4#IX_PI)-=TWMCL3Jj}BTensxU3W=2V6k-DDFVABdG>)3 z5*Bek4I-&qq0i~tdhJGbvT`$9l@nEAV4OR@^awFCQ6=O;HY52s zS6hg8^l|{aR2c;bo108uNbw3n=&h;L*pDg`9`9x74G!SfaK$1)O#H>FNb+vTx^In; z(!BD+KaW(sqnzEhG?L8MF02&c`6_UNXsQ}0%Fl_O}Yn-VDbGNorc z80YS;M-D2wSGaU75JIHOgiq+a9)}P&bwX}wm<=S3>lY-Ud-g%W{gBFWC~Xn}by=Jy zBKAg(Y4{5@5tiKp_Q^Z+l`_v#12ej9R7tC7{?9Cz$D*kUiU^MN<~Ebju|sPQ+oIXG z>LyK;kRXccI{2k@LZWTyeL5v{vP3O}ge$me6S=;p!{sQ;TeE;AtXbOhQhuAcpO}6D#|{6f&%Z-u)mNdkat2ki>0c>n+7to}AU@_suF-+p=8(!t zzwrJ@>z{N%;}yYPG))oiAT;QSY{(R@hPzX?M_suyMT&?#n*0b>?^*GM-Iu!bWwN%D zgw1LTqDkFdnG>QpBzU^}RCP7C{qz)Yd;*%0WX|PiW$54$gnzlKNw%~`JY|1(xEbac zyfn=oZUhfhtsS>X@Mw_n~LmHb}(|)hU~A| zd8jI{tE3I#9%ojs0kKsvHy>9Y4v47^B^M?lDobq4bE$A7LgS#a6yt?)VHMH>nyK6* zs|RSJ|9?Jwmz)C&|9_L^!QKh~E;#*^Mz9)) zzyC*p2LSQ+|4s05@E72A@J8?kFbAFl{x>}Pt>7l`D)3@(0*G(_8E`4s0XBgPz`wzt zzZbj&h`zuJz+v!o@L+I%Ain&+2VedP@M>@ntOE}Lq95={@DU*Q42oUBO`r$nK%QTJ zJkR9b!4HA=f;+&wz&yABJQ#cz9{e8g6(D^2ZJ+}#0~dpfzy|OH@JR4Y`13D;yTIqb zhrsnfbP7&_E~tPj!85@{;E~{=;4JVfc=a!XyTM)HL*N$h3UCy3KpTkN!v?S(oCCfK zzkUa}9lRdA4*Vgw5xfY@f>9uReE`k?--cKJI`|s+@8EslFTow))!+tj9k><YAbA z)}#`a;Es$cMp*a~N9m$CZ20iOG<7NXl|o38q%Y6Y3EBTo&r34R@6t*=m7A*R{=%_= ze&T2ci!bCAoHPa}OxVPa@qpSG9Wz{aYnl9nSZ6%qnw0cFSK5z$%XzhSP@WOjEiYRo zj`fgB3eE#Ab2wgoHIGaP5oUS0IM2F_7ifmV2-Y~5rB?LQV(I6c-7*U)TgkYl_3 zyWTje!y{9DM^lLIc~urFAeh_PQcXgSsDZrEO|sVr3(I9|ps^R`NnXdJDx2IQ?tZ$_ zQ8wezG5u!1nQnNQIO?6WHzmrAs?+2*>)3LqdNHmF6?!J2#5K2lX3q%*fWs>Qq&f z08U2dil~u(!}3|Sg1(Wm`^nJLGvna*0!}yin*>GZlmxNVl6QF~hgT^bg2A6911uvA zNm1e77}pDf)HUj8Rh7M%tvD7$fHZsRD^r%f|+*) zgz-lT^S-PJ=zObQaN;ssw31Xo!TuxtNJeEgyqx!o)7bn!EV=#g^g_E#Eh|y+(k_Mklxn4 z>(PYFBf;+Im}_YrpQ=oO3RD|Y8!L8U8!Ep=rRu`%${>5C)Jrs|uGCN_RxKXqN$}x) zM46oG%S5i~4!Qoj2$8d^fD)3f(h5d1FGi&}((&bEq+|v8*96rEUJy|wU@Adh!TGAM zvqM!y^`d#4DU6+`q)OFxO{N8Pg)ijbmA$${Z6u@tOc(;WsmOh!O=mB|J#8tGJs@M( zp#up7-sESB*`P4(>>vo80P$6%*>x4d>Q=zb(T4M8+`ts zf!n~Vzy#O=HUqH*7y(ZJj|UF|UxmkiHF!1u&(!7eLP4pAF6iXM!`pFV2KF2VVmp1j5U|7(`$T zcp~^2{Q1YgYr(Z(8tevgu6`DHAh-_@Cy0LsZ!UN7y$Boumx6P_58%1)1TO@;!A5X4 z_-AmtkuYk3Es0iP_>Wj6d?$!5{pHFC@W8^LkQ$(vasja9PNpvHm{p*s>k)2`kV zD>=y&b76}))j6j%XVdAo?28pX*-jN*Y^7WS42qj7uNlV+HlzGwW2@?C%4($NY=++^ zS`ci_J~Mk4o8aJ<;H6ug@{w~)xD%JiI^j_~%bREO_e-1IJf>SX+0Z4MO-H9};8a82 z71f!f$n9!HLwwOSD+l|zldHs?Tc%@Jz2f31N)ARnzN7~S#kE{sZuCo4L(FPKx?JT< z&7OiK94Sm|#6{hrPbPvnB>qrt#Oy9)AEa+Y3}#EiW~mxdH{B%RQI|uLu!UNToTTv5!8G@vH_6mSV+SUA%LTY{n$hT^km$0M z>wo1=GB$S2j>$)B7uz^7ryy+Hba!mZ8?Rose$~1)YuB$?yXNU@H>}&ZcD;#rep)e0 z6uV_`)zISDP%Z>pQ96cfiDl4@a1WjCz5Lg@Q`MLI#5O?GD3C{)Ds43+Zr}F6?D26Bf<88H@{IC`D8~>IrrNoGGI>R2IPetvkRY} zO%}SvaUm0y#&h?eQbiYldAqVW)%;LPkuKd=mTmVEajr|jlCA6xK;3gs1y`t(+*s}2 zO9f2X&68^iW(5$D21BIR%?Kthwi|NeQXt+hE^V+|kmF`VrjnL3Ahp845S<$#wBas& zyD{=fCsWS)!aBo$;<(tv!u99@Hd^M+1dguS;xt;ADm!TGo>zuWZORQa5uvMUtqcgH zvg(qCV`Wie7fU_j_l`wyLqrh;_~}&Hoq3wUWV4Zxr~I&%I6wdj=P#OS1hJY--FBlP zmlB$Kq_UfI(#>C8HUc~DbC4VdOVPx~jc5bK3;{L03FA3eEUDe3UZN=>RA$K(z$7NL za3u(gJ>B^jxX>J;0E0%a=o!tR{_j#^c5t#|A^cX4v_@l1>mjf9>G8uQBH!~_=!~pO zOq4APT{i8Z`vZwi+KVu(tgnl_k*UUf1}+@t)#-A_F3HT!sd-ezvTjm!Aa3n+k|fC%-yCh}D%cO&b&4NN1iuJ?Z3ma0z|?)br!r7y7b8(G zK@XHmHoe}+O2@sJNpq{An7r2Yf$`u5Nx@WEmCw?rHW-#zRnCB?`?72j*)1EY-e;C! zij;~SWCR)UktWOVI0YdWp`r@72hq;#YIu(tYL+_N7t^b z<~B}IVw7_X%PwTz4PYTugFy-|MKSL)nGipB=B3wO|0)GOlcrV?85RMJT}8 z-^+RaRbVT4IQTlez1aC*2Sh(V0K)5k1pF~L0Dc#I1OELUa09py>;S($6aRqVKZ7g5 zeS!G-yBs_L$T|DF!SjHetv?2Q0zUjWxDb379{Wyk1K1571O6VK`5oXW*bIc9{t^7~ zz2JplC3p+`@6*8lf*wMXd7;bymyhq;!Se&LJm(Gvt>XwJx^$EDj*6Uj2R{$%(FcuS z?xgI`;JU;*`Ejc`tGKyqGI=w=tHh*Ufh)&{sb>n4vX{*A>R^)EYk2C@sEBH>QP|84 z9OCg}d%xb0t2s_V5vE>YO-vGE8fSI1*d=iNwZ5}52BY?)OKVzFbe7F)vR_^$cNopP zQx+;i%v|=%%186qr+P}KuD(oZQX6qm=bR0h6SpO6v(h%yPSusVkYl=)E;ZwyB)tt7 z;-r1ddz|!;Ooayp7+C8}Ech8wTN~UCnUFMOx66#}7fRQ>io8k6%8tIB3Z$PG5z70iiN zb}k_4+zK@?^-OW}WOY2qdli2)fTf*B`!NhkQs z`I)8Z0BYuI!9*e|pr-%48LgH6_W$4@93ymV-2e zqm|qcZ;O&i!GBK*OE-cmUs}U_=~8EX>Tk}DoKwwDx=vb_NC$v39B>LNOGd8Z0HfVB zmM*6q8QCQ2+PLpFV|>ZX{QKvZ3@l1DNI<&FPF;M!R5=}9c7&<0In_p)dzq!zP1ziW z&dZJwH;~ONJM>J{Y{9Gdo6C+7; Date: Tue, 1 Mar 2016 19:45:55 +0000 Subject: [PATCH 068/255] adds tests and fixes some broken code to main language mr --- app/models/project.rb | 6 +++--- spec/models/project_spec.rb | 8 ++++++++ spec/models/repository_spec.rb | 6 ++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 9400d8a17a..470fa2194d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -89,7 +89,7 @@ class Project < ActiveRecord::Base # checks if the language main language of the project changed before_save :check_main_language def check_main_language - if commit_count.changed? + if self.commit_count_changed? self.main_language = repository.main_language end end @@ -955,12 +955,12 @@ class Project < ActiveRecord::Base end def main_language - language = read_attributes(:main_language) + language = read_attribute(:main_language) return language if language update_attributes(main_language: repository.main_language) - read_attributes(:main_language) + read_attribute(:main_language) end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f9842d23af..60b0f55b31 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -560,6 +560,14 @@ describe Project, models: true do end end + describe "#main_language" do + let(:project) { create :project } + + it 'shows the main language of the project' do + expect(project.main_language).to eq("Ruby") + end + end + describe '#visibility_level_allowed?' do let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 1c7d66398c..2094667565 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -595,4 +595,10 @@ describe Repository, models: true do repository.after_remove_branch end end + + describe "#main_language" do + it 'shows the main language of the project' do + expect(repository.main_language).to eq("Ruby") + end + end end From 4b3f3167a4d7f1b1415052e350bc1f79ef5528dc Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Wed, 2 Mar 2016 12:55:01 +0000 Subject: [PATCH 069/255] implements test to simulate empty repo --- spec/models/repository_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 2094667565..150422ac34 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -600,5 +600,11 @@ describe Repository, models: true do it 'shows the main language of the project' do expect(repository.main_language).to eq("Ruby") end + + it 'returns nil when the repository is empty' do + allow(repository).to receive(:empty?).and_return(true) + + expect(repository.main_language).to be_nil + end end end From 93fb4ce2c5f9c7db40a0ab698cd4812a5cb7373b Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Wed, 2 Mar 2016 13:01:00 +0000 Subject: [PATCH 070/255] removes redundant self --- app/models/project.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index 470fa2194d..a8731514b9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -89,7 +89,7 @@ class Project < ActiveRecord::Base # checks if the language main language of the project changed before_save :check_main_language def check_main_language - if self.commit_count_changed? + if commit_count_changed? self.main_language = repository.main_language end end From 68d6f5bce19bac95e63de165f3e9597273ef97b1 Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Wed, 2 Mar 2016 13:06:21 +0000 Subject: [PATCH 071/255] fixes typo --- app/models/project.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index a8731514b9..34787f5fa3 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -90,7 +90,7 @@ class Project < ActiveRecord::Base before_save :check_main_language def check_main_language if commit_count_changed? - self.main_language = repository.main_language + main_language = repository.main_language end end From 337cb45226cfc4955fdd79a15088522a91058b3c Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Wed, 2 Mar 2016 18:21:53 +0000 Subject: [PATCH 072/255] removes automatic setting of main_language to project for it to set a main language you have now to make a commit to the project --- app/models/project.rb | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 34787f5fa3..137fa42a9b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -953,14 +953,4 @@ class Project < ActiveRecord::Base def wiki @wiki ||= ProjectWiki.new(self, self.owner) end - - def main_language - language = read_attribute(:main_language) - - return language if language - - update_attributes(main_language: repository.main_language) - - read_attribute(:main_language) - end end From 8039bbf7f8e686bdf1e8c00c44faa8a97a6c6fdb Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Wed, 2 Mar 2016 18:23:11 +0000 Subject: [PATCH 073/255] removes the test file of the project spec for invalid method --- spec/models/project_spec.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 60b0f55b31..f9842d23af 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -560,14 +560,6 @@ describe Project, models: true do end end - describe "#main_language" do - let(:project) { create :project } - - it 'shows the main language of the project' do - expect(project.main_language).to eq("Ruby") - end - end - describe '#visibility_level_allowed?' do let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL } From 96c02551070a5f2d90beea7068fc6fddbf1498e8 Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Thu, 3 Mar 2016 10:51:48 +0000 Subject: [PATCH 074/255] moves the main_language update logic to git push service --- app/models/project.rb | 8 -------- app/services/git_push_service.rb | 12 ++++++++++++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 137fa42a9b..3235a1cee5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -86,14 +86,6 @@ class Project < ActiveRecord::Base end end - # checks if the language main language of the project changed - before_save :check_main_language - def check_main_language - if commit_count_changed? - main_language = repository.main_language - end - end - ActsAsTaggableOn.strict_case_match = true acts_as_taggable_on :tags diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 9ba200f7bd..ab883b6ef2 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -14,6 +14,7 @@ class GitPushService < BaseService # 3. Recognizes cross-references from commit messages # 4. Executes the project's web hooks # 5. Executes the project's services + # 6. Checks if the project's main language has changed # def execute @project.repository.after_push_commit(branch_name) @@ -42,6 +43,9 @@ class GitPushService < BaseService @push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev]) process_commit_messages end + # Checks if the main language has changed in the project and if so + # it updates it accordingly + update_main_language # Update merge requests that may be affected by this push. A new branch # could cause the last commit of a merge request to change. update_merge_requests @@ -49,6 +53,14 @@ class GitPushService < BaseService protected + def update_main_language + current_language = @project.repository.main_language + + unless current_language == @project.main_language + @project.update_attributes(main_language: current_language) + end + end + def update_merge_requests @project.update_merge_requests(params[:oldrev], params[:newrev], params[:ref], current_user) From 4e8a5343972e179022ff84954475a6aa0f8901f8 Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Thu, 3 Mar 2016 11:06:37 +0000 Subject: [PATCH 075/255] add my mr to changelog --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index a8dc00ed81..5b3b18ef2d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,7 @@ v 8.6.0 (unreleased) - Don't show Issues/MRs from archived projects in Groups view - Increase the notes polling timeout over time (Roberto Dip) - Show labels in dashboard and group milestone views + - Add main language of a project in the list of projects (Tiago Botelho) v 8.5.4 - Do not cache requests for badges (including builds badge) From 49295924586d89eaff899a4efc83a336148f3862 Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Thu, 3 Mar 2016 15:40:05 +0000 Subject: [PATCH 076/255] adds test for git push service for updating the language of the project --- app/services/git_push_service.rb | 8 +++++--- spec/services/git_push_service_spec.rb | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index ab883b6ef2..736b82e357 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -51,16 +51,18 @@ class GitPushService < BaseService update_merge_requests end - protected - def update_main_language current_language = @project.repository.main_language unless current_language == @project.main_language - @project.update_attributes(main_language: current_language) + return @project.update_attributes(main_language: current_language) end + + true end + protected + def update_merge_requests @project.update_merge_requests(params[:oldrev], params[:newrev], params[:ref], current_user) diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 994585fb32..f5c51e46e8 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -155,6 +155,23 @@ describe GitPushService, services: true do end end + describe "Updates main language" do + + context "before push" do + it { expect(project.main_language).to eq(nil) } + end + + context "after push" do + before do + @service = execute_service(project, user, @oldrev, @newrev, @ref) + end + + it { expect(@service.update_main_language).to eq(true) } + it { expect(project.main_language).to eq("Ruby") } + end + end + + describe "Web Hooks" do context "execute web hooks" do it "when pushing a branch for the first time" do From 406f4a993a946db75d46756f936552144708a789 Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Mon, 7 Mar 2016 16:29:22 -0500 Subject: [PATCH 077/255] 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 ac15a2cdff..cf1f98492e 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 16e47bb99b..148c4ac188 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 623ec857cb..27170c1eb1 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. From 9568b4b4f752bc564fead51756d1fd3044ddeffd Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 7 Mar 2016 21:00:53 -0500 Subject: [PATCH 078/255] 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 a8dc00ed81..a9742d2201 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 c1787a7c6a..0fd54be58b 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!** From 344d6e6f89cceb33e371e10de8262154ee108fbe Mon Sep 17 00:00:00 2001 From: Pascal Bach Date: Wed, 24 Feb 2016 15:48:56 +0100 Subject: [PATCH 079/255] 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 Signed-off-by: Fabio Huser --- 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 1a3f662811..28e074cd28 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -10,7 +10,7 @@ module Ci 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 f3394910c5..1e98280d04 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 = < Date: Tue, 8 Mar 2016 08:41:01 +0000 Subject: [PATCH 080/255] 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 0161642d87..7d414ae003 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; } From 077d3791952a7120d4f28ca36c600e2185e6c1c5 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 8 Mar 2016 11:18:07 +0100 Subject: [PATCH 081/255] 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 22c86e4ae8..00eeffecb0 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) From 059df2256eb2ce5ddbbf43161eb8e9fca733bce4 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 4 Mar 2016 13:08:24 +0100 Subject: [PATCH 082/255] 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 b9a0d81e5b..f5c3107ff4 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 0000000000..23fd760495 --- /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 From fc610c182e73cdff2534bef91ce0385b06befacf Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 8 Mar 2016 15:57:45 +0100 Subject: [PATCH 083/255] 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 c0f2ab91fa..3451779e18 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -908,13 +908,13 @@ class Project < ActiveRecord::Base 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 f328deda35..d9f0849d14 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 From 6ac68d8cb0477367fbf9642526c459b87b116d40 Mon Sep 17 00:00:00 2001 From: Yatish Mehta Date: Fri, 4 Mar 2016 14:34:32 -0800 Subject: [PATCH 084/255] 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 a98bdd26f7..1c09111443 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) - Return empty array instead of 404 when commit has no statuses in commit status API diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 4d3e48f7f8..668396a0f2 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -55,7 +55,7 @@ class Admin::GroupsController < Admin::ApplicationController private def group - @group = Group.find_by(path: params[:id]) + @group ||= Group.find_by(path: params[:id]) end def group_params From cb5a5ba09588af866aed595d960755bf4ced4483 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 8 Mar 2016 17:38:23 +0100 Subject: [PATCH 085/255] 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 ff48f993d4..d3ca268a65 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -160,7 +160,7 @@ class Repository end def rm_tag(tag_name) - expire_tags_cache + before_remove_tag gitlab_shell.rm_tag(path_with_namespace, tag_name) end @@ -183,6 +183,14 @@ class Repository end end + def branch_count + @branch_count ||= cache.fetch(:branch_count) { raw_repository.branch_count } + end + + def tag_count + @tag_count ||= cache.fetch(:tag_count) { raw_repository.rugged.tags.count } + end + # Return repo size in megabytes # Cached in redis def size @@ -278,6 +286,16 @@ class Repository @has_visible_content = nil end + def expire_branch_count_cache + cache.expire(:branch_count) + @branch_count = nil + end + + def expire_tag_count_cache + cache.expire(:tag_count) + @tag_count = nil + end + def rebuild_cache cache_keys.each do |key| cache.expire(key) @@ -313,9 +331,16 @@ class Repository expire_root_ref_cache end - # Runs code before creating a new tag. - def before_create_tag + # Runs code before pushing (= creating or removing) a tag. + def before_push_tag expire_cache + expire_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 @@ class Repository # 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 a62c5fc4fc..c88c767280 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 882a4d0c5e..a21ddaf493 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 498c5e05b3..7a5b0d993d 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 150422ac34..7af8070469 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -405,7 +405,7 @@ describe Repository, models: true do end end - describe '#expire_branch_ache' do + describe '#expire_branch_cache' do # This method is private but we need it for testing purposes. Sadly there's # no other proper way of testing caching operations. let(:cache) { repository.send(:cache) } @@ -556,11 +556,12 @@ describe Repository, models: true do end end - describe '#before_create_tag' do + describe '#before_push_tag' do it 'flushes the cache' do expect(repository).to receive(:expire_cache) + expect(repository).to receive(:expire_tag_count_cache) - repository.before_create_tag + repository.before_push_tag end end @@ -607,4 +608,44 @@ describe Repository, models: true do 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 0000000000..5b7ba52181 --- /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 From 177025b5dde5ca34b05744f000a1eee64501863a Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 8 Mar 2016 17:53:00 +0100 Subject: [PATCH 086/255] 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 d3ca268a65..c3ae461a01 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -144,7 +144,7 @@ class Repository 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 @@ class Repository rugged.branches.delete(branch_name) end - expire_branches_cache + after_remove_branch true end @@ -359,10 +359,16 @@ class Repository 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 7af8070469..b2b4d38756 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -648,4 +648,15 @@ describe Repository, models: true do 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 From 6857b92fabe511c4ed32310ad0f6bf43c534ffab Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 8 Mar 2016 17:53:23 +0100 Subject: [PATCH 087/255] 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 b2b4d38756..40cbb3d635 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -659,4 +659,15 @@ describe Repository, models: true do 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 From 4ec035b48869d8364f6ae8cb14c8486075786757 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 8 Mar 2016 18:01:16 +0100 Subject: [PATCH 088/255] 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 c3ae461a01..c0730b3431 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -138,7 +138,7 @@ class Repository 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 @@ class Repository # 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 40cbb3d635..97778c354a 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -649,6 +649,17 @@ describe Repository, models: true do 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) } From 590e1b4b21f2a39bf573800392b04859b563f3e5 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 8 Mar 2016 18:04:00 +0100 Subject: [PATCH 089/255] 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 c0730b3431..6441cd87e8 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -133,7 +133,7 @@ class Repository rugged.branches.create(branch_name, target) end - expire_branches_cache + after_create_branch find_branch(branch_name) end @@ -356,6 +356,7 @@ class Repository # Runs code after a new branch has been created. def after_create_branch + expire_branches_cache expire_has_visible_content_cache expire_branch_count_cache end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 97778c354a..34866be339 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -148,6 +148,12 @@ describe Repository, models: true do expect(branch.name).to eq('new_feature') end + + it 'calls the after_create_branch hook' do + expect(repository).to receive(:after_create_branch) + + repository.add_branch(user, 'new_feature', 'master') + end end context 'when pre hooks failed' do From 7000e76a01f20fcb739587cb446610c4076bf910 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 8 Mar 2016 16:39:53 -0500 Subject: [PATCH 090/255] 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 b493a6968f..0f13b2c102 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 154442bd3d..d681a2c848 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 b9127b3d75..365f12a0c9 100644 --- a/spec/factories/snippets.rb +++ b/spec/factories/snippets.rb @@ -12,5 +12,17 @@ FactoryGirl.define do title content file_name + + trait :public do + visibility_level Snippet::PUBLIC + end + + trait :internal do + visibility_level Snippet::INTERNAL + end + + trait :private do + visibility_level Snippet::PRIVATE + end end end From b900304c3867454ac11e12d303a318ae7494f93e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 8 Mar 2016 16:40:45 -0500 Subject: [PATCH 091/255] 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 883bbaedd4..70ed8f3a62 100644 --- a/spec/controllers/projects/forks_controller_spec.rb +++ b/spec/controllers/projects/forks_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Projects::ForksController do let(:user) { create(:user) } - let(:project) { create(:project, visibility_level: Project::PUBLIC) } + let(:project) { create(:project, :public) } let(:forked_project) { Projects::ForkService.new(project, user).execute } let(:group) { create(:group, owner: forked_project.creator) } diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index 1b4ffc2d71..7fdc5e5d7a 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -5,15 +5,14 @@ describe SnippetsFinder do let(:user1) { create :user } let(:group) { create :group } - let(:project1) { create(:empty_project, :public, group: group) } - let(:project2) { create(:empty_project, :private, group: group) } - + let(:project1) { create(:empty_project, :public, group: group) } + let(:project2) { create(:empty_project, :private, group: group) } context ':all filter' do before do - @snippet1 = create(:personal_snippet, visibility_level: Snippet::PRIVATE) - @snippet2 = create(:personal_snippet, visibility_level: Snippet::INTERNAL) - @snippet3 = create(:personal_snippet, visibility_level: Snippet::PUBLIC) + @snippet1 = create(:personal_snippet, :private) + @snippet2 = create(:personal_snippet, :internal) + @snippet3 = create(:personal_snippet, :public) end it "returns all private and internal snippets" do @@ -31,9 +30,9 @@ describe SnippetsFinder do context ':by_user filter' do before do - @snippet1 = create(:personal_snippet, visibility_level: Snippet::PRIVATE, author: user) - @snippet2 = create(:personal_snippet, visibility_level: Snippet::INTERNAL, author: user) - @snippet3 = create(:personal_snippet, visibility_level: Snippet::PUBLIC, author: user) + @snippet1 = create(:personal_snippet, :private, author: user) + @snippet2 = create(:personal_snippet, :internal, author: user) + @snippet3 = create(:personal_snippet, :public, author: user) end it "returns all public and internal snippets" do @@ -75,9 +74,9 @@ describe SnippetsFinder do context 'by_project filter' do before do - @snippet1 = create(:project_snippet, visibility_level: Snippet::PRIVATE, project: project1) - @snippet2 = create(:project_snippet, visibility_level: Snippet::INTERNAL, project: project1) - @snippet3 = create(:project_snippet, visibility_level: Snippet::PUBLIC, project: project1) + @snippet1 = create(:project_snippet, :private, project: project1) + @snippet2 = create(:project_snippet, :internal, project: project1) + @snippet3 = create(:project_snippet, :public, project: project1) end it "returns public snippets for unauthorized user" do @@ -93,7 +92,7 @@ describe SnippetsFinder do end it "returns all snippets for project members" do - project1.team << [user, :developer] + project1.team << [user, :developer] snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) expect(snippets).to include(@snippet1, @snippet2, @snippet3) end diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb index aafc24397a..cd7596a763 100644 --- a/spec/helpers/visibility_level_helper_spec.rb +++ b/spec/helpers/visibility_level_helper_spec.rb @@ -58,7 +58,7 @@ describe VisibilityLevelHelper do describe "skip_level?" do describe "forks" do - let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } + let(:project) { create(:project, :internal) } let(:fork_project) { create(:forked_project_with_submodules) } before do @@ -74,7 +74,7 @@ describe VisibilityLevelHelper do end describe "non-forked project" do - let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } + let(:project) { create(:project, :internal) } it "skips levels" do expect(skip_level?(project, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey @@ -84,7 +84,7 @@ describe VisibilityLevelHelper do end describe "Snippet" do - let(:snippet) { create(:snippet, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } + let(:snippet) { create(:snippet, :internal) } it "skips levels" do expect(skip_level?(snippet, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f9842d23af..c458d9c9b1 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -561,7 +561,7 @@ describe Project, models: true do end describe '#visibility_level_allowed?' do - let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL } + let(:project) { create(:project, :internal) } context 'when checking on non-forked project' do it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy } diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 3c06a89016..e8b9e6b923 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -102,8 +102,8 @@ describe Projects::UpdateService, services: true do describe :visibility_level do let(:user) { create :user, admin: true } - let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL } - let(:forked_project) { create :forked_project_with_submodules, visibility_level: Gitlab::VisibilityLevel::INTERNAL } + let(:project) { create(:project, :internal) } + let(:forked_project) { create(:forked_project_with_submodules, :internal) } let(:opts) { {} } before do From 5844a21a0acae08a19fa82984dcc0feb1b8777c5 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 15 Feb 2016 21:17:20 -0500 Subject: [PATCH 092/255] 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 | 41 ++++++++++++------- 3 files changed, 54 insertions(+), 20 deletions(-) create mode 100644 lib/gitlab/devise_failure.rb diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index d82cfb3ec0..31dceaebca 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -203,11 +203,11 @@ Devise.setup do |config| # 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 0000000000..a78fde9d78 --- /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 09ec4f18f9..0ddbec9eac 100644 --- a/spec/controllers/projects/repositories_controller_spec.rb +++ b/spec/controllers/projects/repositories_controller_spec.rb @@ -2,30 +2,41 @@ require "spec_helper" 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] + 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" + + expect(response.content_type).to start_with 'text/html' + expect(response).to be_redirect + end end - it "uses Gitlab::Workhorse" do - expect(Gitlab::Workhorse).to receive(:send_git_archive).with(project, "master", "zip") - - get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" - 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") + + get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" end - it "renders Not Found" do - get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" + context "when the service raises an error" do - expect(response.status).to eq(404) + 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) + end end end end From 0853628cbf982bc3191b3ee7efa42044fb8db9c2 Mon Sep 17 00:00:00 2001 From: Jonas Friedmann Date: Wed, 9 Mar 2016 08:26:39 +0100 Subject: [PATCH 093/255] 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 fd0327686b..5fa39ef1b0 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. From d0c5ac4ae3a5900e2619727b47114ace31a6c7ff Mon Sep 17 00:00:00 2001 From: Florent Baldino Date: Wed, 9 Mar 2016 09:30:50 +0100 Subject: [PATCH 094/255] 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 3aa61de2f5..2a0b947944 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 e6e3bfbea7..9fc2372762 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 From 9d9d2fbbc23e030416a6ebab556e87cf7014a057 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 9 Mar 2016 12:00:17 +0100 Subject: [PATCH 095/255] 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 c0988eca6d..bac9e244d3 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 787fe443ac..a7e3c2478c 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. From c666441d405c1275ef7bcc791153f09d819cc89d Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 9 Mar 2016 10:41:41 +0100 Subject: [PATCH 096/255] 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 2d6b9b5a4c..4f56f3970d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -697,6 +697,7 @@ ActiveRecord::Schema.define(version: 20160305220806) do t.integer "build_timeout", default: 3600, null: false t.boolean "pending_delete", default: false t.boolean "public_builds", default: true, null: false + t.string "main_language" end add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree From 640bf8ab7212988fda0189e1534d46dcbf0c126f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 12 Nov 2015 10:52:20 +0100 Subject: [PATCH 097/255] 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 c477721f9d..f77b5bdc95 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 c66ef3cffa..a5c244fb7b 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 22c86e4ae8..7f3c5911e5 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 8f381f46e5..159fb96417 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -15,6 +15,7 @@ require 'rspec/rails' require 'shoulda/matchers' require 'sidekiq/testing/inline' require 'benchmark/ips' +require 'rspec/retry' # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. @@ -25,6 +26,9 @@ RSpec.configure do |config| config.use_instantiated_fixtures = false config.mock_with :rspec + config.verbose_retry = true + config.display_try_failure_messages = true + config.include Devise::TestHelpers, type: :controller config.include LoginHelpers, type: :feature config.include LoginHelpers, type: :request From af43401602d637c5b91def323ba3a0bac824b785 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 9 Mar 2016 14:12:08 +0100 Subject: [PATCH 098/255] 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 a5c244fb7b..9aa76f92aa 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 7f3c5911e5..dcfef4cd6b 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 62c80b9c94..357d164d87 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -14,6 +14,7 @@ require 'sidekiq/testing/inline' require_relative 'capybara' require_relative 'db_cleaner' +require_relative 'rerun' %w(select2_helper test_env repo_helpers).each do |f| require Rails.root.join('spec', 'support', f) diff --git a/features/support/rerun.rb b/features/support/rerun.rb new file mode 100644 index 0000000000..8b176c5be8 --- /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 3acfc6e207..01d23b89bb 100644 --- a/lib/tasks/spinach.rake +++ b/lib/tasks/spinach.rake @@ -4,53 +4,59 @@ namespace :spinach do namespace :project do desc "GitLab | Spinach | Run project commits, issues and merge requests spinach features" task :half do - cmds = [ - %W(rake gitlab:setup), - %W(spinach --tags @project_commits,@project_issues,@project_merge_requests), - ] - run_commands(cmds) + run_spinach_tests('@project_commits,@project_issues,@project_merge_requests') end desc "GitLab | Spinach | Run remaining project spinach features" task :rest do - cmds = [ - %W(rake gitlab:setup), - %W(spinach --tags ~@admin,~@dashboard,~@profile,~@public,~@snippets,~@project_commits,~@project_issues,~@project_merge_requests), - ] - run_commands(cmds) + run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets,~@project_commits,~@project_issues,~@project_merge_requests') end end desc "GitLab | Spinach | Run project spinach features" task :project do - cmds = [ - %W(rake gitlab:setup), - %W(spinach --tags ~@admin,~@dashboard,~@profile,~@public,~@snippets), - ] - run_commands(cmds) + run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets') end desc "GitLab | Spinach | Run other spinach features" task :other do - cmds = [ - %W(rake gitlab:setup), - %W(spinach --tags @admin,@dashboard,@profile,@public,@snippets), - ] - run_commands(cmds) + run_spinach_tests('@admin,@dashboard,@profile,@public,@snippets') + end + + desc "GitLab | Spinach | Run other spinach features" + task :builds do + run_spinach_tests('@builds') end end desc "GitLab | Run spinach" task :spinach do - cmds = [ - %W(rake gitlab:setup), - %W(spinach), - ] - run_commands(cmds) + run_spinach_tests(nil) end -def run_commands(cmds) - cmds.each do |cmd| - system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) or raise("#{cmd} failed!") +def run_command(cmd) + system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd) +end + +def run_spinach_command(args) + run_command(%w(spinach -r rerun) + args) +end + +def run_spinach_tests(tags) + #run_command(%w(rake gitlab:setup)) or raise('gitlab:setup failed!') + + success = run_spinach_command(%W(--tags #{tags})) + 3.times do |_| + break if success + break unless File.exists?('tmp/spinach-rerun.txt') + + tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp) + puts '' + puts "Spinach tests for #{tags}: Retrying tests... #{tests}".red + puts '' + sleep(3) + success = run_spinach_command(tests) end + + raise("spinach tests for #{tags} failed!") unless success end From 978703abe287519784b8258296ca71d783f67794 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 9 Mar 2016 14:12:29 +0100 Subject: [PATCH 099/255] Retry apt-get installation and bundle install --- .gitlab-ci.yml | 2 +- scripts/prepare_build.sh | 30 +++++++++++++++++++++--------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f77b5bdc95..ffefeb6dfd 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 b6f076a90c..bce9bce99c 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 - wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb - mv phantomjs_1.9.8-0jessie_amd64.deb vendor/ - fi - dpkg -i vendor/phantomjs_1.9.8-0jessie_amd64.deb - apt-get update -qq - apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \ - libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip + # Install phantomjs package + pushd vendor + if [ ! -e phantomjs_1.9.8-0jessie_amd64.deb ]; then + wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb + fi + dpkg -i phantomjs_1.9.8-0jessie_amd64.deb + popd + + # Try to install packages + 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..." + 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 From b651cd227efae152ef56c112576c5464af4ee725 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 9 Mar 2016 14:26:49 +0100 Subject: [PATCH 100/255] 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 bce9bce99c..82de51a9a2 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 From 9e342fb00a50cbb483013acd4644bd31c440f99a Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 9 Mar 2016 11:06:58 -0300 Subject: [PATCH 101/255] 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 3235a1cee5..426464dee8 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -151,6 +151,7 @@ class Project < ActiveRecord::Base has_many :releases, dependent: :destroy has_many :lfs_objects_projects, dependent: :destroy has_many :lfs_objects, through: :lfs_objects_projects + has_many :todos, dependent: :destroy has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index c458d9c9b1..2fa38a5d3d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -68,6 +68,7 @@ describe Project, models: true do it { is_expected.to have_many(:runners) } it { is_expected.to have_many(:variables) } it { is_expected.to have_many(:triggers) } + it { is_expected.to have_many(:todos).dependent(:destroy) } end describe 'modules' do From c46e6eff104891ca4e404605ad3bf302a3aad9c7 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 9 Mar 2016 15:16:46 +0100 Subject: [PATCH 102/255] 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 7fd75ebe69..d8d1e7b467 100644 --- a/config/application.rb +++ b/config/application.rb @@ -4,6 +4,7 @@ require 'rails/all' 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 @@ module Gitlab 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 0fc725842b..3da5d46be9 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -13,9 +13,12 @@ end 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 dcf6ce74d9..cc83137745 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 @@ end 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 f266a70ee0..aed55f74ea 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 0000000000..eea51d5207 --- /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 f221afcf73..51e746ef92 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 From 22140ced0b881617ef376e21ba28c9bb9b6c5cc4 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 9 Mar 2016 11:18:13 -0300 Subject: [PATCH 103/255] 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 0000000000..ebe0fc8230 --- /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 4f56f3970d..a74b86d8e2 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" From cac83cead523bc5a314950c2168fcebf8da8d405 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 9 Mar 2016 11:18:37 -0300 Subject: [PATCH 104/255] Update CHANGELOG --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 10984ebd19..3c19e8477f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -24,6 +24,9 @@ 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 +v 8.5.5 + - Fix error 500 in Todos + v 8.5.4 - Do not cache requests for badges (including builds badge) From a12319c111be3963b61f0d85ceaabd1d1414f645 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 9 Mar 2016 15:56:44 +0100 Subject: [PATCH 105/255] 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 eea51d5207..4949c6db53 100644 --- a/lib/gitlab/redis_config.rb +++ b/lib/gitlab/redis_config.rb @@ -27,4 +27,4 @@ module Gitlab end end end -end \ No newline at end of file +end From 96d35c5975fe8c5ff0a6306b8da5a623cfcb47b6 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 9 Mar 2016 13:49:05 +0100 Subject: [PATCH 106/255] 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 f5c51e46e8..7fee16a058 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -271,22 +271,24 @@ describe GitPushService, services: true do allow(project.repository).to receive(:commits_between). and_return([closing_commit]) + + project.team << [commit_author, :master] end context "to default branches" do it "closes issues" do - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, commit_author, @oldrev, @newrev, @ref ) expect(Issue.find(issue.id)).to be_closed end it "adds a note indicating that the issue is now closed" do expect(SystemNoteService).to receive(:change_status).with(issue, project, commit_author, "closed", closing_commit) - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, commit_author, @oldrev, @newrev, @ref ) end it "doesn't create additional cross-reference notes" do expect(SystemNoteService).not_to receive(:cross_reference) - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, commit_author, @oldrev, @newrev, @ref ) end it "doesn't close issues when external issue tracker is in use" do @@ -294,7 +296,7 @@ describe GitPushService, services: true do # The push still shouldn't create cross-reference notes. expect do - execute_service(project, user, @oldrev, @newrev, 'refs/heads/hurf' ) + execute_service(project, commit_author, @oldrev, @newrev, 'refs/heads/hurf' ) end.not_to change { Note.where(project_id: project.id, system: true).count } end end @@ -316,7 +318,6 @@ describe GitPushService, services: true do end end - # EE-only tests context "for jira issue tracker" do include JiraServiceHelper From 53719ecb80b757a2096e55cd3da995ac9db8d3b8 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 9 Mar 2016 15:38:07 +0100 Subject: [PATCH 107/255] 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 f34554d557..fe9e0aab71 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -9,6 +9,7 @@ class Ability when CommitStatus then commit_status_abilities(user, subject) when Project then project_abilities(user, subject) when Issue then issue_abilities(user, subject) + when ExternalIssue then external_issue_abilities(user, subject) when Note then note_abilities(user, subject) when ProjectSnippet then project_snippet_abilities(user, subject) when PersonalSnippet then personal_snippet_abilities(user, subject) @@ -424,6 +425,10 @@ class Ability end end + def external_issue_abilities(user, subject) + project_abilities(user, subject.project) + end + private def named_abilities(name) diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 7fee16a058..82813cee22 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -367,7 +367,7 @@ describe GitPushService, services: true do } }.to_json - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, commit_author, @oldrev, @newrev, @ref ) expect(WebMock).to have_requested(:post, jira_api_transition_url).with( body: transition_body ).once @@ -378,7 +378,7 @@ describe GitPushService, services: true do body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, commit_author, @oldrev, @newrev, @ref ) expect(WebMock).to have_requested(:post, jira_api_comment_url).with( body: comment_body ).once From a603422edb69248c822dd6c4a9d7860194d89a3d Mon Sep 17 00:00:00 2001 From: Baldinof Date: Wed, 9 Mar 2016 17:13:21 +0000 Subject: [PATCH 108/255] 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 9fc2372762..da3c3711cd 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 From e443c7c6d861391f906e6124188aa305b59a9e63 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 8 Mar 2016 16:13:51 -0500 Subject: [PATCH 109/255] 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 0000000000..ebe301c1fc --- /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 From 7500a12aae91231aba1021df2034d724126ba55f Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 9 Mar 2016 14:47:04 -0300 Subject: [PATCH 110/255] Avoid error 500 when todo author was removed --- app/helpers/application_helper.rb | 2 +- app/views/dashboard/todos/_todo.html.haml | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f0aa2b5712..368969c647 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -72,7 +72,7 @@ module ApplicationHelper if user_or_email.is_a?(User) user = user_or_email else - user = User.find_by(email: user_or_email.downcase) + user = User.find_by(email: user_or_email.try(:downcase)) end if user diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index f878d36e73..45cfe3da18 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) From acd9bc0213098a5626499021eb72595eb556f9be Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 9 Mar 2016 19:11:24 +0100 Subject: [PATCH 111/255] 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 fb74919ea2..bc8019193e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -246,6 +246,8 @@ class ApplicationController < ActionController::Base 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 0000000000..ef77a24cf7 --- /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) + # => # + # [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 da4435c730..29347c05b7 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -7,6 +7,12 @@ module Gitlab 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 4885baf952..46ac5825fd 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -3,8 +3,9 @@ module Gitlab 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 From b100a9f206ce16391038bf7c2b0bd247b1bfe822 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Wed, 9 Mar 2016 16:21:53 -0300 Subject: [PATCH 112/255] Update CHANGELOG --- CHANGELOG | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 3c19e8477f..75ea12320b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -25,7 +25,8 @@ v 8.6.0 (unreleased) - Add ability to show archived projects on dashboard, explore and group pages 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) From 639cad7e3034c895f7f38653b27f1e37e6ee34c2 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Wed, 9 Mar 2016 20:43:54 +0100 Subject: [PATCH 113/255] Ignore .byebug_history --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1eb785451f..8f861d76a3 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ .sass-cache/ .secret .vagrant +.byebug_history Vagrantfile backups/* config/aws.yml From 3da55e37d0d3025893fedb4b87bebd0f2b4635f1 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 9 Mar 2016 21:22:25 +0100 Subject: [PATCH 114/255] 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 ffefeb6dfd..d21785f7af 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 82de51a9a2..4a7ee7dbb6 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 From 0ca2edda56be45088ffca6a85a844ee1efd98137 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 9 Mar 2016 18:54:15 -0500 Subject: [PATCH 115/255] 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 d7e4153ddc..e6609ac710 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; } From 449954ecc2ad0c1796b2949b79b56329c6ce2447 Mon Sep 17 00:00:00 2001 From: Josh Frye Date: Wed, 9 Mar 2016 20:58:41 -0500 Subject: [PATCH 116/255] 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 9474ca50b2..45f8e3ab3d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -24,6 +24,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 + - 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 fc51c3241a..0e8b63872c 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -8,7 +8,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController @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 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController @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 5b811db306..8271ca8743 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -8,7 +8,7 @@ class Explore::ProjectsController < Explore::ApplicationController @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 @@ class Explore::ProjectsController < Explore::ApplicationController 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 @@ class Explore::ProjectsController < Explore::ApplicationController @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 From e32726c63661a9f1ea0578da3b029943bae4df3b Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 8 Mar 2016 15:56:19 +0100 Subject: [PATCH 117/255] 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 52271c7c8c..7f44ca2f7d 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -105,6 +105,31 @@ describe Issue, models: true do 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) } From c6f516626d1cac6947a298a89594be6e486a50a2 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 7 Mar 2016 11:22:13 +0000 Subject: [PATCH 118/255] 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 3dc524ccca..2e847a345f 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 a2c0a85893..1ec3b0e485 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 From 8651137a4fb616441b2d30229f724d1fd0d48bc5 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 7 Mar 2016 11:26:35 +0000 Subject: [PATCH 119/255] 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 2e847a345f..dbf1131681 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 cc84a5ff93..aad7ca1a39 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 1ec3b0e485..97a27e373e 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 From 22b89f35c74c9a27b7d41be3fe11a0a5fcbd059f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 7 Mar 2016 11:50:39 +0000 Subject: [PATCH 120/255] 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 dbf1131681..e5e1c8310e 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 aad7ca1a39..6561b3de7c 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 97a27e373e..0123834bc3 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 From cda0b7e1b142130fd2e4d7073e451a3f8d73b693 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 10 Mar 2016 10:41:16 +0100 Subject: [PATCH 121/255] 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 bc8019193e..15fee9948e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -246,7 +246,7 @@ class ApplicationController < ActionController::Base 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 0000000000..f801e8b60b --- /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 ef77a24cf7..0000000000 --- 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) - # => # - # [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 29347c05b7..76786169a4 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -10,7 +10,7 @@ module Gitlab 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) From 0223b58f019fc3ce906c97caf8c6e361fa4302be Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 10 Mar 2016 11:05:59 +0100 Subject: [PATCH 122/255] 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 76786169a4..90d5996757 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -7,10 +7,14 @@ module Gitlab 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) From d69dff5b4b1c2dd4944269703ff7af32869adf13 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 9 Mar 2016 11:39:48 +0100 Subject: [PATCH 123/255] 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 d21785f7af..bd013d50fa 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 f5c3107ff4..1b281809af 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 88e18ee95f..0000000000 --- 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 0985ef3a66..2cf7a25a0f 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 b57a33004a..0000000000 --- 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 551ce21840..0000000000 --- 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 3855763b20..0000000000 --- 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 -

Hello @alice and @bob, how are you doing today?

-

This is simple @dummy text to see how the @ReferenceFilter class performs -when @processing HTML.

- 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 a94afc4c40..0000000000 --- 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 cee0949edc..0000000000 --- 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 8b039ef731..0000000000 --- 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 1be7a8d3ed..0000000000 --- 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 25ed48c34f..0000000000 --- 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 159fb96417..7d939ca750 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,7 +14,6 @@ require File.expand_path("../../config/environment", __FILE__) 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 @@ RSpec.configure do |config| 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 84f655c211..0000000000 --- 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 From f17de3c2eb914ba0e7b513d9cc46694875491720 Mon Sep 17 00:00:00 2001 From: Roberto Dip Date: Mon, 15 Feb 2016 14:41:02 -0300 Subject: [PATCH 124/255] 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 9474ca50b2..1c27c3388c 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 48570abff4..9381cb3281 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; From 3c6fdc7a0a2804dd9d1bef11a1a6b1eab3b1898c Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 10 Mar 2016 11:02:22 +0000 Subject: [PATCH 125/255] 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 c95ead22e6..863a4edfad 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 @@ -72,7 +72,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" @@ -347,22 +347,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 @@ -371,7 +375,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 @@ -383,7 +388,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 b5f076088c..13e624764d 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' From e7df3f51c9cc246279da36c9488b4c591f30b866 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 10 Mar 2016 12:37:14 +0100 Subject: [PATCH 126/255] 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 15fee9948e..1f55b18e0b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -246,7 +246,7 @@ class ApplicationController < ActionController::Base 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 3098d49d58..505a547d8e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -612,6 +612,13 @@ class User < ActiveRecord::Base 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 f801e8b60b..0ed4c35792 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 90d5996757..da4435c730 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -7,16 +7,6 @@ module Gitlab 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 46ac5825fd..d1b42c1f9b 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -3,9 +3,8 @@ module Gitlab 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 From 662c4df74826d123f354e408583f24c24cd1761b Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 10 Mar 2016 12:52:19 +0100 Subject: [PATCH 127/255] 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 0000000000..aacd170716 --- /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 From 573bbd674b7bc14d50ccfa04aa2af9021f68de2b Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 10 Mar 2016 11:52:31 +0000 Subject: [PATCH 128/255] 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 14ee2263b7..6a60cfeff7 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') From a28c84b4cab48d4ca7cc39985e1e67155a8a95d5 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 10 Mar 2016 12:55:06 +0100 Subject: [PATCH 129/255] 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 aacd170716..fbdb7ea34a 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 From 66d5d9229de499455ca1c28f1718cec456884685 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 10 Mar 2016 12:58:51 +0100 Subject: [PATCH 130/255] 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 0ed4c35792..8bf6f13b99 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:) From a0c4f3dfa90bf3c7c51af2b5d7c0fcbce45d6280 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 10 Mar 2016 12:15:00 +0000 Subject: [PATCH 131/255] 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 e0e41fc4be..c84614c087 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 From ec52d5b14f761170cce24fa9f34b1f24f87043d3 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 10 Mar 2016 13:41:23 +0100 Subject: [PATCH 132/255] Fix projects pagination via ajax Signed-off-by: Dmitriy Zaporozhets --- 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 ed5206368c..a0d7dc80af 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) + ) From 83593e461a7925836f81ebf47454f942d55e7fbd Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 10 Mar 2016 13:55:01 +0100 Subject: [PATCH 133/255] Use modern syntax Signed-off-by: Dmitriy Zaporozhets --- 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 a0d7dc80af..e4c4bf3b27 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) ) From 2cd401676faada31530c5de3d206b5322cfcb3c2 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 10 Mar 2016 13:17:35 +0000 Subject: [PATCH 134/255] 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 d8bee21c82..10456189e2 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -42,12 +42,12 @@ module CiStatusHelper 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 640a1962ff..c6da6f7997 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" From e3efce9237ea15c7b1c609a364d3dac927ac6286 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 10 Mar 2016 14:29:38 +0100 Subject: [PATCH 135/255] Move group activity feed to separate page for consistency with dashboard and project pages Signed-off-by: Dmitriy Zaporozhets --- 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 9474ca50b2..e2846ee48d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -24,6 +24,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 + - 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 54b28f2dd8..ee81fee586 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 f05c29e997..360930f95a 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 @@ class GroupsController < Groups::ApplicationController 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 0000000000..dc76599b77 --- /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 0000000000..10b432ef03 --- /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 6148d8cb3d..3cf0a4baac 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 e5e2a59eae..59411ae1da 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 a918b5bd3f..869fca03ec 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -382,7 +382,7 @@ Rails.application.routes.draw do 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 a60c3860b8..419a5d3963 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 da9d1503eb..2bd8ea745e 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 From 704f46837627df405d2ffcf0bd9b74faf8f72f86 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 10 Mar 2016 13:34:31 +0000 Subject: [PATCH 136/255] 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 7d414ae003..639d639d5b 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 c84614c087..773ce8ac24 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) From 90ffb1c0dbed6f695cedb3b7ded2a1769136f238 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 10 Mar 2016 14:35:23 +0100 Subject: [PATCH 137/255] Fix header title Signed-off-by: Dmitriy Zaporozhets --- 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 10b432ef03..f73e1d9e86 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' From 4407532911c2fb4841cc51f9779ef51ccb6de099 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 7 Mar 2016 15:37:35 +0000 Subject: [PATCH 138/255] 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 0000000000..f15d65473c --- /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 = "
    #{html}
" + + 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 = "
  • " + html += "" + html += text + html += "" + html += "
  • " + + return html + + noResults: -> + html = "
  • " + html += "" + html += "No matching results." + html += "" + html += "
  • " + + 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 = "" + @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 0000000000..09f4ad0aac --- /dev/null +++ b/app/assets/javascripts/lib/fuse.js @@ -0,0 +1,20 @@ +/** + * @license + * Fuse - Lightweight fuzzy-search + * + * Copyright (c) 2012-2016 Kirollos Risk . + * 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<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;SS;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;Ss;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;ss;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;eL.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;ea;)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<=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 e5e1c8310e..1ef9d001ed 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 0000000000..c96b6eeebd --- /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 0123834bc3..db46e51f8d 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 From cae03f8b2bc13c4bba3a7905776a04580efe300d Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 7 Mar 2016 15:41:05 +0000 Subject: [PATCH 139/255] 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 c96b6eeebd..a5728968ae 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 From 11b52e287be32ccd2eed166e4622f1a6b05c9ba7 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 7 Mar 2016 17:17:11 +0000 Subject: [PATCH 140/255] 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 3e0fdb3f79..466b086259 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 f15d65473c..10b8fd07a0 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 = "" - @dropdown.before input + if value isnt 0 + # Create hidden input for form + input = "" + @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 0000000000..c240af0b04 --- /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) -> + "
  • + + #{label.name} + #{label.name} + +
  • " + 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 9467011799..2b5c2f4381 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 "" + + "
  • + + + + #{user.name} + + + #{username} + + +
  • " + ) + $('.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 1ef9d001ed..770534061b 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 a5728968ae..49c40e3117 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 e55159d996..2be1ee1f72 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()); From 7bbab5faa1c2a7b78600f270f25f7ae210611350 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 7 Mar 2016 17:18:53 +0000 Subject: [PATCH 141/255] 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 770534061b..357423709b 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; } } From 04fc44841d3fd4e26d977b88fc1d187f1da79568 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 7 Mar 2016 17:19:36 +0000 Subject: [PATCH 142/255] 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 c240af0b04..8173671364 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) -> "
  • - #{label.name} #{label.name}
  • " From d6e5ed8a97b20eff242b394999a71fb828822d50 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 7 Mar 2016 17:28:37 +0000 Subject: [PATCH 143/255] 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 357423709b..ba65a8b17c 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 49c40e3117..5e3ee73431 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 @@ module DropdownsHelper 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 2be1ee1f72..18d3f0e506 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' From 562a09c1277270a72be580cf04c600e38d0dac06 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 8 Mar 2016 08:38:16 +0000 Subject: [PATCH 144/255] 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 466b086259..987815b6bd 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 10b8fd07a0..8ab82dc252 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 8173671364..f79a877353 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 "" + "
  • - + #{label.name}
  • " diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee new file mode 100644 index 0000000000..184479e209 --- /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 18d3f0e506..402fc7535b 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()); From 8ca880c3b8d677366ed20dd1258a1b780cbd05a8 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 8 Mar 2016 09:09:39 +0000 Subject: [PATCH 145/255] 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 8ab82dc252..c00facd921 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 f79a877353..56fe6dc9a7 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 184479e209..4256158660 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 2b5c2f4381..77db99a617 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 5e3ee73431..73b3e70f3c 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -17,7 +17,7 @@ module DropdownsHelper 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 402fc7535b..0a8ec7cde5 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 From 5cdac7614dff659fad5a4364440d956bb48253c9 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 8 Mar 2016 10:24:03 +0000 Subject: [PATCH 146/255] 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 c00facd921..18b90d32c9 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 "
  • " 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 4256158660..aa0d632561 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 77db99a617..d4e41ffa5e 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} #{user.name} diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index ba65a8b17c..27db075e25 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 1c78aafdb8..8c49f3a923 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 73b3e70f3c..73a8794c25 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -5,7 +5,7 @@ module DropdownsHelper 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 0a8ec7cde5..a336a540eb 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' From 75b21ebd75ddf9afef6939d1af65219110f463cd Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 8 Mar 2016 11:23:54 +0000 Subject: [PATCH 147/255] 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 987815b6bd..36c6a76e09 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 18b90d32c9..2d69280c9e 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 56fe6dc9a7..5582bf457e 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 27db075e25..dce2b3b99c 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 8c49f3a923..5ec0966194 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 a336a540eb..b9f9fb4923 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') From 2e3c082f2a07d93de2f552d6504f33204e93a447 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 8 Mar 2016 11:58:14 +0000 Subject: [PATCH 148/255] 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 2d69280c9e..c65b3e3145 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 = "" @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 0000000000..c5740f27dd --- /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 5582bf457e..0b4e2ad7aa 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 aa0d632561..9c45800af6 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 d4e41ffa5e..2c6faf4bc1 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 b9f9fb4923..b261ca498c 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()); From 730af552682febb3f7ce010bbc0a30b3bff5014c Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 8 Mar 2016 11:59:52 +0000 Subject: [PATCH 149/255] 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 dce2b3b99c..6d3e48437b 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; From dd35de1ad8e6ff8436552666d3f9f9bec0a309d9 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 8 Mar 2016 12:32:04 +0000 Subject: [PATCH 150/255] 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 36c6a76e09..2ddf8612db 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 c65b3e3145..a714ba3d67 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 0b4e2ad7aa..14ee529ba3 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 "" "
  • - #{label.name} + #{label.title}
  • " 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 9c45800af6..92b42bfe86 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 b261ca498c..0e023a6809 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 From 7744b67f7518727dceb83682402772432870cdab Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 8 Mar 2016 15:18:01 +0000 Subject: [PATCH 151/255] 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 a714ba3d67..591e0b6f41 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 14ee529ba3..b4708f6537 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 " 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 0e023a6809..d7db2ed6e5 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') From 0e82bcad8af991b22e7423b0bc4170cdbe0c8216 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 8 Mar 2016 15:34:35 +0000 Subject: [PATCH 152/255] 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 2c6faf4bc1..987c6f4b8d 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') From 57294f3043300e43d636d0fd327e3ccd36f2f307 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 8 Mar 2016 15:45:03 +0000 Subject: [PATCH 153/255] 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 591e0b6f41..a162955d39 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 09f4ad0aac..0000000000 --- a/app/assets/javascripts/lib/fuse.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @license - * Fuse - Lightweight fuzzy-search - * - * Copyright (c) 2012-2016 Kirollos Risk . - * 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<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;SS;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;Ss;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;ss;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;eL.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;ea;)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<=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); From 6c8919627824648a8a4275832c67dedff5bd3e3f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 8 Mar 2016 16:53:31 +0000 Subject: [PATCH 154/255] 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 a162955d39..420f67ef19 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 = "" @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 6d3e48437b..009d621fc7 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; From f3afcb87b74685edb91eb9cfa839758275af9a0e Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 8 Mar 2016 17:33:45 +0000 Subject: [PATCH 155/255] Correctly handles multi-selected values Fixes an issue where values couldnlt be unselected --- app/assets/javascripts/gl_dropdown.js.coffee | 42 ++++++++++++------- .../javascripts/labels_select.js.coffee | 13 +++++- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 420f67ef19..8e1449bc59 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() + fieldName = @options.fieldName + selectedIndex = el.parent().index() + if @renderedData + selectedObject = @renderedData[selectedIndex] + value = if @options.id then @options.id(selectedObject, el) else selectedObject.id - # Toggle active class for the tick mark - el.toggleClass "is-active" + if @options.multiSelect + oldValue = field.val() + if oldValue + value = "#{oldValue},#{value}" + else + @dropdown.find(ACTIVE_CLASS).removeClass ACTIVE_CLASS + field.remove() - if value - # Create hidden input for form - input = "" - @dropdown.before input + # Toggle active class for the tick mark + el.toggleClass "is-active" + + if value + if !field.length + # Create hidden input for form + input = "" + @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 b4708f6537..5ade2cb66c 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 "" "
  • From a9ea06ed2967e67d102c83e2bd6c9653778e64bc Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 9 Mar 2016 10:19:41 +0000 Subject: [PATCH 156/255] 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 73a8794c25..74f326e0b8 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 + dropdown_output = dropdown_toggle(toggle_text, data_attr, options) + + dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.has_key?(:dropdown_class)}") 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"}, 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 @@ module DropdownsHelper 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 db46e51f8d..d084559abc 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 d7db2ed6e5..1e36dd9fb8 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 From 8d7e0f0ba93be42119f1c497f280353a13065c85 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 9 Mar 2016 13:34:39 +0000 Subject: [PATCH 157/255] 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 92b42bfe86..5e884454a6 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 eab4162867..c431e2b0df 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 1e36dd9fb8..c3fbba2ba5 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 cbe54e2dc7..d723300f48 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 28c8c6b601..7fc0e444e8 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 50bb32429b..6d50501a72 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 d9842ccf95..383ab4e14d 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 591866b40d..e48f48153f 100644 --- a/spec/features/issues/filter_by_milestone_spec.rb +++ b/spec/features/issues/filter_by_milestone_spec.rb @@ -31,6 +31,9 @@ feature 'Issue filtering by Milestone', feature: true do 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 From ee9a6d7fb3215f5ec66152aa64ed40a7ef61fb1b Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 10 Mar 2016 09:51:39 +0000 Subject: [PATCH 158/255] 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 e48f48153f..f6e33f651c 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 @@ feature 'Issue filtering by Milestone', feature: true do 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 f70214e112..1b2fd1bab1 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 @@ feature 'Merge Request filtering by Milestone', feature: true do 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 From f2992cf343c28736efa5b49fc6a4c3e40a9f0a8f Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 10 Mar 2016 15:32:31 +0100 Subject: [PATCH 159/255] 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 426464dee8..65829bec77 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -528,11 +528,11 @@ class Project < ActiveRecord::Base 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? From 2d446267d67c9a6e28d68b018329345baebb9fe0 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 10 Mar 2016 15:48:53 +0000 Subject: [PATCH 160/255] 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 44d5ddb7d9..6c75876a20 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 e110eece16..0000000000 --- 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 f5e5c7c75c..d7feb5d5c8 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -103,8 +103,6 @@ class Dispatcher new ProjectFork() when 'projects:artifacts:browse' new BuildArtifacts() - when 'projects:builds:show' - new CiBuild() switch path.first() when 'admin' From 3729c0822a51c74ac775c84d005f664aca40f3dd Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 10 Mar 2016 17:05:36 +0100 Subject: [PATCH 161/255] 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 8bf6f13b99..324e32a5c6 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 @@ module Gitlab 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 @@ module Gitlab def redis_key "gitlab:exclusive_lease:#{@key}" end - - def redis_value - @redis_value ||= SecureRandom.hex(10) - end end end From fd49f1b0d3eca1a226df1552e11848d1d1c92f2f Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Thu, 10 Mar 2016 08:13:11 -0800 Subject: [PATCH 162/255] 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 2120b5b285..4abc45bf9b 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 31f29f4a08..0a83c4dd85 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 +## Outside the documentation -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. - -- [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 624d9899c7..9aba4326e1 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 . - [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 From e6ce1206e55aada88c7ca59a8a7f11114b9d916b Mon Sep 17 00:00:00 2001 From: Sean Lang Date: Tue, 8 Mar 2016 13:10:19 -0500 Subject: [PATCH 163/255] 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 1c27c3388c..72ae0b8b50 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 c09785cb96..fc4553137f 100644 --- a/public/logo.svg +++ b/public/logo.svg @@ -1,26 +1,9 @@ - - - - Slice 1 - Created with Sketch. - - - - - \ No newline at end of file + + + + + + + + + From d6f97daa19fa5ff5a61d9396405d6407f8411ba6 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 10 Mar 2016 17:17:41 +0000 Subject: [PATCH 164/255] 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 6c75876a20..607bb6e965 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: From 4f954d6adc87e976204ea7b7e47a974d2938cf59 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Thu, 25 Feb 2016 13:51:17 -0500 Subject: [PATCH 165/255] 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 863a4edfad..cb6b7b2993 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 @@ -309,6 +312,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 From 136ba502d3ad3378f7746a18c3045346a2f49081 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Mon, 29 Feb 2016 13:03:21 -0500 Subject: [PATCH 166/255] 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 51b1579167..243469b8e7 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 From bd411675d2e742b183737644e73212fec198db73 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 10 Mar 2016 17:34:06 +0000 Subject: [PATCH 167/255] 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 321da10a00..1212e89975 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 58373ba87a..8322b4c46a 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 617b043780..2a7d0b0757 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 d24c12251f..83b056eef6 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 36f0637788..9020a1330a 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) From 261214efc270623157d13a62e508b2a7f848b250 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 10 Mar 2016 18:39:50 +0100 Subject: [PATCH 168/255] 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 324e32a5c6..1d2d6981c5 100644 --- a/lib/gitlab/exclusive_lease.rb +++ b/lib/gitlab/exclusive_lease.rb @@ -23,8 +23,7 @@ module Gitlab # 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 From bc19acab2543f959a1fca4c83faf5c1975b7bb90 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 10 Mar 2016 17:39:53 +0000 Subject: [PATCH 169/255] 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 c6da6f7997..3eb0926b74 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" From 7b2bf4ce7589f73bf98a754fecb89d4f92d410b3 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 10 Mar 2016 18:41:57 +0100 Subject: [PATCH 170/255] 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 1d2d6981c5..2ef50286b1 100644 --- a/lib/gitlab/exclusive_lease.rb +++ b/lib/gitlab/exclusive_lease.rb @@ -20,7 +20,7 @@ module Gitlab @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 From 145143ac7fd0ecd9d94d2f0bd9cc48426d5ac829 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 10 Mar 2016 18:05:41 +0000 Subject: [PATCH 171/255] 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 10456189e2..f20779f2fb 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -42,12 +42,12 @@ module CiStatusHelper 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 3eb0926b74..d9868ad1f0 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 From 4dd1d6c4017b5772013d13e8964f06f285056de5 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 10 Mar 2016 13:10:40 -0500 Subject: [PATCH 172/255] 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 07907e6e5a..eb0ae03273 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 } } } From 3a156eb73dfa22c373c964c3df055acee3934fc4 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 10 Mar 2016 13:14:10 -0500 Subject: [PATCH 173/255] 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 e53d6fc6bd..74be90b773 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); From c54879508423d5a2c0ecef7511d28ea08121be57 Mon Sep 17 00:00:00 2001 From: "A. Felipe Cabargas Madrid" Date: Thu, 10 Mar 2016 15:54:13 -0300 Subject: [PATCH 174/255] 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 cbf57db568..e399524e52 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" From 43a4762d50d06e5bd575a33842944ffde073abdd Mon Sep 17 00:00:00 2001 From: "A. Felipe Cabargas Madrid" Date: Thu, 10 Mar 2016 16:11:59 -0300 Subject: [PATCH 175/255] 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 e399524e52..593fc5a6ee 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. From b04020506d6e7b641427763b0052c8b7582db9b7 Mon Sep 17 00:00:00 2001 From: "A. Felipe Cabargas Madrid" Date: Thu, 10 Mar 2016 16:12:50 -0300 Subject: [PATCH 176/255] 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 593fc5a6ee..e6eb1cf381 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 ```, or are indented with four spaces. Only the fenced code blocks support syntax highlighting. From bfcf6a045c34263a355dc0e678275e92f5a9656d Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 10 Mar 2016 14:31:21 -0500 Subject: [PATCH 177/255] 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 eb0ae03273..b034a4882c 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 74be90b773..c0cc30d33a 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); From 4cd9a5208cd5148c95d1de33c419b9d579985fb9 Mon Sep 17 00:00:00 2001 From: ashleys Date: Thu, 10 Mar 2016 14:48:29 -0500 Subject: [PATCH 178/255] 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 aaffa51898..9f29798043 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -81,7 +81,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 @@ -214,7 +214,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) @@ -314,7 +314,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 @@ -534,7 +534,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 @@ -577,7 +577,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) @@ -868,7 +868,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) @@ -975,7 +975,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 element in markdown @@ -1149,7 +1149,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 @@ -1272,7 +1272,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. @@ -1289,7 +1289,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 @@ -1774,7 +1774,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) @@ -1844,7 +1844,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 @@ -2047,7 +2047,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 @@ -2081,7 +2081,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 1eb790b179..eb84c04d22 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -40,7 +40,7 @@ module SearchHelper { 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 93a16e8896..bd31a61774 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 970da78a5c..3359716202 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 a0511819c9..67d016bd87 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 be6c5f96ea..0ca30e4e0f 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 5a4b2f43ba..15051dd76f 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 cc8a22cd00..6be954ad68 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 5a8b94af9b..2ebf7c48f4 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 be1abb88c3..4cd0fdd409 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 1e9d33e87c..bb46aebf4b 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 e2b53c45ab..8704942713 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. ![SSL Verification](ssl.png) diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index 9e96fa5ba4..19d81453d8 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 be4db77094..4994df589a 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 da1c15fef6..97d1edab9c 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -63,7 +63,7 @@ module Gitlab 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 76e443e55e..cc0f668474 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 1455661485..f800f415bd 100644 --- a/spec/models/hooks/service_hook_spec.rb +++ b/spec/models/hooks/service_hook_spec.rb @@ -31,7 +31,7 @@ describe ServiceHook, models: true do 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 6ea99952a8..04bc2dcfb1 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -52,7 +52,7 @@ describe WebHook, models: true do 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 82813cee22..a7e2e1b179 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -172,8 +172,8 @@ describe GitPushService, services: true do 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 b982274c52..cc780587e7 100644 --- a/spec/services/git_tag_push_service_spec.rb +++ b/spec/services/git_tag_push_service_spec.rb @@ -78,8 +78,8 @@ describe GitTagPushService, services: true do 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 e4151b9bb6..0265dbe9c6 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -11,7 +11,7 @@ describe PostReceive do 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 } From 444c6295ba2d67ccf02072b5c089980ec9579334 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 10 Mar 2016 15:24:37 -0500 Subject: [PATCH 179/255] 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 eb84c04d22..494dad0b41 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -40,7 +40,7 @@ module SearchHelper { 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 From e0b6ad8d61f94b2baed701866e4abfc26f289278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 10 Mar 2016 21:59:52 +0100 Subject: [PATCH 180/255] Add 8.5.5 CHANGELOG items [ci skip] --- CHANGELOG | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index aaffa51898..6406c4bf72 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,11 +26,12 @@ 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 - - Fix pagination for filtered dashboard and explore pages 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) From 32ddf092f3ca5cbb9fdb7b7b3754ba61495d0532 Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Thu, 10 Mar 2016 16:07:10 -0500 Subject: [PATCH 181/255] 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 aaffa51898..047ff0425c 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 7e70761a77..1550afb1b5 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 432bdc344a..ea5058e641 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 148c4ac188..1c7c114dbd 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 From e4237f1fe36e85501b624488cd227a91c6770eb3 Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Thu, 10 Mar 2016 16:52:52 -0500 Subject: [PATCH 182/255] 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 1c7c114dbd..b61b7cb967 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 } ``` From 37b00b16a568655343a96976bf2b5252e0894c9c Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Thu, 10 Mar 2016 18:55:18 -0300 Subject: [PATCH 183/255] 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 e2a85f2982..172c5441e3 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -45,10 +45,13 @@ module Gitlab 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 f96fed0f5c..4e507b090e 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -17,16 +17,12 @@ module Gitlab } 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 @@ module Gitlab 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 6cebcb5009..e49dcb4234 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 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do 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 Gitlab::GithubImport::PullRequestFormatter, lib: true do 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 From 36ddca101e05ce885f23e9a797c577e81f70ab16 Mon Sep 17 00:00:00 2001 From: Josh Frye Date: Thu, 10 Mar 2016 21:22:46 -0500 Subject: [PATCH 184/255] 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 d8d1e7b467..2b103c4592 100644 --- a/config/application.rb +++ b/config/application.rb @@ -34,7 +34,7 @@ module Gitlab 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 From 07e1754c7c9e82bbd8cf65cb7dcccec2c8c758d5 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 11 Mar 2016 08:57:53 +0000 Subject: [PATCH 185/255] 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 2a7d0b0757..f5bb5d998b 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 83b056eef6..a75c0d96c5 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 From 732b6b5547d7ba70b771fc53753ee79e6c6791de Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 11 Mar 2016 09:00:19 +0000 Subject: [PATCH 186/255] 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 607bb6e965..7afe8bf79e 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 From 5e145918b4dc20672ca212d094a73b87476d1db5 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 1 Mar 2016 11:25:59 +0000 Subject: [PATCH 187/255] 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 | 211 ++++++++---------- 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, 142 insertions(+), 151 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 8f89d3e61a..351f871de9 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("
    ") - nodes.push("
    ") - nodes.push("
    1
    ") - nodes.push("
    ") + nodes.push "" - emoji_node = $(nodes.join("\n")).insertBefore(".awards-controls").find(".emoji-icon").data("emoji", emoji) - - $(".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 e6609ac710..6edabe2013 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 87dd30f411..e692730bdd 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); - border: 1px solid; - padding: 0px 10px; - float: left; - margin-right: 5px; - border-color: $border-color; - cursor: pointer; + .emoji-menu-content { + padding: $gl-padding; + width: 300px; + height: 300px; + overflow-y: scroll; - &:hover { - background-color: #dce0e5; - } - - &.active { - border-color: $border-gray-light; - background-color: $gray-light; - - &:hover { - background-color: #dce0e5; - } - - .counter { - font-weight: bold; - } - } - - .icon { - float: left; - margin-right: 10px; - } - - .counter { - float: left; - } - } - - .awards-controls { - position: relative; - margin-left: 10px; - float: left; - - .add-award { - font-size: 24px; - color: $gl-gray; - position: relative; - top: 2px; - - &:hover, - &:link { - text-decoration: 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; - } - } - } + 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; } } } + +.emoji-menu-list { + list-style: none; + padding-left: 0; + margin-bottom: 0; +} + +.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; + } +} + +.award-menu-holder { + display: inline-block; + position: relative; +} + +.award-control { + margin-right: 5px; + line-height: 20px; + outline: 0; + + &.active, + &:active { + background-color: #ededed; + box-shadow: none; + outline: 0; + } + + &.is-loading { + .award-control-icon { + display: none; + } + + .award-control-icon-loading { + display: block; + } + } + + .icon, + .award-control-icon { + float: left; + margin-right: 5px; + font-size: 20px; + } + + .award-control-icon-loading { + display: none; + } + + .award-control-icon { + color: #DCDCDC; + } +} diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml index b66e513e4d..3443a8e230 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 617b043780..eedeff2bed 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 b262892ac6..ee5b9fd95a 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 176fd29cb5..20d2d5f317 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}); From a7e76a6119b4f51bdb29ea1420c151081cd12d77 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 1 Mar 2016 13:00:26 +0000 Subject: [PATCH 188/255] 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 351f871de9..829340dea6 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 277c63914d..9f0f1812b4 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 From e3d844984b385cebcb92d21695b1d513e6b745c7 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 2 Mar 2016 15:40:00 +0000 Subject: [PATCH 189/255] 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 829340dea6..e9bf053aa5 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 6561b3de7c..fe8ea399d9 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 e692730bdd..3134511cb3 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; } } From b5f3cf82184c1293684337e2be847c2bc3268578 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 2 Mar 2016 15:43:01 +0000 Subject: [PATCH 190/255] Moved method calls to separate lines --- app/assets/javascripts/awards_handler.coffee | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index e9bf053aa5..66b26e4e6d 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 "" + nodes.push( + "" + ) - emoji_node = $(nodes.join("\n")).insertBefore(".js-award-holder").find(".emoji-icon").data("emoji", emoji) + emoji_node = $(nodes.join("\n")) + .insertBefore(".js-award-holder") + .find(".emoji-icon") + .data("emoji", emoji) $('.award-control').tooltip() resolveNameToCssClass: (emoji) -> From fc79fe7b535770507e457ec996e87fbd8504c91e Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 2 Mar 2016 18:55:05 +0000 Subject: [PATCH 191/255] 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 fe8ea399d9..b038c7958d 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 3134511cb3..b0cd187fc0 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; } } From bfa6064dc9c5eda998db8ff8945c0cc1354f0b7d Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 3 Mar 2016 08:57:40 +0000 Subject: [PATCH 192/255] 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 b0cd187fc0..6a19562e59 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; From cb72f9b30cd524cb238f3462cfbcb2ee90dee146 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 4 Mar 2016 09:13:06 +0000 Subject: [PATCH 193/255] 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 66b26e4e6d..13b4a7e05e 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() From 57a9d07716b39206c683430ef65d00af32fd9811 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 11 Mar 2016 09:11:36 +0000 Subject: [PATCH 194/255] 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 13b4a7e05e..da102db39c 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 = $("
    ").text("Search results").addClass("emoji-search") found_emojis = @searchEmojis(term).show() - ul = $("
      ").addClass("emoji-search").append(found_emojis) + 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 From 25f392eab734bd68da377a61c608bca73355ba43 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 11 Mar 2016 09:47:18 +0000 Subject: [PATCH 195/255] 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 da102db39c..03a4487416 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 6a19562e59..28994e60ba 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 { From f48f51ac7e204aa174effcda7cc79e06e2bbaba0 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 29 Feb 2016 13:39:29 +0000 Subject: [PATCH 196/255] 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 | 237 ++++++++++-------- 6 files changed, 204 insertions(+), 120 deletions(-) diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee index 9110b732ad..59d44c30be 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 c98e43ad09..ff551f151f 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 6561b3de7c..0fd2f74cf8 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 4826b994e3..f8aeab6857 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 175afbf842..669fe05e5c 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 @@ class Profiles::AccountsController < Profiles::ApplicationController 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 9fa96084f9..3e8f660635 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" - - else - = f.submit 'Generate', class: "btn btn-default" - - .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 - %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. - - - 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| - %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" +.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 - - 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. + %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 + = 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 + +- 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 + 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 + - 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. +.append-bottom-default From d5f145bf8be6c787044726a8e77f928a78d0e100 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 1 Mar 2016 15:58:00 +0000 Subject: [PATCH 197/255] 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 3e8f660635..9fe064e588 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 0c60328583..d9436e9e21 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 From 9dc71dcaa081592c226f7c6be574d24f0fadec1e Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 1 Mar 2016 16:22:04 +0000 Subject: [PATCH 198/255] Updated CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 1847c5193a..c1c90903d5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -41,6 +41,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 From 4d4573268e055606c90dfe1c433b1a331e696378 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 2 Mar 2016 10:34:25 +0000 Subject: [PATCH 199/255] 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 0fd2f74cf8..d55d5cfbca 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 f8aeab6857..40037c548f 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; } } From c4baf2417fec7b8a65b6cafe515f73ea1a3474f4 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 2 Mar 2016 17:34:31 +0000 Subject: [PATCH 200/255] 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 1f55b18e0b..fef79cefc9 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -240,7 +240,7 @@ class ApplicationController < ActionController::Base 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 669fe05e5c..bd827f2ab1 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 @@ class Profiles::AccountsController < Profiles::ApplicationController 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 8f83fdd02b..65ecee0746 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 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController 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 9fe064e588..a541765500 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 From 218f3e702a19b8682219d0d8932e7b34986c00d4 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 3 Mar 2016 09:28:33 +0000 Subject: [PATCH 201/255] 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 fef79cefc9..1f55b18e0b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -240,7 +240,7 @@ class ApplicationController < ActionController::Base 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 bd827f2ab1..175afbf842 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 @@ class Profiles::AccountsController < Profiles::ApplicationController 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 65ecee0746..8f83fdd02b 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 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController 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 a541765500..6efd119f26 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 b2830aa083..c82c596219 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' From a9d97838a399f153a23c7a9a9656a3a0ed912dcc Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 3 Mar 2016 16:59:33 +0000 Subject: [PATCH 202/255] 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 c82c596219..5d342ef58e 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? From c9b62662cc569af613b94e41c6d8de503eb2749a Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 4 Mar 2016 09:03:50 +0000 Subject: [PATCH 203/255] 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 d55d5cfbca..835364b299 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 40037c548f..248c56e459 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 { From 6690d8e25b0da2fa83e218d6f2411df7cec9ef32 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 11 Mar 2016 10:03:35 +0000 Subject: [PATCH 204/255] 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 eff8beb97e..884abff120 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -610,7 +610,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') From 6c69868cfc595a4d64ba8ae3a530a1cf49d2e1f3 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 11 Mar 2016 15:29:29 +0000 Subject: [PATCH 205/255] 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 ec478a5963..4ef544136a 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 From 9f8661987db44f6c1d2cfd75c889344fc49bd6bc Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 11 Mar 2016 16:30:58 +0000 Subject: [PATCH 206/255] 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 009d621fc7..5b647fc617 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; } From 95e73f87a99a0869d503451ab93a36bdc2530445 Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Fri, 11 Mar 2016 11:47:34 -0500 Subject: [PATCH 207/255] 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 b61b7cb967..1c3dc707f6 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: { From 5461170fb39d62d8b780d4bd02eaddc8b4918686 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Fri, 11 Mar 2016 21:26:18 +0200 Subject: [PATCH 208/255] 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 9bd2f5aff2..c10f82054e 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`. From d24ee2a2065692bd4edff59af55353de3c1c49e6 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 26 Feb 2016 17:14:44 +0100 Subject: [PATCH 209/255] 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 0000000000..fca5ac01a0 --- /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 a74b86d8e2..8f44ccebac 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -15,6 +15,7 @@ ActiveRecord::Schema.define(version: 20160309140734) do # 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 @@ ActiveRecord::Schema.define(version: 20160309140734) do 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 @@ ActiveRecord::Schema.define(version: 20160309140734) do 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 @@ ActiveRecord::Schema.define(version: 20160309140734) do 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 @@ ActiveRecord::Schema.define(version: 20160309140734) do 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 @@ ActiveRecord::Schema.define(version: 20160309140734) do 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 @@ ActiveRecord::Schema.define(version: 20160309140734) do 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 @@ ActiveRecord::Schema.define(version: 20160309140734) do 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 @@ ActiveRecord::Schema.define(version: 20160309140734) do add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree add_index "snippets", ["created_at", "id"], name: "index_snippets_on_created_at_and_id", using: :btree add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree + add_index "snippets", ["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 @@ ActiveRecord::Schema.define(version: 20160309140734) do 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 From 135659a75135b47563b349d13a9846b9b017af15 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 1 Mar 2016 12:02:06 +0100 Subject: [PATCH 210/255] 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 65829bec77..2f19ec9ba8 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -266,13 +266,31 @@ class Project < ActiveRecord::Base 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 2fa38a5d3d..6627432aa8 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -582,7 +582,58 @@ describe Project, models: true do 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 From db615d0a7992d5118c3e9e8914064eb26970666b Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 1 Mar 2016 12:16:15 +0100 Subject: [PATCH 211/255] 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 2f19ec9ba8..cb236bf41c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -298,7 +298,10 @@ class Project < ActiveRecord::Base 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 6627432aa8..59c5ffa6b9 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -698,4 +698,20 @@ describe Project, models: true do 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 From 1f5284e5ddf2ce9b555799f43ca73be32d9bdf67 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 1 Mar 2016 12:51:01 +0100 Subject: [PATCH 212/255] 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 dd3925c7a7..35d05af38b 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -113,12 +113,32 @@ class Snippet < ActiveRecord::Base 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 7e5b5499ae..5077ac7b62 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -59,4 +59,48 @@ describe Snippet, models: true do 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 From 508b6b46fea6b4bae9ce19c0337bafdb694edda8 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 1 Mar 2016 15:43:19 +0100 Subject: [PATCH 213/255] 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 3b20d5d22b..76e86fdbca 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -105,8 +105,18 @@ class Note < ActiveRecord::Base [: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 33085dac4e..cd620ea544 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -140,10 +140,16 @@ describe Note, models: true do 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 From 800aa296953c4a99e16e0493d9260359ef8c8414 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 1 Mar 2016 16:08:48 +0100 Subject: [PATCH 214/255] Use ILIKE/LIKE for searching users --- app/models/user.rb | 16 +++++++++++++- spec/models/user_spec.rb | 46 +++++++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 505a547d8e..fc4cf92be6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -286,8 +286,22 @@ class User < ActiveRecord::Base 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 412101ac9f..90207f5153 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -463,17 +463,43 @@ describe User, models: true do 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') } + describe '.search' do + let(:user) { create(:user) } - 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) + 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 From ce5e831bcfd7daf1a12d488e2857d9424de091dd Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 1 Mar 2016 16:15:42 +0100 Subject: [PATCH 215/255] 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 76042b3e3f..bfeddf8b4d 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 3c995053ee..c9245fc953 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -103,4 +103,30 @@ describe Group, models: true do 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 From 2076bdb62e6535cd8b5027c5636610eb06af00a0 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 1 Mar 2016 16:41:18 +0100 Subject: [PATCH 216/255] 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 e725a6d468..ce2750b071 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -23,7 +23,7 @@ module Ci 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 @@ module Ci 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 e891838672..25e9e5eca4 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -132,4 +132,32 @@ describe Ci::Runner, models: true do 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 From 87e7c3e1321ac5ae26d6a23d7b16e8dadaff98d2 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 1 Mar 2016 16:59:36 +0100 Subject: [PATCH 217/255] 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 27b97944e3..3c42f58293 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 600089802b..f0e4de8539 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -32,9 +32,54 @@ describe Issue, "Issuable" do 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 From 2cf7f3f410832560bf049b24fbcaa27c1e3f30c7 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 1 Mar 2016 17:05:26 +0100 Subject: [PATCH 218/255] 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 e3969f32dd..e3b6c552f9 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 28f13100d1..de1757bf67 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -181,4 +181,34 @@ describe Milestone, models: true do 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 From 013542965c2b6d84d28aab4823e5400897610087 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 1 Mar 2016 17:56:14 +0100 Subject: [PATCH 219/255] 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 e904cb6c6f..e1e94c5cc3 100644 --- a/app/services/search/global_service.rb +++ b/app/services/search/global_service.rb @@ -10,9 +10,8 @@ module Search 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 2ab2d4af79..036fa6f937 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 @@ module Gitlab 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 @@ module Gitlab 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 @@ module Gitlab 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 0000000000..bb18f41785 --- /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 From 42fde69d39234368f8252febc9bf6ca3eca6f275 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 1 Mar 2016 18:01:09 +0100 Subject: [PATCH 220/255] 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 8ca0877321..0b3e713e22 100644 --- a/app/services/search/snippet_service.rb +++ b/app/services/search/snippet_service.rb @@ -7,8 +7,9 @@ module Search 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 addda95be2..e0e74ff835 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 @@ module Gitlab 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 0000000000..e86b9ef6a6 --- /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 From ec349dc1b6659d8f691cb1c1fb287aa05c9d9246 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 1 Mar 2016 18:04:37 +0100 Subject: [PATCH 221/255] 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 f630c0a379..c08881dce4 100644 --- a/app/services/search/project_service.rb +++ b/app/services/search/project_service.rb @@ -7,7 +7,7 @@ module Search 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 70de6a74e7..f36600c575 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 @@ module Gitlab 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 @@ module Gitlab project.repository.find_commits_by_message(query).compact end end - - def limit_project_ids - [project.id] - end end end From 49e122df852170cc1e5fc99f7e64e9f47408e1a8 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 3 Mar 2016 12:35:06 +0100 Subject: [PATCH 222/255] 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 0000000000..ac803778ab --- /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 From 2080fd395c98e1f5c822450b6dc9e8638b4e0329 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 3 Mar 2016 12:36:28 +0100 Subject: [PATCH 223/255] 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 8f44ccebac..3ac6203632 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -259,8 +259,8 @@ ActiveRecord::Schema.define(version: 20160309140734) do 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 @@ ActiveRecord::Schema.define(version: 20160309140734) do 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 @@ ActiveRecord::Schema.define(version: 20160309140734) do 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 @@ ActiveRecord::Schema.define(version: 20160309140734) do 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 @@ ActiveRecord::Schema.define(version: 20160309140734) do 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 @@ ActiveRecord::Schema.define(version: 20160309140734) do 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 @@ ActiveRecord::Schema.define(version: 20160309140734) do 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 @@ ActiveRecord::Schema.define(version: 20160309140734) do add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree add_index "snippets", ["created_at", "id"], name: "index_snippets_on_created_at_and_id", using: :btree add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree - add_index "snippets", ["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 @@ ActiveRecord::Schema.define(version: 20160309140734) do 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 From d38dcf1e3b08608aab15e9dd811f9fc878751ce5 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 3 Mar 2016 12:55:44 +0100 Subject: [PATCH 224/255] 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 0000000000..835f3ec557 --- /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 From 12082db41ea8e1718012f95f3a4c42dafaa55392 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 3 Mar 2016 13:09:28 +0100 Subject: [PATCH 225/255] 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 ac803778ab..820cc89ef5 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. From 300332bbf6061267176d83ef8431daf6fae47e2b Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 3 Mar 2016 13:12:00 +0100 Subject: [PATCH 226/255] 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 efc2e5f4ef..09adbc07dc 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::ProjectSearchResults, lib: true do 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 Gitlab::ProjectSearchResults, lib: true do 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) } From 8c2868e8ea5d6340efcedfe5fd6388b0087eae89 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 3 Mar 2016 15:10:14 +0100 Subject: [PATCH 227/255] 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 f36600c575..0607a8b959 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -83,5 +83,9 @@ module Gitlab project.repository.find_commits_by_message(query).compact end end + + def project_ids_relation + project + end end end From b77b3b16b6d34e19b07ec58bc9b1e1ca3b05ba25 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 3 Mar 2016 18:35:17 +0100 Subject: [PATCH 228/255] 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 036fa6f937..f13528a2ee 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -94,7 +94,7 @@ module Gitlab end def project_ids_relation - limit_projects.select(:id) + limit_projects.select(:id).reorder(nil) end end end From 9e00a237161679b0d6afdefd246e0a7bf209510c Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 4 Mar 2016 11:39:00 +0100 Subject: [PATCH 229/255] 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 3b4e0362e0..2b8fba77bb 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -52,7 +52,10 @@ class ProjectsFinder 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 fc4cf92be6..725f748faf 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -442,6 +442,11 @@ class User < ActiveRecord::Base 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 @@ class User < ActiveRecord::Base 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 From 0ab9571ad791c608ec258426edb195ae95b45220 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 4 Mar 2016 11:54:37 +0100 Subject: [PATCH 230/255] 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 f0e4de8539..aff384c294 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -32,7 +32,7 @@ describe Issue, "Issuable" do 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 @@ describe Issue, "Issuable" do 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 @@ describe Issue, "Issuable" do 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 90207f5153..909b679659 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -474,7 +474,7 @@ describe User, models: true 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 + 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 @@ describe User, models: true 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 + 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 @@ describe User, models: true 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 + it 'returns users with a matching username regardless of the casing' do expect(described_class.search(user.username.upcase)).to eq([user]) end end From d7d5937531350283f6cbd68dead5750227e6d962 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 4 Mar 2016 12:01:21 +0100 Subject: [PATCH 231/255] 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 ce2750b071..90349a0759 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -59,7 +59,7 @@ module Ci # # 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 bfeddf8b4d..afbc292201 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -41,7 +41,7 @@ class Group < Namespace # # 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 76e86fdbca..8b0610ff77 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -113,7 +113,7 @@ class Note < ActiveRecord::Base # # 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 cb236bf41c..ce103398a9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -274,7 +274,7 @@ class Project < ActiveRecord::Base # # 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 35d05af38b..b9e835a448 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -121,7 +121,7 @@ class Snippet < ActiveRecord::Base # # 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 725f748faf..101303e1f1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -294,7 +294,7 @@ class User < ActiveRecord::Base # # Returns an ActiveRecord::Relation. def search(query) - table = User.arel_table + table = arel_table pattern = "%#{query}%" where( From ee75c49313127f53fe60a0701f40024b897a15e8 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 4 Mar 2016 12:06:25 +0100 Subject: [PATCH 232/255] 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 bdb33f3749..55842df1e2 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -52,8 +52,18 @@ class Namespace < ActiveRecord::Base 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 e0b3290e41..bc31b49742 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -41,13 +41,32 @@ describe Namespace, models: true do 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 { expect(Namespace.search(@namespace.path)).to eq([@namespace]) } - it { expect(Namespace.search('unknown')).to eq([]) } + 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 '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 From 78244ffcbff2d5f31611c7a793cad797905cd384 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 4 Mar 2016 12:15:30 +0100 Subject: [PATCH 233/255] 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 bc31b49742..3c3a580942 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -41,7 +41,7 @@ describe Namespace, models: true do 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 From 0ba20457fb7e16538e447bd6d3782cb8b7b506fa Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 4 Mar 2016 12:21:26 +0100 Subject: [PATCH 234/255] 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 8df142c531..d59b7f0e84 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. From c2df121f8eeaf8e0fccab43f5b181f70f3725e58 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 4 Mar 2016 12:23:51 +0100 Subject: [PATCH 235/255] Added changelog entry for search performance --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 8917eebafd..d4554b9619 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 From 4f3fa519c627b7caf45273226ae438181dfbc392 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 7 Mar 2016 12:56:46 +0100 Subject: [PATCH 236/255] 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 c1e18bb3cc..188325045e 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 @@ class MergeRequest < ActiveRecord::Base super("merge_requests", /(?\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 59c40922ab..8bf68013fd 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -80,6 +80,12 @@ describe MergeRequest, models: true do 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}" From 67143e25a2395c743e0537e7f40b893a65912b7d Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 10 Mar 2016 14:49:15 +0100 Subject: [PATCH 237/255] 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 fca5ac01a0..003169c13c 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 @@ class AddTrigramIndexesForSearching < ActiveRecord::Migration 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 From e932a4164f527e51567e4476eee51dbc4102cd7c Mon Sep 17 00:00:00 2001 From: Dennis van de Hoef Date: Fri, 11 Mar 2016 22:34:51 +0100 Subject: [PATCH 238/255] 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 7de874c8bc..b2fbc95e04 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%; } } From 01f6db4f643380d143772958b22d3f318b1db3a7 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 11 Mar 2016 17:46:50 -0500 Subject: [PATCH 239/255] 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 8b0610ff77..2e084b5c80 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 @@ class Note < ActiveRecord::Base 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 0000000000..49e787d9a9 --- /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 cd620ea544..b854de1d3d 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -226,4 +226,12 @@ describe Note, models: true do 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 From cc065fbeaa8b5e480d6ba5349198042e8a84b011 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 12 Mar 2016 01:09:37 -0800 Subject: [PATCH 240/255] 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 1847c5193a..b29ad11e6a 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 0000000000..7e7896b490 --- /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 0000000000..bc9bee7cbb --- /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) + + "\n"; + end + + def strip_url(url) + url.gsub('http://', ''). + gsub('https://', '') + end + end + end +end From 3b76b73ab178f0504e2a9be934fb13f239c81857 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Sat, 12 Mar 2016 15:44:48 +0100 Subject: [PATCH 241/255] 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 2b8fba77bb..0e5a8f5ee0 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -53,7 +53,7 @@ class ProjectsFinder 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 101303e1f1..043bc825ad 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -442,11 +442,6 @@ class User < ActiveRecord::Base 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 @@ class User < ActiveRecord::Base 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 From 2563213ccf3c1b26d58292570f9170f73034b221 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 12 Mar 2016 07:49:16 -0800 Subject: [PATCH 242/255] 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 aea08ecce3..c70add86a2 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 @@ class ProjectsController < ApplicationController 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 87ac75a350..0000000000 --- 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"} From bb206f947fd66460c09f464e3667b80ca14086d6 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 12 Mar 2016 07:50:02 -0800 Subject: [PATCH 243/255] 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 bc9bee7cbb..44d378642a 100644 --- a/lib/gitlab/middleware/go.rb +++ b/lib/gitlab/middleware/go.rb @@ -26,11 +26,11 @@ module Gitlab 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 @@ module Gitlab end def strip_url(url) - url.gsub('http://', ''). - gsub('https://', '') + url.gsub(/\Ahttps?:\/\//, '') end end end From b23a05d09bef7b10722ae7975360ad51c5d7765f Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 12 Mar 2016 08:10:38 -0800 Subject: [PATCH 244/255] 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 0000000000..117a15264d --- /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 = "\n" + expect(resp[2].body).to eq([expected_body]) + end + end + end +end From 380a67ad4c635ca6fbfc4e8845f97788873f4e36 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 12 Mar 2016 08:10:54 -0800 Subject: [PATCH 245/255] 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 44d378642a..50b0dd3238 100644 --- a/lib/gitlab/middleware/go.rb +++ b/lib/gitlab/middleware/go.rb @@ -26,7 +26,7 @@ module Gitlab 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) From 5a586f364c5d2b866d6a074eff7996f05585b7d5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 12 Mar 2016 17:42:51 +0000 Subject: [PATCH 246/255] 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 07b187b31a..d4554b9619 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 c70add86a2..aea08ecce3 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 @@ class ProjectsController < ApplicationController 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 0000000000..87ac75a350 --- /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 7e7896b490..0000000000 --- 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 50b0dd3238..0000000000 --- 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) - - "\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 117a15264d..0000000000 --- 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 = "\n" - expect(resp[2].body).to eq([expected_body]) - end - end - end -end From b9d13c11dee8f555b0d80fd5b9b6a42be7721461 Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Thu, 10 Mar 2016 01:49:13 +0000 Subject: [PATCH 247/255] implements upcoming filter in milstones --- app/finders/issuable_finder.rb | 8 ++++++++ app/helpers/milestones_helper.rb | 1 + app/models/milestone.rb | 1 + 3 files changed, 10 insertions(+) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index c88a420b41..410e6f6456 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -94,6 +94,10 @@ class IssuableFinder params[:milestone_title].present? end + def upcoming? + params[:milestone_title] == 'Upcoming' + end + def filter_by_no_milestone? milestones? && params[:milestone_title] == Milestone::None.title end @@ -248,6 +252,10 @@ class IssuableFinder if milestones? if filter_by_no_milestone? items = items.where(milestone_id: [-1, nil]) + elsif upcoming? + upcoming = Milestone.where(project_id: projects) + .where('due_date > ?', Time.now).order(due_date: :asc).first + items = items.joins(:milestone).where(milestone: { title: upcoming.title }) else items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] }) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index e3e7daa49c..e8ac8788d9 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -59,6 +59,7 @@ module MilestonesHelper grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date } grouped_milestones.unshift(Milestone::None) grouped_milestones.unshift(Milestone::Any) + grouped_milestones.unshift(Milestone::Upcoming) options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title]) end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index e3b6c552f9..85f7d8a775 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -19,6 +19,7 @@ class Milestone < ActiveRecord::Base MilestoneStruct = Struct.new(:title, :name, :id) None = MilestoneStruct.new('No Milestone', 'No Milestone', 0) Any = MilestoneStruct.new('Any Milestone', '', -1) + Upcoming = MilestoneStruct.new('Upcoming', '', -2) include InternalId include Sortable From 7530827ecae0596616623d1c4f7775b08d5ada3c Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Fri, 11 Mar 2016 17:46:14 +0000 Subject: [PATCH 248/255] fixes issues for mr acceptance --- app/finders/issuable_finder.rb | 13 ++++++------- app/models/milestone.rb | 7 ++++++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 410e6f6456..d592bdd0eb 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -94,10 +94,6 @@ class IssuableFinder params[:milestone_title].present? end - def upcoming? - params[:milestone_title] == 'Upcoming' - end - def filter_by_no_milestone? milestones? && params[:milestone_title] == Milestone::None.title end @@ -248,14 +244,17 @@ class IssuableFinder items end + def upcoming? + params[:milestone_title] == '#upcoming' && projects + end + def by_milestone(items) if milestones? if filter_by_no_milestone? items = items.where(milestone_id: [-1, nil]) elsif upcoming? - upcoming = Milestone.where(project_id: projects) - .where('due_date > ?', Time.now).order(due_date: :asc).first - items = items.joins(:milestone).where(milestone: { title: upcoming.title }) + upcoming = Milestone.upcoming(projects) + items = items.joins(:milestone).where(milestones: { title: upcoming.title }) else items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] }) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 85f7d8a775..7697072d23 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -19,7 +19,7 @@ class Milestone < ActiveRecord::Base MilestoneStruct = Struct.new(:title, :name, :id) None = MilestoneStruct.new('No Milestone', 'No Milestone', 0) Any = MilestoneStruct.new('Any Milestone', '', -1) - Upcoming = MilestoneStruct.new('Upcoming', '', -2) + Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2) include InternalId include Sortable @@ -82,6 +82,11 @@ class Milestone < ActiveRecord::Base super("milestones", /(?\d+)/) end + def self.upcoming(projects) + self.where(project_id: projects) + .where('due_date > ?', Time.now). order(due_date: :asc).first + end + def to_reference(from_project = nil) escaped_title = self.title.gsub("]", "\\]") From 0291473b98b457e85657aac29520b5f2b5b631ec Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Sat, 12 Mar 2016 16:54:05 +0000 Subject: [PATCH 249/255] fixes issues --- app/finders/issuable_finder.rb | 4 ++-- app/models/milestone.rb | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index d592bdd0eb..5e22aca45f 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -245,7 +245,7 @@ class IssuableFinder end def upcoming? - params[:milestone_title] == '#upcoming' && projects + params[:milestone_title] == '#upcoming' end def by_milestone(items) @@ -253,7 +253,7 @@ class IssuableFinder if filter_by_no_milestone? items = items.where(milestone_id: [-1, nil]) elsif upcoming? - upcoming = Milestone.upcoming(projects) + upcoming = Milestone.where(project_id: projects).upcoming items = items.joins(:milestone).where(milestones: { title: upcoming.title }) else items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] }) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 7697072d23..374590ba0c 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -82,9 +82,8 @@ class Milestone < ActiveRecord::Base super("milestones", /(?\d+)/) end - def self.upcoming(projects) - self.where(project_id: projects) - .where('due_date > ?', Time.now). order(due_date: :asc).first + def self.upcoming + self.where('due_date > ?', Time.now).order(due_date: :asc).first end def to_reference(from_project = nil) From 8e6bd86cf26dd6e659fc7ccaf7361029b256891a Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Sat, 12 Mar 2016 17:24:33 +0000 Subject: [PATCH 250/255] adds my name to changelog --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index d4554b9619..afef4b578c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -35,6 +35,7 @@ v 8.5.5 - Prevent a 500 error in Todos when author was removed - Fix pagination for filtered dashboard and explore pages - Fix "Show all" link behavior + - Add #upcoming filter to Milestone filter (Tiago Botelho) v 8.5.4 - Do not cache requests for badges (including builds badge) From b94bdb698dedb519a8478440a3dc8a763856b6a0 Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Sat, 12 Mar 2016 17:44:29 +0000 Subject: [PATCH 251/255] removes tab and replaces with space in changelog --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index afef4b578c..2535a78af4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -36,6 +36,7 @@ v 8.5.5 - Fix pagination for filtered dashboard and explore pages - Fix "Show all" link behavior - Add #upcoming filter to Milestone filter (Tiago Botelho) + - Add #upcoming filter to Milestone filter (Tiago Botelho) v 8.5.4 - Do not cache requests for badges (including builds badge) From 3315fb820b50a5005200a41221434a2e7cf8cf14 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 12 Mar 2016 11:14:56 -0800 Subject: [PATCH 252/255] 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 6eee4dfe22..1893e946f5 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -9,19 +9,6 @@ describe ProjectsController do 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 From 74d7de81960e6d7037c5ae7d4ecd953ac6ea5deb Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 12 Mar 2016 21:05:23 +0000 Subject: [PATCH 253/255] 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 d4554b9619..07b187b31a 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 aea08ecce3..c70add86a2 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 @@ class ProjectsController < ApplicationController 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 87ac75a350..0000000000 --- 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 0000000000..7e7896b490 --- /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 0000000000..50b0dd3238 --- /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) + + "\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 0000000000..117a15264d --- /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 = "\n" + expect(resp[2].body).to eq([expected_body]) + end + end + end +end From 4e27284a7b9cbd153d61b831b3794f6a2b5ef6a1 Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Sat, 12 Mar 2016 17:28:54 -0800 Subject: [PATCH 254/255] 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 0a83c4dd85..cc059dc437 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) From d3b7633da4a3964eb4609c7591763aa8e43fd0eb Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Sun, 13 Mar 2016 12:19:27 +0000 Subject: [PATCH 255/255] fixes small issues --- CHANGELOG | 1 - app/finders/issuable_finder.rb | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2535a78af4..b7bc48802d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -35,7 +35,6 @@ v 8.5.5 - Prevent a 500 error in Todos when author was removed - Fix pagination for filtered dashboard and explore pages - Fix "Show all" link behavior - - Add #upcoming filter to Milestone filter (Tiago Botelho) - Add #upcoming filter to Milestone filter (Tiago Botelho) v 8.5.4 diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 5e22aca45f..19e8c7a92b 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -244,7 +244,7 @@ class IssuableFinder items end - def upcoming? + def filter_by_upcoming_milestone? params[:milestone_title] == '#upcoming' end @@ -252,7 +252,7 @@ class IssuableFinder if milestones? if filter_by_no_milestone? items = items.where(milestone_id: [-1, nil]) - elsif upcoming? + elsif filter_by_upcoming_milestone? upcoming = Milestone.where(project_id: projects).upcoming items = items.joins(:milestone).where(milestones: { title: upcoming.title }) else