From 197f00cc3bb95c3133eead831acde4e1f25eeb53 Mon Sep 17 00:00:00 2001 From: Fabio Busatto Date: Tue, 6 Jun 2017 22:25:13 +0000 Subject: [PATCH 001/141] Add new directory --- .../.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/.gitkeep diff --git a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/.gitkeep b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d From 0f76c9eadbce4a49e7a5e68cb89382df71a6520a Mon Sep 17 00:00:00 2001 From: Fabio Busatto Date: Tue, 6 Jun 2017 22:33:44 +0000 Subject: [PATCH 002/141] Add new file --- .../index.md | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md diff --git a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md new file mode 100644 index 00000000000..31bac149245 --- /dev/null +++ b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md @@ -0,0 +1,167 @@ +> **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** tutorial || +> **Level:** intermediary || +> **Author:** [Fabio busatto](https://gitlab.com/bikebilly) || +> **Publication date:** AAAA/MM/DD + +In this article, we're going to show how we can leverage the power of GitLab CI to compile, test and deploy a Maven application to an Artifactory repository with just a very few lines of configuration. + +Every time we change our sample application, the Continuos Integration will check that everything is correct, and after a merge to master branch it will automatically push our package, making it ready for use. + + +# Create a simple Maven application + +First of all, we need to create our application. We choose to have a very simple one, but it could be any Maven application. The simplest way is to use Maven itself, we've just to run the following command: + +```bash +mvn archetype:generate -DgroupId=com.example.app -DartifactId=maven-example-app -Dversion=1.0 -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false +``` + +Done! Let's move into the maven-example-app directory. Now we've our app to work with. + +The project structure is quite simple, and we're interested mainly in these resources: + +`pom.xml`: project object model (POM) file +`src/main/java/com/example/app/App.java`: source of our application (it prints "Hello World!" to stdout) + +# Test our app locally + +If we want to be sure the application has been created correctly, we can compile and test it: + +```bash +mvn compile && mvn test +``` + +Note: every time we run a `mvn` command it may happen that a bunch of files are downloaded: it's totally normal, and these files are cached so we don't have to download them again next time. + +At the end of the run, we should see an output like this: + +``` +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 5.614 s +[INFO] Finished at: 2017-06-05T10:50:36+02:00 +[INFO] Final Memory: 17M/130M +[INFO] ------------------------------------------------------------------------ +``` + +# Create `.gitlab-ci.yml` + +A very simple `.gitlab-ci.yml` file that performs build and tests of our application is the following: + +```yaml +image: maven:latest + +cache: + paths: + - target/ + +build: + script: + - mvn compile + +test: + script: + - mvn test +``` + +We want to use the latest docker image available for Maven, that already contains everything we need to perform our tasks. We also want to cache the `.m2` folder in the user homedir: this is the place where all the files automatically download by Maven commands are stored, so we can reuse them between stages. The `target` folder is where our application will be created: Maven runs all the phases in a specific order, so running `mvn test` will automatically run `mvn compile` if needed, but we want to improve performances caching everything that is reused. + +# Push the code to GitLab + +Now that we've our app, we want to put it on GitLab! Long story short, we've to create a new project and push the code to it as usual. A new pipeline will run and you've just to wait until it succeed! + +# Set up Artifactory as the deployment repo + +## Configure POM file + +Next step is to setup our project to use Artifactory as its repository for artifacts deployment: in order to complete this, we need access to the Artifactory instance. +So, first of all let's select the `libs-release-local` repository in the `Set Me Up` section, and copy to clipboard the configuration snipped marked as `Deploy`. This is the "address" of our repo, and it is needed by Maven to push artifacts during the `deploy` stage. +Now let's go back to our project and edit the pom.xml file: we have to add the snipped we just copied from Artifactory into the project section, so we can paste it after the dependencies. +The final POM will look like this: + +```xml + + 4.0.0 + com.example.app + maven-example-app + jar + 1.0 + maven-example-app + http://maven.apache.org + + + junit + junit + 3.8.1 + test + + + + + central + 0072a36394cd-releases + http://localhost:8081/artifactory/libs-release-local + + + +``` + +## Configure credentials for the repo + +One last step is required to actully deploy artifacts to Artifactory: we need to configure credentials for our repo, and best practices want us to create an API key for this task, so we don't have to expose our account password. +Let's go back to Artifactory, edit the account settings and generate a new API key. For security reasons, we don't want to expose directly this key into the `.gitlab-ci.yml, so we're going to create secret variables REPO_USERNAME and REPO_PASSWORD containing the username and the key in our GitLab project settings. + +[screenshot of secret variables window] + +We must now include these credentials in the `~/.m2/settings.xml` file, so let's create a file named `.maven-settings.xml` in our project folder with the following content: + +```xml + + + + ${REPO_USERNAME} + ${REPO_PASSWORD} + central + + + +``` + +Note that `id` must have the same value as the related `id` field of the `repository` section in `pom.xml`. + +# Configure automatic deployment + +Time to change `.gitlab-ci.yml` and add the deploy stage! Maven has the perfect command for that, but it requires `settings.xml` to be in the correct folder, so we need to move it before executing `mvn deploy` command. + +The complete file is now this: + +```yaml +image: maven:latest + +cache: + paths: + - target/ + +Build: + Stage: build + script: + - mvn compile + +Test: + Stage: test + script: + - mvn test + +Deploy: + Stage: deploy + script: + - cp .maven-settings.xml ~/.m2/settings.xml + - mvn deploy + only: + - master +``` + +We're ready to go! Every merge (or push) to master will now trigger the deployment to our Artifactory repository! \ No newline at end of file From a320af5febee6bda490c7d87755bbc4d9cc610db Mon Sep 17 00:00:00 2001 From: Fabio Busatto Date: Tue, 6 Jun 2017 22:37:20 +0000 Subject: [PATCH 003/141] Update index.md --- .../index.md | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md index 31bac149245..77718077f37 100644 --- a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md +++ b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md @@ -1,6 +1,6 @@ > **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** tutorial || > **Level:** intermediary || -> **Author:** [Fabio busatto](https://gitlab.com/bikebilly) || +> **Author:** [Fabio Busatto](https://gitlab.com/bikebilly) || > **Publication date:** AAAA/MM/DD In this article, we're going to show how we can leverage the power of GitLab CI to compile, test and deploy a Maven application to an Artifactory repository with just a very few lines of configuration. @@ -20,14 +20,14 @@ Done! Let's move into the maven-example-app directory. Now we've our app to work The project structure is quite simple, and we're interested mainly in these resources: -`pom.xml`: project object model (POM) file -`src/main/java/com/example/app/App.java`: source of our application (it prints "Hello World!" to stdout) +- `pom.xml`: project object model (POM) file +- `src/main/java/com/example/app/App.java`: source of our application (it prints "Hello World!" to stdout) # Test our app locally If we want to be sure the application has been created correctly, we can compile and test it: -```bash +``` mvn compile && mvn test ``` @@ -145,23 +145,22 @@ cache: paths: - target/ -Build: - Stage: build +build: + stage: build script: - mvn compile -Test: - Stage: test +test: + stage: test script: - mvn test -Deploy: - Stage: deploy +deploy: + stage: deploy script: - cp .maven-settings.xml ~/.m2/settings.xml - mvn deploy only: - master ``` - We're ready to go! Every merge (or push) to master will now trigger the deployment to our Artifactory repository! \ No newline at end of file From 0abb83c0acaab15891bd18de879debc5c6cc40f8 Mon Sep 17 00:00:00 2001 From: Fabio Busatto Date: Mon, 26 Jun 2017 15:15:41 +0000 Subject: [PATCH 004/141] Update index.md --- .../index.md | 124 ++++++++++-------- 1 file changed, 66 insertions(+), 58 deletions(-) diff --git a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md index 77718077f37..53eebcfdcd2 100644 --- a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md +++ b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md @@ -3,78 +3,86 @@ > **Author:** [Fabio Busatto](https://gitlab.com/bikebilly) || > **Publication date:** AAAA/MM/DD -In this article, we're going to show how we can leverage the power of GitLab CI to compile, test and deploy a Maven application to an Artifactory repository with just a very few lines of configuration. - -Every time we change our sample application, the Continuos Integration will check that everything is correct, and after a merge to master branch it will automatically push our package, making it ready for use. - - -# Create a simple Maven application - -First of all, we need to create our application. We choose to have a very simple one, but it could be any Maven application. The simplest way is to use Maven itself, we've just to run the following command: - -```bash -mvn archetype:generate -DgroupId=com.example.app -DartifactId=maven-example-app -Dversion=1.0 -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false -``` +## Index -Done! Let's move into the maven-example-app directory. Now we've our app to work with. - -The project structure is quite simple, and we're interested mainly in these resources: - -- `pom.xml`: project object model (POM) file -- `src/main/java/com/example/app/App.java`: source of our application (it prints "Hello World!" to stdout) - -# Test our app locally - -If we want to be sure the application has been created correctly, we can compile and test it: - -``` -mvn compile && mvn test -``` +1. [Get a simple Maven application](#get-a-simple-maven-application) +1. [Configure Continuous Integration with `.gitlab-ci.yml`](#configure-continuous-integration-with-gitlab-ciyml) +1. [Set up Artifactory as the deployment repo](#set-up-artifactory-as-the-deployment-repo) +1. [Configure automatic deployment](#configure-automatic-deployment) -Note: every time we run a `mvn` command it may happen that a bunch of files are downloaded: it's totally normal, and these files are cached so we don't have to download them again next time. - -At the end of the run, we should see an output like this: - -``` -[INFO] ------------------------------------------------------------------------ -[INFO] BUILD SUCCESS -[INFO] ------------------------------------------------------------------------ -[INFO] Total time: 5.614 s -[INFO] Finished at: 2017-06-05T10:50:36+02:00 -[INFO] Final Memory: 17M/130M -[INFO] ------------------------------------------------------------------------ -``` +In this article, we're going to see how we can leverage the power of GitLab Continuous Integration features to compile and test a Maven application, +and finally deploy it to an Artifactory repository with just a very few lines of configuration. + +Every time we change our sample application, GitLab checks that the new version is still bug free, and after merging to `master` branch it will automatically push the new package +to the remote Artifactory repository, making it ready to use. + +## Get a simple Maven application + +First of all, we need an application to work with: in this specific case we're going to make it simple, but it could be any Maven application. + +For this article we'll use a Maven app that can be cloned at `https://gitlab.com/gitlab-examples/maven/simple-maven-app.git`, so let's login into our GitLab account and create a new project +with `Import project from` -> `Repo by URL`. + +This application is nothing more than a basic Hello World with a stub for a JUnit based test suite. It was created with the `maven-archetype-quickstart` Maven template. +The project structure is really simple, and we're mainly interested in these two resources: +- `pom.xml`: project object model (POM) file - here we've the configuration for our project +- `src/main/java/com/example/app/App.java`: source of our application - it prints "Hello World!" to stdout + +## Configure Continuous Integration with `.gitlab-ci.yml` + +Now that we've our application, we need to define stages that will build and test it automatically. In order to achieve this result, we create a file named `.gitlab-ci.yml` in the root of our git repository, once pushed this file will instruct the runner with all the commands needed. + +Let's see the content of the file: -# Create `.gitlab-ci.yml` - -A very simple `.gitlab-ci.yml` file that performs build and tests of our application is the following: - ```yaml image: maven:latest - + cache: paths: - target/ - + build: + stage: build script: - mvn compile - + test: + stage: test script: - mvn test ``` +We want to use the latest Docker image publicly available for Maven, which already contains everything we need to perform our tasks. Caching the `target` folder, that is the location where our application will be created, is useful in order to speed up the process: Maven runs all its phases in a specific order, so executing `mvn test` will automatically run `mvn compile` if needed, but we want to improve performances caching everything that is already created in a previous step. Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the repository. + +If you're creating the file using the GitLab UI, you just have to commit directly into `master`. Otherwise, if you cloned locally your brand new project, commit and push to remote. + +Done! We've now our changes in the GitLab repo, and a pipeline has already been started for this commit. Let's wait until the pipeline ends, and we should see something like the following text in the job output log. + +``` +Running com.example.app.AppTest +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.049 sec + +Results : + +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 + +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 13.165 s +[INFO] Finished at: 2017-06-26T14:26:43Z +[INFO] Final Memory: 17M/147M +[INFO] ------------------------------------------------------------------------ +Creating cache default... +Created cache +Job succeeded +``` + +**Note**: the `mvn` command downloads a lot of files from the internet, so you'll see a lot of extra activity in the log. + +## Set up Artifactory as the deployment repo -We want to use the latest docker image available for Maven, that already contains everything we need to perform our tasks. We also want to cache the `.m2` folder in the user homedir: this is the place where all the files automatically download by Maven commands are stored, so we can reuse them between stages. The `target` folder is where our application will be created: Maven runs all the phases in a specific order, so running `mvn test` will automatically run `mvn compile` if needed, but we want to improve performances caching everything that is reused. - -# Push the code to GitLab - -Now that we've our app, we want to put it on GitLab! Long story short, we've to create a new project and push the code to it as usual. A new pipeline will run and you've just to wait until it succeed! - -# Set up Artifactory as the deployment repo - -## Configure POM file - +### Configure POM file + Next step is to setup our project to use Artifactory as its repository for artifacts deployment: in order to complete this, we need access to the Artifactory instance. So, first of all let's select the `libs-release-local` repository in the `Set Me Up` section, and copy to clipboard the configuration snipped marked as `Deploy`. This is the "address" of our repo, and it is needed by Maven to push artifacts during the `deploy` stage. Now let's go back to our project and edit the pom.xml file: we have to add the snipped we just copied from Artifactory into the project section, so we can paste it after the dependencies. @@ -108,7 +116,7 @@ The final POM will look like this: ``` -## Configure credentials for the repo +### Configure credentials for the repo One last step is required to actully deploy artifacts to Artifactory: we need to configure credentials for our repo, and best practices want us to create an API key for this task, so we don't have to expose our account password. Let's go back to Artifactory, edit the account settings and generate a new API key. For security reasons, we don't want to expose directly this key into the `.gitlab-ci.yml, so we're going to create secret variables REPO_USERNAME and REPO_PASSWORD containing the username and the key in our GitLab project settings. @@ -132,7 +140,7 @@ We must now include these credentials in the `~/.m2/settings.xml` file, so let's Note that `id` must have the same value as the related `id` field of the `repository` section in `pom.xml`. -# Configure automatic deployment +## Configure automatic deployment Time to change `.gitlab-ci.yml` and add the deploy stage! Maven has the perfect command for that, but it requires `settings.xml` to be in the correct folder, so we need to move it before executing `mvn deploy` command. From 2da50a7ec2084e9bb35546e9f3ae7f0bd913ab2d Mon Sep 17 00:00:00 2001 From: Fabio Busatto Date: Wed, 28 Jun 2017 11:06:24 +0000 Subject: [PATCH 005/141] Update index.md --- .../index.md | 298 ++++++++++-------- 1 file changed, 167 insertions(+), 131 deletions(-) diff --git a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md index 53eebcfdcd2..4c11257e1eb 100644 --- a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md +++ b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md @@ -5,32 +5,86 @@ ## Index -1. [Get a simple Maven application](#get-a-simple-maven-application) -1. [Configure Continuous Integration with `.gitlab-ci.yml`](#configure-continuous-integration-with-gitlab-ciyml) -1. [Set up Artifactory as the deployment repo](#set-up-artifactory-as-the-deployment-repo) -1. [Configure automatic deployment](#configure-automatic-deployment) +## Introduction -In this article, we're going to see how we can leverage the power of GitLab Continuous Integration features to compile and test a Maven application, -and finally deploy it to an Artifactory repository with just a very few lines of configuration. +In this article, we're going to see how we can leverage the power of [GitLab Continuous Integration](https://about.gitlab.com/features/gitlab-ci-cd/) to build a [Maven](https://maven.apache.org/) project, deploy it to [Artifactory](https://www.jfrog.com/artifactory/) and then use it from another Maven application as a dependency. -Every time we change our sample application, GitLab checks that the new version is still bug free, and after merging to `master` branch it will automatically push the new package -to the remote Artifactory repository, making it ready to use. +We're going to create two different projects: +- `simple-maven-dep`: the app built and deployed to Artifactory (available at https://gitlab.com/gitlab-examples/maven/simple-maven-dep) +- `simple-maven-app`: the app using the previous one as a dependency (available at https://gitlab.com/gitlab-examples/maven/simple-maven-app) -## Get a simple Maven application +We assume that we already have a GitLab account on [GitLab.com](https://gitlab.com/), and that we know the basic usage of CI. +We also assume that an Artifactory instance is available and reachable from the Internet, and that we've valid credentials to deploy on it. -First of all, we need an application to work with: in this specific case we're going to make it simple, but it could be any Maven application. +## Create the simple Maven dependency -For this article we'll use a Maven app that can be cloned at `https://gitlab.com/gitlab-examples/maven/simple-maven-app.git`, so let's login into our GitLab account and create a new project -with `Import project from` -> `Repo by URL`. +#### Get the sources + +First of all, we need an application to work with: in this specific case we're going to make it simple, but it could be any Maven application. This will be our dependency we want to package and deploy to Artifactory, in order to be available to other projects. + +For this article we'll use a Maven app that can be cloned at `https://gitlab.com/gitlab-examples/maven/simple-maven-dep.git`, so let's login into our GitLab account and create a new project +with **Import project from ➔ Repo by URL**. + +This application is nothing more than a basic class with a stub for a JUnit based test suite. +It exposes a method called `hello` that accepts a string as input, and prints an hello message on the screen. -This application is nothing more than a basic Hello World with a stub for a JUnit based test suite. It was created with the `maven-archetype-quickstart` Maven template. The project structure is really simple, and we're mainly interested in these two resources: -- `pom.xml`: project object model (POM) file - here we've the configuration for our project -- `src/main/java/com/example/app/App.java`: source of our application - it prints "Hello World!" to stdout +- `pom.xml`: project object model (POM) configuration file +- `src/main/java/com/example/dep/Dep.java`: source of our application -## Configure Continuous Integration with `.gitlab-ci.yml` +#### Configure Artifactory deployment -Now that we've our application, we need to define stages that will build and test it automatically. In order to achieve this result, we create a file named `.gitlab-ci.yml` in the root of our git repository, once pushed this file will instruct the runner with all the commands needed. +The application is ready to use, but we need some additional steps for deploying it to Artifactory: +1. login to Artifactory with your user's credentials +2. from the main screen, click on the `libs-release-local` item in the **Set Me Up** panel +3. copy to clipboard the configuration snippet under the **Deploy** paragraph + +The snippet should look like this: + +```xml + + + central + 83d43b5afeb5-releases + ${repoURL}/libs-release-local + + +``` +**Note**: `url` has been added in order to make it configurable but could be kept static, we'll see later how to use secret variables for this. + +Now let's copy the snippet in the `pom.xml` file for our project, just after the `dependencies` section. Easy! + +Another step we need to do before we can deploy our dependency to Artifactory is to configure authentication data. It is a simple task, but Maven requires it to stay in a file called `settings.xml` that has to be in the `.m2` subfolder in the user's homedir. Since we want to use GitLab Runner to automatically deploy the application, we should create the file in our project home and then move it to the proper location with a specific command in the `.gitlab-ci.yml`. + +For this scope, let's create a file `.maven-settings.xml` and copy the following text in it. + +```xml + + + + central + ${repoUser} + ${repoKey} + + + +``` + +**Note**: `username` and `password` will be replaced by the correct values using secret variables. + +We should remember to commit this file to our repo! + +#### Configure GitLab Continuous Integration for `simple-maven-dep` + +Now it's time we set up GitLab CI to automatically build, test and deploy our dependency! + +First of all, we should remember that we need to setup some secret variable for making the deploy happen, so let's go in the **Settings ➔ Pipelines** and add the following secret variables (replace them with your current values, of course): +- **ARTIFACTORY_REPO_URL**: `http://artifactory.example.com:8081/artifactory` (your Artifactory URL) +- **ARTIFACTORY_REPO_USER**: `gitlab` (your Artifactory username) +- **ARTIFACTORY_REPO_KEY**: `AKCp2WXr3G61Xjz1PLmYa3arm3yfBozPxSta4taP3SeNu2HPXYa7FhNYosnndFNNgoEds8BCS` (your Artifactory API Key) + +Now it's time to define stages in our `.gitlab-ci.yml` file: once pushed to our repo it will instruct the GitLab Runner with all the needed commands. Let's see the content of the file: @@ -50,125 +104,107 @@ test: stage: test script: - mvn test -``` -We want to use the latest Docker image publicly available for Maven, which already contains everything we need to perform our tasks. Caching the `target` folder, that is the location where our application will be created, is useful in order to speed up the process: Maven runs all its phases in a specific order, so executing `mvn test` will automatically run `mvn compile` if needed, but we want to improve performances caching everything that is already created in a previous step. Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the repository. -If you're creating the file using the GitLab UI, you just have to commit directly into `master`. Otherwise, if you cloned locally your brand new project, commit and push to remote. - -Done! We've now our changes in the GitLab repo, and a pipeline has already been started for this commit. Let's wait until the pipeline ends, and we should see something like the following text in the job output log. - -``` -Running com.example.app.AppTest -Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.049 sec - -Results : - -Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 - -[INFO] ------------------------------------------------------------------------ -[INFO] BUILD SUCCESS -[INFO] ------------------------------------------------------------------------ -[INFO] Total time: 13.165 s -[INFO] Finished at: 2017-06-26T14:26:43Z -[INFO] Final Memory: 17M/147M -[INFO] ------------------------------------------------------------------------ -Creating cache default... -Created cache -Job succeeded -``` - -**Note**: the `mvn` command downloads a lot of files from the internet, so you'll see a lot of extra activity in the log. - -## Set up Artifactory as the deployment repo - -### Configure POM file - -Next step is to setup our project to use Artifactory as its repository for artifacts deployment: in order to complete this, we need access to the Artifactory instance. -So, first of all let's select the `libs-release-local` repository in the `Set Me Up` section, and copy to clipboard the configuration snipped marked as `Deploy`. This is the "address" of our repo, and it is needed by Maven to push artifacts during the `deploy` stage. -Now let's go back to our project and edit the pom.xml file: we have to add the snipped we just copied from Artifactory into the project section, so we can paste it after the dependencies. -The final POM will look like this: - -```xml - - 4.0.0 - com.example.app - maven-example-app - jar - 1.0 - maven-example-app - http://maven.apache.org - - - junit - junit - 3.8.1 - test - - - - - central - 0072a36394cd-releases - http://localhost:8081/artifactory/libs-release-local - - - -``` - -### Configure credentials for the repo - -One last step is required to actully deploy artifacts to Artifactory: we need to configure credentials for our repo, and best practices want us to create an API key for this task, so we don't have to expose our account password. -Let's go back to Artifactory, edit the account settings and generate a new API key. For security reasons, we don't want to expose directly this key into the `.gitlab-ci.yml, so we're going to create secret variables REPO_USERNAME and REPO_PASSWORD containing the username and the key in our GitLab project settings. - -[screenshot of secret variables window] - -We must now include these credentials in the `~/.m2/settings.xml` file, so let's create a file named `.maven-settings.xml` in our project folder with the following content: - -```xml - - - - ${REPO_USERNAME} - ${REPO_PASSWORD} - central - - - -``` - -Note that `id` must have the same value as the related `id` field of the `repository` section in `pom.xml`. - -## Configure automatic deployment - -Time to change `.gitlab-ci.yml` and add the deploy stage! Maven has the perfect command for that, but it requires `settings.xml` to be in the correct folder, so we need to move it before executing `mvn deploy` command. - -The complete file is now this: - -```yaml -image: maven:latest - -cache: - paths: - - target/ - -build: - stage: build - script: - - mvn compile - -test: - stage: test - script: - - mvn test - deploy: stage: deploy script: - cp .maven-settings.xml ~/.m2/settings.xml - - mvn deploy + - mvn deploy -DrepoUrl=$ARTIFACTORY_REPO_URL -DrepoUsername=$ARTIFACTORY_REPO_USER -DrepoPassword=$ARTIFACTORY_REPO_KEY only: - master ``` -We're ready to go! Every merge (or push) to master will now trigger the deployment to our Artifactory repository! \ No newline at end of file + +We're going to use the latest Docker image publicly available for Maven, which already contains everything we need to perform our tasks. Caching the `target` folder, that is the location where our application will be created, is useful in order to speed up the process: Maven runs all its phases in a sequential order, so executing `mvn test` will automatically run `mvn compile` if needed, but we want to improve performances by caching everything that has been already created in a previous stage. Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the repository. + +Deployment copies the configuration file in the proper location, and then deploys to Artifactory as defined by the secret variables we set up earlier. The deployment occurs only if we're pushing or merging to `master` branch, so development versions are tested but not published. + +Done! We've now our changes in the GitLab repo, and a pipeline has already been started for this commit. Let's go to the **Pipelines** tab and see what happens. +If we've no errors, we can see some text like this at the end of the `deploy` job output log: + +``` +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 1.983 s + +``` + +**Note**: the `mvn` command downloads a lot of files from the Internet, so you'll see a lot of extra activity in the log. + +Wow! We did it! Checking in Artifactory will confirm that we've a new artifact available in the `libs-release-local` repo. + +## Create the main Maven application + +#### Prepare the application + +Now that we've our dependency available on Artifactory, we want to use it! + +Let's create another application by cloning the one we can find at `https://gitlab.com/gitlab-examples/maven/simple-maven-app.git`. +If you look at the `src/main/java/com/example/app/App.java` file you can see that it imports the `com.example.dep.Dep` class and calls the `hello` method passing `GitLab` as a parameter. + +Since Maven doesn't know how to resolve the dependency, we need to modify the configuration. +Let's go back to Artifactory, and browse the `libs-release-local` repository selecting the `simple-maven-dep-1.0.jar` file. In the **Dependency Declaration** section of the main panel we can copy the configuration snippet: + +```xml + + com.example.dep + simple-maven-dep + 1.0 + +``` + +Let's just copy this in the `dependencies` section of our `pom.xml` file. + +#### Configure the Artifactory repository location + +At this point we defined our dependency for the application, but we still miss where we can find the required files. +We need to create a `.maven-settings.xml` file as we did for our dependency project, and move it to the proper location for each job. + +Here is how we can get the content of the file directly from Artifactory: +1. from the main screen, click on the `libs-release-local` item in the **Set Me Up** panel +2. click on **Generate Maven Settings** +3. click on **Generate Settings** +3. copy to clipboard the configuration file +4. save the file as `.maven-settings.xml` in your repo, removing the `servers` section entirely + +Now we're ready to use our Artifactory repository to resolve dependencies and use `simple-maven-dep` in our application! + +#### Configure GitLab Continuous Integration for `simple-maven-app` + +We need a last step to have everything in place: configure `.gitlab-ci.yml`. + +We want to build, test and run our awesome application, and see if we can get the greeting we expect! + +So let's add the `.gitlab-ci.yml` to our repo: + +```yaml +image: maven:latest + +stages: + - build + - test + - run + +cache: + paths: + - target/ + +build: + stage: build + script: + - mvn compile + +test: + stage: test + script: + - mvn test + +run: + stage: run + script: + - cp .maven-settings.xml ~/.m2/settings.xml + - mvn package + - mvn exec:java -Dexec.mainClass="com.example.app.App" +``` + +And that's it! In the `run` job output log we will find a friendly hello to GitLab! \ No newline at end of file From 43f48bc27b8fa5f8ed2465ce401df965a3529ea9 Mon Sep 17 00:00:00 2001 From: Fabio Busatto Date: Wed, 28 Jun 2017 12:50:19 +0000 Subject: [PATCH 006/141] Update index.md --- .../index.md | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md index 4c11257e1eb..49145da5d1d 100644 --- a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md +++ b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md @@ -5,6 +5,18 @@ ## Index +- [Introduction](#introduction) + +- [Create the simple Maven dependency](#create-the-simple-maven-dependency) + - [Get the sources](#get-the-sources) + - [Configure Artifactory deployment](#configure-artifactory-deployment) + - [Configure GitLab Continuous Integration for `simple-maven-dep`](#configure-gitlab-continuous-integration-for-simple-maven-dep) + +- [Create the main Maven application](#create-the-main-maven-application) + - [Prepare the application](#prepare-the-application) + - [Configure the Artifactory repository location](#configure-the-artifactory-repository-location) + - [Configure GitLab Continuous Integration for `simple-maven-app`](#configure-gitlab-continuous-integration-for-simple-maven-app) + ## Introduction In this article, we're going to see how we can leverage the power of [GitLab Continuous Integration](https://about.gitlab.com/features/gitlab-ci-cd/) to build a [Maven](https://maven.apache.org/) project, deploy it to [Artifactory](https://www.jfrog.com/artifactory/) and then use it from another Maven application as a dependency. @@ -23,7 +35,7 @@ We also assume that an Artifactory instance is available and reachable from the First of all, we need an application to work with: in this specific case we're going to make it simple, but it could be any Maven application. This will be our dependency we want to package and deploy to Artifactory, in order to be available to other projects. For this article we'll use a Maven app that can be cloned at `https://gitlab.com/gitlab-examples/maven/simple-maven-dep.git`, so let's login into our GitLab account and create a new project -with **Import project from ➔ Repo by URL**. +with **Import project from ➔ Repo by URL**. Let's make it `public` so anyone can contribute! This application is nothing more than a basic class with a stub for a JUnit based test suite. It exposes a method called `hello` that accepts a string as input, and prints an hello message on the screen. @@ -38,6 +50,7 @@ The application is ready to use, but we need some additional steps for deploying 1. login to Artifactory with your user's credentials 2. from the main screen, click on the `libs-release-local` item in the **Set Me Up** panel 3. copy to clipboard the configuration snippet under the **Deploy** paragraph +4. change the `url` value in order to have it configurable via secret variables The snippet should look like this: @@ -46,11 +59,10 @@ The snippet should look like this: central 83d43b5afeb5-releases - ${repoURL}/libs-release-local + ${repoUrl}/libs-release-local ``` -**Note**: `url` has been added in order to make it configurable but could be kept static, we'll see later how to use secret variables for this. Now let's copy the snippet in the `pom.xml` file for our project, just after the `dependencies` section. Easy! @@ -73,7 +85,7 @@ For this scope, let's create a file `.maven-settings.xml` and copy the following **Note**: `username` and `password` will be replaced by the correct values using secret variables. -We should remember to commit this file to our repo! +We should remember to commit all the changes to our repo! #### Configure GitLab Continuous Integration for `simple-maven-dep` @@ -109,7 +121,7 @@ deploy: stage: deploy script: - cp .maven-settings.xml ~/.m2/settings.xml - - mvn deploy -DrepoUrl=$ARTIFACTORY_REPO_URL -DrepoUsername=$ARTIFACTORY_REPO_USER -DrepoPassword=$ARTIFACTORY_REPO_KEY + - mvn deploy -DrepoUrl=$ARTIFACTORY_REPO_URL -DrepoUser=$ARTIFACTORY_REPO_USER -DrepoKey=$ARTIFACTORY_REPO_KEY only: - master ``` @@ -139,7 +151,7 @@ Wow! We did it! Checking in Artifactory will confirm that we've a new artifact a Now that we've our dependency available on Artifactory, we want to use it! -Let's create another application by cloning the one we can find at `https://gitlab.com/gitlab-examples/maven/simple-maven-app.git`. +Let's create another application by cloning the one we can find at `https://gitlab.com/gitlab-examples/maven/simple-maven-app.git`, and make it `public` too! If you look at the `src/main/java/com/example/app/App.java` file you can see that it imports the `com.example.dep.Dep` class and calls the `hello` method passing `GitLab` as a parameter. Since Maven doesn't know how to resolve the dependency, we need to modify the configuration. @@ -189,6 +201,9 @@ cache: paths: - target/ +before_script: + - cp .maven-settings.xml ~/.m2/settings.xml + build: stage: build script: @@ -202,7 +217,6 @@ test: run: stage: run script: - - cp .maven-settings.xml ~/.m2/settings.xml - mvn package - mvn exec:java -Dexec.mainClass="com.example.app.App" ``` From 10c2ae8c5a884de6cb7339e0a7af70cb08e35268 Mon Sep 17 00:00:00 2001 From: Fabio Busatto Date: Wed, 28 Jun 2017 23:41:12 +0000 Subject: [PATCH 007/141] Update index.md --- .../index.md | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md index 49145da5d1d..ddb9ee6c9b8 100644 --- a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md +++ b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md @@ -66,9 +66,9 @@ The snippet should look like this: Now let's copy the snippet in the `pom.xml` file for our project, just after the `dependencies` section. Easy! -Another step we need to do before we can deploy our dependency to Artifactory is to configure authentication data. It is a simple task, but Maven requires it to stay in a file called `settings.xml` that has to be in the `.m2` subfolder in the user's homedir. Since we want to use GitLab Runner to automatically deploy the application, we should create the file in our project home and then move it to the proper location with a specific command in the `.gitlab-ci.yml`. +Another step we need to do before we can deploy our dependency to Artifactory is to configure authentication data. It is a simple task, but Maven requires it to stay in a file called `settings.xml` that has to be in the `.m2` subfolder in the user's homedir. Since we want to use GitLab Runner to automatically deploy the application, we should create the file in our project home and set a command line parameter in `.gitlab-ci.yml` to use our location instead of the default one. -For this scope, let's create a file `.maven-settings.xml` and copy the following text in it. +For this scope, let's create a folder called `.m2` in the root of our repo. Inside we must create a file named `settings.xml` and copy the following text in it. ```xml Date: Wed, 28 Jun 2017 23:47:10 +0000 Subject: [PATCH 008/141] Update index.md --- .../index.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md index ddb9ee6c9b8..8f67b56385d 100644 --- a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md +++ b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md @@ -56,11 +56,11 @@ The snippet should look like this: ```xml - - central - 83d43b5afeb5-releases - ${repoUrl}/libs-release-local - + + central + 83d43b5afeb5-releases + ${repoUrl}/libs-release-local + ``` @@ -132,7 +132,7 @@ deploy: We're going to use the latest Docker image publicly available for Maven, which already contains everything we need to perform our tasks. Environment variables are set to instruct Maven to use the homedir of our repo instead of the user's home. Caching the `.m2` folder, where all the Maven files are stored, and the `target` folder, that is the location where our application will be created, is useful in order to speed up the process: Maven runs all its phases in a sequential order, so executing `mvn test` will automatically run `mvn compile` if needed, but we want to improve performances by caching everything that has been already created in a previous stage. Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the repository. -Deployment copies the configuration file in the proper location, and then deploys to Artifactory as defined by the secret variables we set up earlier. The deployment occurs only if we're pushing or merging to `master` branch, so development versions are tested but not published. +Deploy to Artifactory is done as defined by the secret variables we set up earlier. The deployment occurs only if we're pushing or merging to `master` branch, so development versions are tested but not published. Done! We've now our changes in the GitLab repo, and a pipeline has already been started for this commit. Let's go to the **Pipelines** tab and see what happens. If we've no errors, we can see some text like this at the end of the `deploy` job output log: @@ -145,7 +145,7 @@ If we've no errors, we can see some text like this at the end of the `deploy` jo ``` -**Note**: the `mvn` command downloads a lot of files from the Internet, so you'll see a lot of extra activity in the log. +**Note**: the `mvn` command downloads a lot of files from the Internet, so you'll see a lot of extra activity in the log the first time you run it. Wow! We did it! Checking in Artifactory will confirm that we've a new artifact available in the `libs-release-local` repo. @@ -163,9 +163,9 @@ Let's go back to Artifactory, and browse the `libs-release-local` repository sel ```xml - com.example.dep - simple-maven-dep - 1.0 + com.example.dep + simple-maven-dep + 1.0 ``` From 5474b4e0ce643f316d146dfa57f28a0dc874033e Mon Sep 17 00:00:00 2001 From: Fabio Busatto Date: Thu, 29 Jun 2017 00:01:25 +0000 Subject: [PATCH 009/141] Update index.md --- .../index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md index 8f67b56385d..5f326588372 100644 --- a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md +++ b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md @@ -109,7 +109,7 @@ variables: cache: paths: - - .m2/ + - .m2/repository/ - target/ build: @@ -130,7 +130,7 @@ deploy: - master ``` -We're going to use the latest Docker image publicly available for Maven, which already contains everything we need to perform our tasks. Environment variables are set to instruct Maven to use the homedir of our repo instead of the user's home. Caching the `.m2` folder, where all the Maven files are stored, and the `target` folder, that is the location where our application will be created, is useful in order to speed up the process: Maven runs all its phases in a sequential order, so executing `mvn test` will automatically run `mvn compile` if needed, but we want to improve performances by caching everything that has been already created in a previous stage. Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the repository. +We're going to use the latest Docker image publicly available for Maven, which already contains everything we need to perform our tasks. Environment variables are set to instruct Maven to use the homedir of our repo instead of the user's home. Caching the `.m2/repository` folder, where all the Maven files are stored, and the `target` folder, that is the location where our application will be created, is useful in order to speed up the process: Maven runs all its phases in a sequential order, so executing `mvn test` will automatically run `mvn compile` if needed, but we want to improve performances by caching everything that has been already created in a previous stage. Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the repository. Deploy to Artifactory is done as defined by the secret variables we set up earlier. The deployment occurs only if we're pushing or merging to `master` branch, so development versions are tested but not published. @@ -207,7 +207,7 @@ variables: cache: paths: - - .m2/ + - .m2/repository/ - target/ build: From 1697e2c485b8d1e85df8e5723793a6316272ec15 Mon Sep 17 00:00:00 2001 From: Fabio Busatto Date: Thu, 29 Jun 2017 07:05:59 +0000 Subject: [PATCH 010/141] Update index.md --- .../index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md index 5f326588372..afcce2dfe23 100644 --- a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md +++ b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md @@ -34,7 +34,7 @@ We also assume that an Artifactory instance is available and reachable from the First of all, we need an application to work with: in this specific case we're going to make it simple, but it could be any Maven application. This will be our dependency we want to package and deploy to Artifactory, in order to be available to other projects. -For this article we'll use a Maven app that can be cloned at `https://gitlab.com/gitlab-examples/maven/simple-maven-dep.git`, so let's login into our GitLab account and create a new project +For this article we'll use a Maven app that can be cloned from `https://gitlab.com/gitlab-examples/maven/simple-maven-dep.git`, so let's login into our GitLab account and create a new project with **Import project from ➔ Repo by URL**. Let's make it `public` so anyone can contribute! This application is nothing more than a basic class with a stub for a JUnit based test suite. @@ -125,7 +125,7 @@ test: deploy: stage: deploy script: - - mvn deploy $fMAVEN_CLI_OPTS -DrepoUrl=$ARTIFACTORY_REPO_URL -DrepoUser=$ARTIFACTORY_REPO_USER -DrepoKey=$ARTIFACTORY_REPO_KEY + - mvn deploy $MAVEN_CLI_OPTS -DrepoUrl=$ARTIFACTORY_REPO_URL -DrepoUser=$ARTIFACTORY_REPO_USER -DrepoKey=$ARTIFACTORY_REPO_KEY only: - master ``` @@ -181,7 +181,7 @@ Here is how we can get the content of the file directly from Artifactory: 2. click on **Generate Maven Settings** 3. click on **Generate Settings** 3. copy to clipboard the configuration file -4. save the file as `.m2/settings.xml` in your repo, removing the `servers` section entirely +4. save the file as `.m2/settings.xml` in your repo Now we're ready to use our Artifactory repository to resolve dependencies and use `simple-maven-dep` in our application! From ecd377cd3531bb925bfe26af131e224d3fdc3fea Mon Sep 17 00:00:00 2001 From: Fabio Busatto Date: Mon, 3 Jul 2017 10:38:37 +0000 Subject: [PATCH 011/141] Update index.md --- .../index.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md index afcce2dfe23..142ab373a73 100644 --- a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md +++ b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md @@ -59,7 +59,7 @@ The snippet should look like this: central 83d43b5afeb5-releases - ${repoUrl}/libs-release-local + ${env.MAVEN_REPO_URL}/libs-release-local ``` @@ -76,8 +76,8 @@ For this scope, let's create a folder called `.m2` in the root of our repo. Insi central - ${repoUser} - ${repoKey} + ${env.MAVEN_REPO_USER} + ${env.MAVEN_REPO_KEY} @@ -92,9 +92,9 @@ We should remember to commit all the changes to our repo! Now it's time we set up GitLab CI to automatically build, test and deploy our dependency! First of all, we should remember that we need to setup some secret variable for making the deploy happen, so let's go in the **Settings ➔ Pipelines** and add the following secret variables (replace them with your current values, of course): -- **ARTIFACTORY_REPO_URL**: `http://artifactory.example.com:8081/artifactory` (your Artifactory URL) -- **ARTIFACTORY_REPO_USER**: `gitlab` (your Artifactory username) -- **ARTIFACTORY_REPO_KEY**: `AKCp2WXr3G61Xjz1PLmYa3arm3yfBozPxSta4taP3SeNu2HPXYa7FhNYosnndFNNgoEds8BCS` (your Artifactory API Key) +- **MAVEN_REPO_URL**: `http://artifactory.example.com:8081/artifactory` (your Artifactory URL) +- **MAVEN_REPO_USER**: `gitlab` (your Artifactory username) +- **MAVEN_REPO_KEY**: `AKCp2WXr3G61Xjz1PLmYa3arm3yfBozPxSta4taP3SeNu2HPXYa7FhNYosnndFNNgoEds8BCS` (your Artifactory API Key) Now it's time to define stages in our `.gitlab-ci.yml` file: once pushed to our repo it will instruct the GitLab Runner with all the needed commands. @@ -104,7 +104,7 @@ Let's see the content of the file: image: maven:latest variables: - MAVEN_CLI_OPTS: "-s .m2/settings.xml" + MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode" MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository" cache: @@ -125,7 +125,7 @@ test: deploy: stage: deploy script: - - mvn deploy $MAVEN_CLI_OPTS -DrepoUrl=$ARTIFACTORY_REPO_URL -DrepoUser=$ARTIFACTORY_REPO_USER -DrepoKey=$ARTIFACTORY_REPO_KEY + - mvn $MAVEN_CLI_OPTS deploy only: - master ``` @@ -202,7 +202,7 @@ stages: - run variables: - MAVEN_CLI_OPTS: "-s .m2/settings.xml" + MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode" MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository" cache: From 41b9a08cebd8b63a0b5441244af921e631e79a51 Mon Sep 17 00:00:00 2001 From: Fabio Busatto Date: Fri, 21 Jul 2017 07:01:59 +0000 Subject: [PATCH 012/141] Add Conclusion Fix styling --- .../index.md | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md index 142ab373a73..b9472602e7b 100644 --- a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md +++ b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md @@ -17,6 +17,8 @@ - [Configure the Artifactory repository location](#configure-the-artifactory-repository-location) - [Configure GitLab Continuous Integration for `simple-maven-app`](#configure-gitlab-continuous-integration-for-simple-maven-app) +- [Conclusion](#conclusion) + ## Introduction In this article, we're going to see how we can leverage the power of [GitLab Continuous Integration](https://about.gitlab.com/features/gitlab-ci-cd/) to build a [Maven](https://maven.apache.org/) project, deploy it to [Artifactory](https://www.jfrog.com/artifactory/) and then use it from another Maven application as a dependency. @@ -83,7 +85,8 @@ For this scope, let's create a folder called `.m2` in the root of our repo. Insi ``` -**Note**: `username` and `password` will be replaced by the correct values using secret variables. +>**Note**: +`username` and `password` will be replaced by the correct values using secret variables. We should remember to commit all the changes to our repo! @@ -91,7 +94,8 @@ We should remember to commit all the changes to our repo! Now it's time we set up GitLab CI to automatically build, test and deploy our dependency! -First of all, we should remember that we need to setup some secret variable for making the deploy happen, so let's go in the **Settings ➔ Pipelines** and add the following secret variables (replace them with your current values, of course): +First of all, we should remember that we need to setup some secret variable for making the deploy happen, so let's go in the **Settings ➔ Pipelines** +and add the following secret variables (replace them with your current values, of course): - **MAVEN_REPO_URL**: `http://artifactory.example.com:8081/artifactory` (your Artifactory URL) - **MAVEN_REPO_USER**: `gitlab` (your Artifactory username) - **MAVEN_REPO_KEY**: `AKCp2WXr3G61Xjz1PLmYa3arm3yfBozPxSta4taP3SeNu2HPXYa7FhNYosnndFNNgoEds8BCS` (your Artifactory API Key) @@ -130,9 +134,15 @@ deploy: - master ``` -We're going to use the latest Docker image publicly available for Maven, which already contains everything we need to perform our tasks. Environment variables are set to instruct Maven to use the homedir of our repo instead of the user's home. Caching the `.m2/repository` folder, where all the Maven files are stored, and the `target` folder, that is the location where our application will be created, is useful in order to speed up the process: Maven runs all its phases in a sequential order, so executing `mvn test` will automatically run `mvn compile` if needed, but we want to improve performances by caching everything that has been already created in a previous stage. Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the repository. +We're going to use the latest Docker image publicly available for Maven, which already contains everything we need to perform our tasks. +Environment variables are set to instruct Maven to use the homedir of our repo instead of the user's home. +Caching the `.m2/repository` folder, where all the Maven files are stored, and the `target` folder, that is the location where our application will be created, +is useful in order to speed up the process: Maven runs all its phases in a sequential order, so executing `mvn test` will automatically run `mvn compile` if needed, +but we want to improve performances by caching everything that has been already created in a previous stage. +Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the repository. -Deploy to Artifactory is done as defined by the secret variables we set up earlier. The deployment occurs only if we're pushing or merging to `master` branch, so development versions are tested but not published. +Deploy to Artifactory is done as defined by the secret variables we set up earlier. +The deployment occurs only if we're pushing or merging to `master` branch, so development versions are tested but not published. Done! We've now our changes in the GitLab repo, and a pipeline has already been started for this commit. Let's go to the **Pipelines** tab and see what happens. If we've no errors, we can see some text like this at the end of the `deploy` job output log: @@ -145,7 +155,8 @@ If we've no errors, we can see some text like this at the end of the `deploy` jo ``` -**Note**: the `mvn` command downloads a lot of files from the Internet, so you'll see a lot of extra activity in the log the first time you run it. +>**Note**: +the `mvn` command downloads a lot of files from the Internet, so you'll see a lot of extra activity in the log the first time you run it. Wow! We did it! Checking in Artifactory will confirm that we've a new artifact available in the `libs-release-local` repo. @@ -159,7 +170,8 @@ Let's create another application by cloning the one we can find at `https://gitl If you look at the `src/main/java/com/example/app/App.java` file you can see that it imports the `com.example.dep.Dep` class and calls the `hello` method passing `GitLab` as a parameter. Since Maven doesn't know how to resolve the dependency, we need to modify the configuration. -Let's go back to Artifactory, and browse the `libs-release-local` repository selecting the `simple-maven-dep-1.0.jar` file. In the **Dependency Declaration** section of the main panel we can copy the configuration snippet: +Let's go back to Artifactory, and browse the `libs-release-local` repository selecting the `simple-maven-dep-1.0.jar` file. +In the **Dependency Declaration** section of the main panel we can copy the configuration snippet: ```xml @@ -227,4 +239,13 @@ run: - mvn $MAVEN_CLI_OPTS exec:java -Dexec.mainClass="com.example.app.App" ``` -And that's it! In the `run` job output log we will find a friendly hello to GitLab! \ No newline at end of file +And that's it! In the `run` job output log we will find a friendly hello to GitLab! + +## Conclusion + +In this article we covered the basic steps to use an Artifactory Maven repository to automatically publish and consume our artifacts. + +A similar approach could be used to interact with any other Maven compatible Binary Repository Manager. +You can improve these examples, optimizing the `.gitlab-ci.yml` file to better suit your needs, and adapting to your workflow. + +Enjoy GitLab CI with all your Maven projects! \ No newline at end of file From 829a73af1ff0d866633d22ddea673bbf74bb4e30 Mon Sep 17 00:00:00 2001 From: Fabio Busatto Date: Tue, 25 Jul 2017 00:38:19 +0000 Subject: [PATCH 013/141] Update password options --- .../index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md index b9472602e7b..784676ae8d2 100644 --- a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md +++ b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md @@ -79,7 +79,7 @@ For this scope, let's create a folder called `.m2` in the root of our repo. Insi central ${env.MAVEN_REPO_USER} - ${env.MAVEN_REPO_KEY} + ${env.MAVEN_REPO_PASS} @@ -98,7 +98,7 @@ First of all, we should remember that we need to setup some secret variable for and add the following secret variables (replace them with your current values, of course): - **MAVEN_REPO_URL**: `http://artifactory.example.com:8081/artifactory` (your Artifactory URL) - **MAVEN_REPO_USER**: `gitlab` (your Artifactory username) -- **MAVEN_REPO_KEY**: `AKCp2WXr3G61Xjz1PLmYa3arm3yfBozPxSta4taP3SeNu2HPXYa7FhNYosnndFNNgoEds8BCS` (your Artifactory API Key) +- **MAVEN_REPO_PASS**: `AKCp2WXr3G61Xjz1PLmYa3arm3yfBozPxSta4taP3SeNu2HPXYa7FhNYosnndFNNgoEds8BCS` (your Artifactory Encrypted Password) Now it's time to define stages in our `.gitlab-ci.yml` file: once pushed to our repo it will instruct the GitLab Runner with all the needed commands. From 135e37db6a20b4ee97d7529c5c2636045373812b Mon Sep 17 00:00:00 2001 From: Fabio Busatto Date: Thu, 3 Aug 2017 01:06:19 +0000 Subject: [PATCH 014/141] Update index.md --- .../index.md | 126 ++++++++---------- 1 file changed, 56 insertions(+), 70 deletions(-) diff --git a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md index 784676ae8d2..d2061279646 100644 --- a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md +++ b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md @@ -1,55 +1,43 @@ +# How to deploy Maven projects to Artifactory with GitLab CI/CD + > **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** tutorial || > **Level:** intermediary || > **Author:** [Fabio Busatto](https://gitlab.com/bikebilly) || > **Publication date:** AAAA/MM/DD -## Index - -- [Introduction](#introduction) - -- [Create the simple Maven dependency](#create-the-simple-maven-dependency) - - [Get the sources](#get-the-sources) - - [Configure Artifactory deployment](#configure-artifactory-deployment) - - [Configure GitLab Continuous Integration for `simple-maven-dep`](#configure-gitlab-continuous-integration-for-simple-maven-dep) - -- [Create the main Maven application](#create-the-main-maven-application) - - [Prepare the application](#prepare-the-application) - - [Configure the Artifactory repository location](#configure-the-artifactory-repository-location) - - [Configure GitLab Continuous Integration for `simple-maven-app`](#configure-gitlab-continuous-integration-for-simple-maven-app) - -- [Conclusion](#conclusion) - ## Introduction -In this article, we're going to see how we can leverage the power of [GitLab Continuous Integration](https://about.gitlab.com/features/gitlab-ci-cd/) to build a [Maven](https://maven.apache.org/) project, deploy it to [Artifactory](https://www.jfrog.com/artifactory/) and then use it from another Maven application as a dependency. +In this article, we will show how you can leverage the power of [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/) +to build a [Maven](https://maven.apache.org/) project, deploy it to [Artifactory](https://www.jfrog.com/artifactory/), and then use it from another Maven application as a dependency. -We're going to create two different projects: +You'll create two different projects: - `simple-maven-dep`: the app built and deployed to Artifactory (available at https://gitlab.com/gitlab-examples/maven/simple-maven-dep) - `simple-maven-app`: the app using the previous one as a dependency (available at https://gitlab.com/gitlab-examples/maven/simple-maven-app) -We assume that we already have a GitLab account on [GitLab.com](https://gitlab.com/), and that we know the basic usage of CI. -We also assume that an Artifactory instance is available and reachable from the Internet, and that we've valid credentials to deploy on it. +We assume that you already have a GitLab account on [GitLab.com](https://gitlab.com/), and that you know the basic usage of GitLab CI/CD. +We also assume that an Artifactory instance is available and reachable from the internet, and that you have valid credentials to deploy on it. ## Create the simple Maven dependency -#### Get the sources +### Get the sources -First of all, we need an application to work with: in this specific case we're going to make it simple, but it could be any Maven application. This will be our dependency we want to package and deploy to Artifactory, in order to be available to other projects. +First of all, you need an application to work with: in this specific it is a simple one, but it could be any Maven application. +This will be the dependency you want to package and deploy to Artifactory, in order to be available to other projects. -For this article we'll use a Maven app that can be cloned from `https://gitlab.com/gitlab-examples/maven/simple-maven-dep.git`, so let's login into our GitLab account and create a new project -with **Import project from ➔ Repo by URL**. Let's make it `public` so anyone can contribute! +For this article you'll use a Maven app that can be cloned from `https://gitlab.com/gitlab-examples/maven/simple-maven-dep.git`, +so log in to your GitLab account and create a new project with **Import project from ➔ Repo by URL**. This application is nothing more than a basic class with a stub for a JUnit based test suite. -It exposes a method called `hello` that accepts a string as input, and prints an hello message on the screen. +It exposes a method called `hello` that accepts a string as input, and prints a hello message on the screen. -The project structure is really simple, and we're mainly interested in these two resources: +The project structure is really simple, and you should consider these two resources: - `pom.xml`: project object model (POM) configuration file - `src/main/java/com/example/dep/Dep.java`: source of our application -#### Configure Artifactory deployment +### Configure Artifactory deployment -The application is ready to use, but we need some additional steps for deploying it to Artifactory: -1. login to Artifactory with your user's credentials +The application is ready to use, but you need some additional steps to deploy it to Artifactory: +1. log in to Artifactory with your user's credentials 2. from the main screen, click on the `libs-release-local` item in the **Set Me Up** panel 3. copy to clipboard the configuration snippet under the **Deploy** paragraph 4. change the `url` value in order to have it configurable via secret variables @@ -66,11 +54,14 @@ The snippet should look like this: ``` -Now let's copy the snippet in the `pom.xml` file for our project, just after the `dependencies` section. Easy! +Now copy the snippet in the `pom.xml` file for your project, just after the `dependencies` section. Easy! -Another step we need to do before we can deploy our dependency to Artifactory is to configure authentication data. It is a simple task, but Maven requires it to stay in a file called `settings.xml` that has to be in the `.m2` subfolder in the user's homedir. Since we want to use GitLab Runner to automatically deploy the application, we should create the file in our project home and set a command line parameter in `.gitlab-ci.yml` to use our location instead of the default one. +Another step you need to do before you can deploy the dependency to Artifactory is to configure authentication data. +It is a simple task, but Maven requires it to stay in a file called `settings.xml` that has to be in the `.m2` subfolder in the user's homedir. +Since you want to use GitLab Runner to automatically deploy the application, you should create the file in the project home +and set a command line parameter in `.gitlab-ci.yml` to use the custom location instead of the default one. -For this scope, let's create a folder called `.m2` in the root of our repo. Inside we must create a file named `settings.xml` and copy the following text in it. +For this scope, create a folder called `.m2` in the root of the repo. You must create a file named `settings.xml` there, and copy the following text into it. ```xml **Note**: `username` and `password` will be replaced by the correct values using secret variables. -We should remember to commit all the changes to our repo! +Remember to commit all the changes to the repo! -#### Configure GitLab Continuous Integration for `simple-maven-dep` +### Configure GitLab CI/CD for `simple-maven-dep` -Now it's time we set up GitLab CI to automatically build, test and deploy our dependency! +Now it's time we set up GitLab CI/CD to automatically build, test and deploy the dependency! -First of all, we should remember that we need to setup some secret variable for making the deploy happen, so let's go in the **Settings ➔ Pipelines** +First of all, remember to set up secret variables for your deployment. Navigate to your project's **Settings > Pipelines** page and add the following secret variables (replace them with your current values, of course): - **MAVEN_REPO_URL**: `http://artifactory.example.com:8081/artifactory` (your Artifactory URL) - **MAVEN_REPO_USER**: `gitlab` (your Artifactory username) - **MAVEN_REPO_PASS**: `AKCp2WXr3G61Xjz1PLmYa3arm3yfBozPxSta4taP3SeNu2HPXYa7FhNYosnndFNNgoEds8BCS` (your Artifactory Encrypted Password) -Now it's time to define stages in our `.gitlab-ci.yml` file: once pushed to our repo it will instruct the GitLab Runner with all the needed commands. - -Let's see the content of the file: +Now it's time to define jobs in `.gitlab-ci.yml` file: once pushed to the repo it will instruct the GitLab Runner with all the needed commands. ```yaml image: maven:latest @@ -134,18 +123,17 @@ deploy: - master ``` -We're going to use the latest Docker image publicly available for Maven, which already contains everything we need to perform our tasks. -Environment variables are set to instruct Maven to use the homedir of our repo instead of the user's home. -Caching the `.m2/repository` folder, where all the Maven files are stored, and the `target` folder, that is the location where our application will be created, -is useful in order to speed up the process: Maven runs all its phases in a sequential order, so executing `mvn test` will automatically run `mvn compile` if needed, -but we want to improve performances by caching everything that has been already created in a previous stage. +It uses the latest Docker image for Maven, which already contains everything you need to perform all the tasks. +Environment variables are set to instruct Maven to use the `homedir` of the repo instead of the user's home. +Caching the .m2/repository folder (where all the Maven files are stored), and the target folder (where our application will be created), will be useful for speeding up the process +by running all Maven phases in a sequential order, therefore, executing `mvn test` will automatically run `mvn compile` if necessary. Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the repository. Deploy to Artifactory is done as defined by the secret variables we set up earlier. -The deployment occurs only if we're pushing or merging to `master` branch, so development versions are tested but not published. +The deployment occurs only if we're pushing or merging to `master` branch, so that the development versions are tested but not published. -Done! We've now our changes in the GitLab repo, and a pipeline has already been started for this commit. Let's go to the **Pipelines** tab and see what happens. -If we've no errors, we can see some text like this at the end of the `deploy` job output log: +Done! Now you have all the changes in the GitLab repo, and a pipeline has already been started for this commit. In the **Pipelines** tab you can see what's happening. +If the deployment has been successful, the deploy job log will output: ``` [INFO] ------------------------------------------------------------------------ @@ -156,22 +144,22 @@ If we've no errors, we can see some text like this at the end of the `deploy` jo ``` >**Note**: -the `mvn` command downloads a lot of files from the Internet, so you'll see a lot of extra activity in the log the first time you run it. +the `mvn` command downloads a lot of files from the internet, so you'll see a lot of extra activity in the log the first time you run it. -Wow! We did it! Checking in Artifactory will confirm that we've a new artifact available in the `libs-release-local` repo. +Yay! You did it! Checking in Artifactory will confirm that you have a new artifact available in the `libs-release-local` repo. ## Create the main Maven application -#### Prepare the application +### Prepare the application -Now that we've our dependency available on Artifactory, we want to use it! +Now that you have the dependency available on Artifactory, you want to use it! -Let's create another application by cloning the one we can find at `https://gitlab.com/gitlab-examples/maven/simple-maven-app.git`, and make it `public` too! +Create another application by cloning the one you can find at `https://gitlab.com/gitlab-examples/maven/simple-maven-app.git`. If you look at the `src/main/java/com/example/app/App.java` file you can see that it imports the `com.example.dep.Dep` class and calls the `hello` method passing `GitLab` as a parameter. -Since Maven doesn't know how to resolve the dependency, we need to modify the configuration. -Let's go back to Artifactory, and browse the `libs-release-local` repository selecting the `simple-maven-dep-1.0.jar` file. -In the **Dependency Declaration** section of the main panel we can copy the configuration snippet: +Since Maven doesn't know how to resolve the dependency, you need to modify the configuration. +Go back to Artifactory, and browse the `libs-release-local` repository selecting the `simple-maven-dep-1.0.jar` file. +In the **Dependency Declaration** section of the main panel you can copy the configuration snippet: ```xml @@ -181,29 +169,29 @@ In the **Dependency Declaration** section of the main panel we can copy the conf ``` -Let's just copy this in the `dependencies` section of our `pom.xml` file. +Copy this in the `dependencies` section of the `pom.xml` file. -#### Configure the Artifactory repository location +### Configure the Artifactory repository location -At this point we defined our dependency for the application, but we still miss where we can find the required files. -We need to create a `.m2/settings.xml` file as we did for our dependency project, and let Maven know the location using environment variables. +At this point you defined the dependency for the application, but you still miss where you can find the required files. +You need to create a `.m2/settings.xml` file as you did for the dependency project, and let Maven know the location using environment variables. -Here is how we can get the content of the file directly from Artifactory: +Here is how you can get the content of the file directly from Artifactory: 1. from the main screen, click on the `libs-release-local` item in the **Set Me Up** panel 2. click on **Generate Maven Settings** 3. click on **Generate Settings** 3. copy to clipboard the configuration file 4. save the file as `.m2/settings.xml` in your repo -Now we're ready to use our Artifactory repository to resolve dependencies and use `simple-maven-dep` in our application! +Now you are ready to use the Artifactory repository to resolve dependencies and use `simple-maven-dep` in your application! -#### Configure GitLab Continuous Integration for `simple-maven-app` +### Configure GitLab CI/CD for `simple-maven-app` -We need a last step to have everything in place: configure `.gitlab-ci.yml`. +You need a last step to have everything in place: configure `.gitlab-ci.yml`. -We want to build, test and run our awesome application, and see if we can get the greeting we expect! +You want to build, test and run your awesome application, and see if you can get the greeting as expected! -So let's add the `.gitlab-ci.yml` to our repo: +Add the `.gitlab-ci.yml` to the repo: ```yaml image: maven:latest @@ -239,13 +227,11 @@ run: - mvn $MAVEN_CLI_OPTS exec:java -Dexec.mainClass="com.example.app.App" ``` -And that's it! In the `run` job output log we will find a friendly hello to GitLab! +And that's it! In the `run` job output log you will find a friendly hello to GitLab! ## Conclusion -In this article we covered the basic steps to use an Artifactory Maven repository to automatically publish and consume our artifacts. +In this article we covered the basic steps to use an Artifactory Maven repository to automatically publish and consume artifacts. A similar approach could be used to interact with any other Maven compatible Binary Repository Manager. -You can improve these examples, optimizing the `.gitlab-ci.yml` file to better suit your needs, and adapting to your workflow. - -Enjoy GitLab CI with all your Maven projects! \ No newline at end of file +Obviously, you can improve these examples, optimizing the `.gitlab-ci.yml` file to better suit your needs, and adapting to your workflow. \ No newline at end of file From ed5445388de13f1d126fec14cc0a9ea9ae03b397 Mon Sep 17 00:00:00 2001 From: Fabio Busatto Date: Thu, 3 Aug 2017 01:23:11 +0000 Subject: [PATCH 015/141] Update index.md --- .../index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md index d2061279646..2dba8688a24 100644 --- a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md +++ b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md @@ -129,7 +129,7 @@ Caching the .m2/repository folder (where all the Maven files are stored), and th by running all Maven phases in a sequential order, therefore, executing `mvn test` will automatically run `mvn compile` if necessary. Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the repository. -Deploy to Artifactory is done as defined by the secret variables we set up earlier. +Deploy to Artifactory is done as defined by the secret variables we have just set up. The deployment occurs only if we're pushing or merging to `master` branch, so that the development versions are tested but not published. Done! Now you have all the changes in the GitLab repo, and a pipeline has already been started for this commit. In the **Pipelines** tab you can see what's happening. From 1379f0e3826b953e8f67a427b759abb7af88df80 Mon Sep 17 00:00:00 2001 From: Fabio Busatto Date: Thu, 3 Aug 2017 09:13:08 +0000 Subject: [PATCH 016/141] Update index.md --- .../index.md | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md index 2dba8688a24..2a8e9ea2717 100644 --- a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md +++ b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md @@ -14,7 +14,7 @@ You'll create two different projects: - `simple-maven-dep`: the app built and deployed to Artifactory (available at https://gitlab.com/gitlab-examples/maven/simple-maven-dep) - `simple-maven-app`: the app using the previous one as a dependency (available at https://gitlab.com/gitlab-examples/maven/simple-maven-app) -We assume that you already have a GitLab account on [GitLab.com](https://gitlab.com/), and that you know the basic usage of GitLab CI/CD. +We assume that you already have a GitLab account on [GitLab.com](https://gitlab.com/), and that you know the basic usage of Git and GitLab CI/CD. We also assume that an Artifactory instance is available and reachable from the internet, and that you have valid credentials to deploy on it. ## Create the simple Maven dependency @@ -41,6 +41,7 @@ The application is ready to use, but you need some additional steps to deploy it 2. from the main screen, click on the `libs-release-local` item in the **Set Me Up** panel 3. copy to clipboard the configuration snippet under the **Deploy** paragraph 4. change the `url` value in order to have it configurable via secret variables +5. copy the snippet in the `pom.xml` file for your project, just after the `dependencies` section The snippet should look like this: @@ -54,14 +55,14 @@ The snippet should look like this: ``` -Now copy the snippet in the `pom.xml` file for your project, just after the `dependencies` section. Easy! - Another step you need to do before you can deploy the dependency to Artifactory is to configure authentication data. It is a simple task, but Maven requires it to stay in a file called `settings.xml` that has to be in the `.m2` subfolder in the user's homedir. -Since you want to use GitLab Runner to automatically deploy the application, you should create the file in the project home -and set a command line parameter in `.gitlab-ci.yml` to use the custom location instead of the default one. -For this scope, create a folder called `.m2` in the root of the repo. You must create a file named `settings.xml` there, and copy the following text into it. +Since you want to use GitLab Runner to automatically deploy the application, you should create the file in the project home +and set a command line parameter in `.gitlab-ci.yml` to use the custom location instead of the default one: +1. create a folder called `.m2` in the root of the repo +2. create a file called `settings.xml` in the `.m2` folder +3. copy the following content into `settings.xml` ```xml **Note**: `username` and `password` will be replaced by the correct values using secret variables. -Remember to commit all the changes to the repo! - ### Configure GitLab CI/CD for `simple-maven-dep` Now it's time we set up GitLab CI/CD to automatically build, test and deploy the dependency! @@ -157,9 +156,14 @@ Now that you have the dependency available on Artifactory, you want to use it! Create another application by cloning the one you can find at `https://gitlab.com/gitlab-examples/maven/simple-maven-app.git`. If you look at the `src/main/java/com/example/app/App.java` file you can see that it imports the `com.example.dep.Dep` class and calls the `hello` method passing `GitLab` as a parameter. -Since Maven doesn't know how to resolve the dependency, you need to modify the configuration. -Go back to Artifactory, and browse the `libs-release-local` repository selecting the `simple-maven-dep-1.0.jar` file. -In the **Dependency Declaration** section of the main panel you can copy the configuration snippet: +Since Maven doesn't know how to resolve the dependency, you need to modify the configuration: +1. go back to Artifactory +2. browse the `libs-release-local` repository +3. select the `simple-maven-dep-1.0.jar` file +4. find the configuration snippet from the **Dependency Declaration** section of the main panel +5. copy the snippet in the `dependencies` section of the `pom.xml` file + +The snippet should look like this: ```xml @@ -169,8 +173,6 @@ In the **Dependency Declaration** section of the main panel you can copy the con ``` -Copy this in the `dependencies` section of the `pom.xml` file. - ### Configure the Artifactory repository location At this point you defined the dependency for the application, but you still miss where you can find the required files. From 4a9dd1d0d18cdb5cdb8b7f45d467554efd7e514b Mon Sep 17 00:00:00 2001 From: Fabio Busatto Date: Thu, 3 Aug 2017 09:44:40 +0000 Subject: [PATCH 017/141] Update index.md --- .../index.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md index 2a8e9ea2717..9622b4b0761 100644 --- a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md +++ b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md @@ -14,7 +14,7 @@ You'll create two different projects: - `simple-maven-dep`: the app built and deployed to Artifactory (available at https://gitlab.com/gitlab-examples/maven/simple-maven-dep) - `simple-maven-app`: the app using the previous one as a dependency (available at https://gitlab.com/gitlab-examples/maven/simple-maven-app) -We assume that you already have a GitLab account on [GitLab.com](https://gitlab.com/), and that you know the basic usage of Git and GitLab CI/CD. +We assume that you already have a GitLab account on [GitLab.com](https://gitlab.com/), and that you know the basic usage of Git and [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/). We also assume that an Artifactory instance is available and reachable from the internet, and that you have valid credentials to deploy on it. ## Create the simple Maven dependency @@ -82,7 +82,10 @@ and set a command line parameter in `.gitlab-ci.yml` to use the custom location ### Configure GitLab CI/CD for `simple-maven-dep` -Now it's time we set up GitLab CI/CD to automatically build, test and deploy the dependency! +Now it's time we set up [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/) to automatically build, test and deploy the dependency! + +[GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/) uses a file in the root of the repo, named `.gitlab-ci.yml`, to read the definitions for jobs +that will be executed by the configured GitLab Runners. You can read more about this file in the [GitLab Documentation](https://docs.gitlab.com/ee/ci/yaml/). First of all, remember to set up secret variables for your deployment. Navigate to your project's **Settings > Pipelines** page and add the following secret variables (replace them with your current values, of course): @@ -189,11 +192,12 @@ Now you are ready to use the Artifactory repository to resolve dependencies and ### Configure GitLab CI/CD for `simple-maven-app` -You need a last step to have everything in place: configure `.gitlab-ci.yml`. +You need a last step to have everything in place: configure the `.gitlab-ci.yml` file for this project, as you already did for `simple-maven-dep`. -You want to build, test and run your awesome application, and see if you can get the greeting as expected! +You want to leverage [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/) to automatically build, test and run your awesome application, +and see if you can get the greeting as expected! -Add the `.gitlab-ci.yml` to the repo: +All you need to do is to add the following `.gitlab-ci.yml` to the repo: ```yaml image: maven:latest @@ -229,6 +233,9 @@ run: - mvn $MAVEN_CLI_OPTS exec:java -Dexec.mainClass="com.example.app.App" ``` +It is very similar to the configuration used for `simple-maven-dep`, but instead of the `deploy` job there is a `run` job. +Probably something that you don't want to use in real projects, but here it is useful to see the application automatically running. + And that's it! In the `run` job output log you will find a friendly hello to GitLab! ## Conclusion From f38e0d5be0df51870d4a619462dfde53d8afb60e Mon Sep 17 00:00:00 2001 From: Fabio Busatto Date: Thu, 3 Aug 2017 09:52:12 +0000 Subject: [PATCH 018/141] Update index.md --- .../index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md index 9622b4b0761..c029cb65956 100644 --- a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md +++ b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md @@ -234,7 +234,7 @@ run: ``` It is very similar to the configuration used for `simple-maven-dep`, but instead of the `deploy` job there is a `run` job. -Probably something that you don't want to use in real projects, but here it is useful to see the application automatically running. +Probably something that you don't want to use in real projects, but here it is useful to see the application executed automatically. And that's it! In the `run` job output log you will find a friendly hello to GitLab! From c90f3009c67078b97a41b716e31c46376ee3e47e Mon Sep 17 00:00:00 2001 From: Fabio Busatto Date: Thu, 3 Aug 2017 10:03:16 +0000 Subject: [PATCH 019/141] Update index.md --- .../index.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md index c029cb65956..9b15159e08c 100644 --- a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md +++ b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md @@ -125,11 +125,12 @@ deploy: - master ``` -It uses the latest Docker image for Maven, which already contains everything you need to perform all the tasks. -Environment variables are set to instruct Maven to use the `homedir` of the repo instead of the user's home. -Caching the .m2/repository folder (where all the Maven files are stored), and the target folder (where our application will be created), will be useful for speeding up the process +GitLab Runner will use the latest [Maven Docker image](https://hub.docker.com/_/maven/), which already contains all the tools and the dependencies you need to manage the project, +in order to run the jobs. +Environment variables are set to instruct Maven to use the `homedir` of the repo instead of the user's home when searching for configuration and dependencies. +Caching the `.m2/repository folder` (where all the Maven files are stored), and the `target` folder (where our application will be created), is useful for speeding up the process by running all Maven phases in a sequential order, therefore, executing `mvn test` will automatically run `mvn compile` if necessary. -Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the repository. +Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the application. Deploy to Artifactory is done as defined by the secret variables we have just set up. The deployment occurs only if we're pushing or merging to `master` branch, so that the development versions are tested but not published. From 9e71d42761f1164bc90de041ed6813248fa1ec7d Mon Sep 17 00:00:00 2001 From: Fabio Busatto Date: Thu, 3 Aug 2017 10:10:14 +0000 Subject: [PATCH 020/141] Update index.md --- .../index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md index 9b15159e08c..5d9c8b17053 100644 --- a/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md +++ b/doc/articles/how_to_use_gitlab_ci_to_deploy_maven_projects_to_artifactory/index.md @@ -3,7 +3,7 @@ > **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** tutorial || > **Level:** intermediary || > **Author:** [Fabio Busatto](https://gitlab.com/bikebilly) || -> **Publication date:** AAAA/MM/DD +> **Publication date:** 2017/08/03 ## Introduction From fd84e9f0346121afbc07be473259ede7e8a43e68 Mon Sep 17 00:00:00 2001 From: Regis Date: Mon, 7 Aug 2017 19:38:15 -0600 Subject: [PATCH 021/141] use border radisu scss var --- app/assets/stylesheets/pages/issuable.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index b78db402c13..7365f96d041 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -8,13 +8,13 @@ .is-confidential { color: $orange-600; background-color: $orange-50; - border-radius: 3px; + border-radius: $border-radius-default; padding: 5px; margin: 0 3px 0 -4px; } .is-not-confidential { - border-radius: 3px; + border-radius: $border-radius-default; padding: 5px; margin: 0 3px 0 -4px; } From 3d08e47239ffd00856507b73f1ce36ffa1e05256 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 7 Aug 2017 23:03:27 -0500 Subject: [PATCH 022/141] fix commit metadata in project:show page --- app/assets/javascripts/dispatcher.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 265e304b957..745c3e892f3 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -343,6 +343,9 @@ import UserFeatureHelper from './helpers/user_feature_helper'; if ($('#tree-slider').length) new TreeView(); if ($('.blob-viewer').length) new BlobViewer(); + $('#tree-slider').waitForImages(function() { + gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); + }); break; case 'projects:edit': setupProjectEdit(); From 3ba782d5de967ff8c765ac6da94c875837d352de Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 9 Aug 2017 08:54:32 +0100 Subject: [PATCH 023/141] Wait for requests to finish before running the ace JS Closes #36191 --- spec/features/projects/user_edits_files_spec.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/spec/features/projects/user_edits_files_spec.rb b/spec/features/projects/user_edits_files_spec.rb index 8ae89c980b9..0c306fd34fb 100644 --- a/spec/features/projects/user_edits_files_spec.rb +++ b/spec/features/projects/user_edits_files_spec.rb @@ -24,6 +24,9 @@ describe 'User edits files' do it 'inserts a content of a file', js: true do click_link('.gitignore') find('.js-edit-blob').click + + wait_for_requests + execute_script("ace.edit('editor').setValue('*.rbca')") expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca') @@ -39,6 +42,9 @@ describe 'User edits files' do it 'commits an edited file', js: true do click_link('.gitignore') find('.js-edit-blob').click + + wait_for_requests + execute_script("ace.edit('editor').setValue('*.rbca')") fill_in(:commit_message, with: 'New commit message', visible: true) click_button('Commit changes') @@ -53,6 +59,9 @@ describe 'User edits files' do it 'commits an edited file to a new branch', js: true do click_link('.gitignore') find('.js-edit-blob').click + + wait_for_requests + execute_script("ace.edit('editor').setValue('*.rbca')") fill_in(:commit_message, with: 'New commit message', visible: true) fill_in(:branch_name, with: 'new_branch_name', visible: true) @@ -69,6 +78,9 @@ describe 'User edits files' do it 'shows the diff of an edited file', js: true do click_link('.gitignore') find('.js-edit-blob').click + + wait_for_requests + execute_script("ace.edit('editor').setValue('*.rbca')") click_link('Preview changes') @@ -93,6 +105,8 @@ describe 'User edits files' do expect(page).to have_content(fork_message) + wait_for_requests + execute_script("ace.edit('editor').setValue('*.rbca')") expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca') @@ -106,6 +120,9 @@ describe 'User edits files' do expect(page).to have_button('Cancel') click_link('Fork') + + wait_for_requests + execute_script("ace.edit('editor').setValue('*.rbca')") fill_in(:commit_message, with: 'New commit message', visible: true) click_button('Commit changes') From 029fb98b02f00e55243eaa781dc2849e94f16ae5 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 9 Aug 2017 17:55:53 +0800 Subject: [PATCH 024/141] Detect if we didn't create the ref sooner --- app/models/repository.rb | 5 ++++- spec/models/repository_spec.rb | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 049bebdbe42..02f7f70ecb0 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -999,7 +999,7 @@ class Repository yield(commit(branch_name_or_sha)) ensure - rugged.references.delete(tmp_ref) if tmp_ref + rugged.references.delete(tmp_ref) if tmp_ref && ref_exists?(tmp_ref) end def add_remote(name, url) @@ -1022,6 +1022,9 @@ class Repository def fetch_ref(source_path, source_ref, target_ref) args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) run_git(args) + + # Make sure ref was created, and raise Rugged::ReferenceError when not + raise Rugged::ReferenceError unless ref_exists?(target_ref) end def create_ref(ref, ref_path) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index cfa77648338..1341f7c294c 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -961,6 +961,27 @@ describe Repository, models: true do end end + context 'when temporary ref failed to be created from other project' do + let(:target_project) { create(:project, :empty_repo) } + + before do + expect(target_project.repository).to receive(:run_git) + end + + it 'raises Rugged::ReferenceError' do + raise_reference_error = raise_error(Rugged::ReferenceError) do |err| + expect(err.cause).to be_nil + end + + expect do + GitOperationService.new(user, target_project.repository) + .with_branch('feature', + start_project: project, + &:itself) + end.to raise_reference_error + end + end + context 'when the update adds more than one commit' do let(:old_rev) { '33f3729a45c02fc67d00adb1b8bca394b0e761d9' } From a85eed6446fa0b4b899a71cb9a3cb5e011a41c3a Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 9 Aug 2017 19:30:07 +0800 Subject: [PATCH 025/141] Fake out Repository#fetch_ref for merge request if the project didn't have a repository setup. We don't try to stub it if the repository was already there. --- spec/factories/merge_requests.rb | 10 ++++++++++ spec/features/task_lists_spec.rb | 5 ++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 1bc530d06db..19bf7582747 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -68,6 +68,16 @@ FactoryGirl.define do merge_user author end + after(:build) do |merge_request| + target_project = merge_request.target_project + + # Fake `fetch_ref` if we don't have repository + # We have too many existing tests replying on this behaviour + unless target_project.repository_exists? + allow(target_project.repository).to receive(:fetch_ref) + end + end + factory :merged_merge_request, traits: [:merged] factory :closed_merge_request, traits: [:closed] factory :reopened_merge_request, traits: [:opened] diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index c14826df55a..35f025830f1 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -52,8 +52,8 @@ feature 'Task Lists' do before do Warden.test_mode! - project.team << [user, :master] - project.team << [user2, :guest] + project.add_master(user) + project.add_guest(user2) login_as(user) end @@ -246,7 +246,6 @@ feature 'Task Lists' do end describe 'multiple tasks' do - let(:project) { create(:project, :repository) } let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) } it 'renders for description' do From 412db1874fbf2847ad9d84e9d2344d4c4d4b9fef Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 9 Aug 2017 21:41:45 +0800 Subject: [PATCH 026/141] Fix some tests and report the error message --- app/models/repository.rb | 4 ++-- .../projects/issues_controller_spec.rb | 2 +- spec/factories/deployments.rb | 4 ++++ spec/factories/merge_requests.rb | 3 ++- spec/requests/api/merge_requests_spec.rb | 20 +++++++++++++++---- .../ci/create_pipeline_service_spec.rb | 2 +- .../issues/resolve_discussions_spec.rb | 2 +- ...issuables_list_metadata_shared_examples.rb | 4 ++-- 8 files changed, 29 insertions(+), 12 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 02f7f70ecb0..c032baa5d2c 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1021,10 +1021,10 @@ class Repository def fetch_ref(source_path, source_ref, target_ref) args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) - run_git(args) + message, status = run_git(args) # Make sure ref was created, and raise Rugged::ReferenceError when not - raise Rugged::ReferenceError unless ref_exists?(target_ref) + raise Rugged::ReferenceError, message unless ref_exists?(target_ref) end def create_ref(ref, ref_path) diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index bdee3894a13..7cda5c8e40e 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -1,7 +1,7 @@ require('spec_helper') describe Projects::IssuesController do - let(:project) { create(:project_empty_repo) } + let(:project) { create(:project) } let(:user) { create(:user) } let(:issue) { create(:issue, project: project) } diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb index 29ad1af9fd9..e5abfd67d60 100644 --- a/spec/factories/deployments.rb +++ b/spec/factories/deployments.rb @@ -10,6 +10,10 @@ FactoryGirl.define do after(:build) do |deployment, evaluator| deployment.project ||= deployment.environment.project + + unless deployment.project.repository_exists? + allow(deployment.project.repository).to receive(:fetch_ref) + end end end end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 19bf7582747..04493981945 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -70,10 +70,11 @@ FactoryGirl.define do after(:build) do |merge_request| target_project = merge_request.target_project + source_project = merge_request.source_project # Fake `fetch_ref` if we don't have repository # We have too many existing tests replying on this behaviour - unless target_project.repository_exists? + unless [target_project, source_project].all?(&:repository_exists?) allow(target_project.repository).to receive(:fetch_ref) end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 9eda6836ded..aa52d240e39 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -580,15 +580,27 @@ describe API::MergeRequests do let!(:fork_project) { create(:project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) } let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) } - before :each do |each| - fork_project.team << [user2, :reporter] + before do + fork_project.add_reporter(user2) + + Project.all.each(&method(:stub_project_repository_fetch_ref)) + end + + def stub_project_repository_fetch_ref(project) + allow(Project).to receive(:find_by).with(id: project.id.to_s) + .and_return(project) + + allow(Project).to receive(:find).with(project.id) + .and_return(project) + + allow(project.repository).to receive(:fetch_ref) end it "returns merge_request" do post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master", author: user2, target_project_id: project.id, description: 'Test description for Test merge_request' - expect(response).to have_http_status(201) + expect(response).to have_gitlab_http_status(201) expect(json_response['title']).to eq('Test merge_request') expect(json_response['description']).to eq('Test description for Test merge_request') end @@ -599,7 +611,7 @@ describe API::MergeRequests do expect(fork_project.forked_from_project).to eq(project) post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id - expect(response).to have_http_status(201) + expect(response).to have_gitlab_http_status(201) expect(json_response['title']).to eq('Test merge_request') end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 730df4e0336..53d4fcfed18 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -66,7 +66,7 @@ describe Ci::CreatePipelineService do context 'when there is no pipeline for source branch' do it "does not update merge request head pipeline" do - merge_request = create(:merge_request, source_branch: 'other_branch', target_branch: "branch_1", source_project: project) + merge_request = create(:merge_request, source_branch: 'feature', target_branch: "branch_1", source_project: project) head_pipeline = pipeline diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb index fac66791ffb..67a86c50fc1 100644 --- a/spec/services/issues/resolve_discussions_spec.rb +++ b/spec/services/issues/resolve_discussions_spec.rb @@ -20,7 +20,7 @@ describe Issues::ResolveDiscussions do describe "for resolving discussions" do let(:discussion) { create(:diff_note_on_merge_request, project: project, note: "Almost done").to_discussion } let(:merge_request) { discussion.noteable } - let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: "other") } + let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: "fix") } describe "#merge_request_for_resolving_discussion" do let(:service) { DummyService.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) } diff --git a/spec/support/issuables_list_metadata_shared_examples.rb b/spec/support/issuables_list_metadata_shared_examples.rb index a60d3b0d22d..75982432ab4 100644 --- a/spec/support/issuables_list_metadata_shared_examples.rb +++ b/spec/support/issuables_list_metadata_shared_examples.rb @@ -2,12 +2,12 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil| before do @issuable_ids = [] - 2.times do |n| + %w[fix improve/awesome].each do |source_branch| issuable = if issuable_type == :issue create(issuable_type, project: project) else - create(issuable_type, source_project: project, source_branch: "#{n}-feature") + create(issuable_type, source_project: project, source_branch: source_branch) end @issuable_ids << issuable.id From 8858ddaf83e57adc6c003e03e72929f6068a6bfa Mon Sep 17 00:00:00 2001 From: Simon Knox Date: Tue, 18 Jul 2017 15:09:04 +1000 Subject: [PATCH 027/141] take edit note button out of dropdown --- app/assets/stylesheets/pages/notes.scss | 56 ++++++++++--------- app/views/projects/notes/_actions.html.haml | 38 ++++++++----- .../notes/_more_actions_dropdown.html.haml | 9 +-- app/views/shared/icons/_ellipsis_v.svg | 1 + app/views/snippets/notes/_actions.html.haml | 17 ++++-- ...n-always-available-outside-of-dropdown.yml | 4 ++ features/steps/project/merge_requests.rb | 3 - features/steps/shared/note.rb | 7 +-- spec/features/issues/note_polling_spec.rb | 2 - .../merge_requests/diff_notes_avatars_spec.rb | 2 +- .../merge_requests/user_posts_notes_spec.rb | 3 - .../notes_on_personal_snippets_spec.rb | 6 +- .../reportable_note_shared_examples.rb | 7 ++- .../_more_actions_dropdown.html.haml_spec.rb | 6 +- 14 files changed, 83 insertions(+), 78 deletions(-) create mode 100644 app/views/shared/icons/_ellipsis_v.svg create mode 100644 changelogs/unreleased/34527-make-edit-comment-button-always-available-outside-of-dropdown.yml diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 2bb867052f6..0a194f3707f 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -453,7 +453,10 @@ ul.notes { } .note-actions { + align-self: flex-start; flex-shrink: 0; + display: inline-flex; + align-items: center; // For PhantomJS that does not support flex float: right; margin-left: 10px; @@ -463,18 +466,12 @@ ul.notes { float: none; margin-left: 0; } - - .note-action-button { - margin-left: 8px; - } - - .more-actions-toggle { - margin-left: 2px; - } } .more-actions { - display: inline-block; + float: right; // phantomjs fallback + display: flex; + align-items: flex-end; .tooltip { white-space: nowrap; @@ -482,16 +479,10 @@ ul.notes { } .more-actions-toggle { - padding: 0; - &:hover .icon, &:focus .icon { color: $blue-600; } - - .icon { - padding: 0 6px; - } } .more-actions-dropdown { @@ -519,28 +510,42 @@ ul.notes { @include notes-media('max', $screen-md-max) { float: none; margin-left: 0; + } +} - .note-action-button { - margin-left: 0; - } +.note-actions-item { + margin-left: 15px; + display: flex; + align-items: center; + + &.more-actions { + // compensate for narrow icon + margin-left: 10px; } } .note-action-button { - display: inline; - line-height: 20px; + line-height: 1; + padding: 0; + min-width: 16px; + color: $gray-darkest; .fa { - color: $gray-darkest; position: relative; - font-size: 17px; + font-size: 16px; } + + svg { height: 16px; width: 16px; - fill: $gray-darkest; + top: 0; vertical-align: text-top; + + path { + fill: currentColor; + } } .award-control-icon-positive, @@ -613,10 +618,7 @@ ul.notes { .note-role { position: relative; - top: -2px; - display: inline-block; - padding-left: 7px; - padding-right: 7px; + padding: 0 7px; color: $notes-role-color; font-size: 12px; line-height: 20px; diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml index 9c42be4e0ff..cb737d129f0 100644 --- a/app/views/projects/notes/_actions.html.haml +++ b/app/views/projects/notes/_actions.html.haml @@ -17,24 +17,32 @@ "inline-template" => true, "ref" => "note_#{note.id}" } - %button.note-action-button.line-resolve-btn{ type: "button", - class: ("is-disabled" unless can_resolve), - ":class" => "{ 'is-active': isResolved }", - ":aria-label" => "buttonText", - "@click" => "resolve", - ":title" => "buttonText", - ":ref" => "'button'" } + .note-actions-item + %button.note-action-button.line-resolve-btn{ type: "button", + class: ("is-disabled" unless can_resolve), + ":class" => "{ 'is-active': isResolved }", + ":aria-label" => "buttonText", + "@click" => "resolve", + ":title" => "buttonText", + ":ref" => "'button'" } - = icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading') - %div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg' + = icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading') + %div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg' - if current_user - if note.emoji_awardable? - user_authored = note.user_authored?(current_user) - = link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do - = icon('spinner spin') - %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') - %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') - %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') + .note-actions-item + = button_tag title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip btn btn-transparent", data: { position: 'right', container: 'body' } do + = icon('spinner spin') + %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') + %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') + %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') - = render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable + - if note_editable + .note-actions-item + = button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn btn-transparent', data: { container: 'body' } do + %span.link-highlight + = custom_icon('icon_pencil') + + = render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml index 75a4687e1e3..5930209a682 100644 --- a/app/views/projects/notes/_more_actions_dropdown.html.haml +++ b/app/views/projects/notes/_more_actions_dropdown.html.haml @@ -1,14 +1,11 @@ - is_current_user = current_user == note.author - if note_editable || !is_current_user - .dropdown.more-actions + .dropdown.more-actions.note-actions-item = button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn btn-transparent', data: { toggle: 'dropdown', container: 'body' } do - = icon('ellipsis-v', class: 'icon') + %span.icon + = custom_icon('ellipsis_v') %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left - - if note_editable - %li - = button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent' - %li.divider - unless is_current_user %li = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do diff --git a/app/views/shared/icons/_ellipsis_v.svg b/app/views/shared/icons/_ellipsis_v.svg new file mode 100644 index 00000000000..9117a9bb9ec --- /dev/null +++ b/app/views/shared/icons/_ellipsis_v.svg @@ -0,0 +1 @@ + diff --git a/app/views/snippets/notes/_actions.html.haml b/app/views/snippets/notes/_actions.html.haml index 098a88c48c5..3a50324770d 100644 --- a/app/views/snippets/notes/_actions.html.haml +++ b/app/views/snippets/notes/_actions.html.haml @@ -1,10 +1,17 @@ - if current_user - if note.emoji_awardable? - user_authored = note.user_authored?(current_user) - = link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do - = icon('spinner spin') - %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') - %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') - %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') + .note-actions-item + = link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do + = icon('spinner spin') + %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') + %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') + %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') + + - if note_editable + .note-actions-item + = button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn btn-transparent', data: { container: 'body' } do + %span.link-highlight + = custom_icon('icon_pencil') = render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable diff --git a/changelogs/unreleased/34527-make-edit-comment-button-always-available-outside-of-dropdown.yml b/changelogs/unreleased/34527-make-edit-comment-button-always-available-outside-of-dropdown.yml new file mode 100644 index 00000000000..08171f6bcec --- /dev/null +++ b/changelogs/unreleased/34527-make-edit-comment-button-always-available-outside-of-dropdown.yml @@ -0,0 +1,4 @@ +--- +title: move edit comment button outside of dropdown +merge_request: +author: diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 810cd75591b..7254fbc2e4e 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -299,9 +299,6 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I change the comment "Line is wrong" to "Typo, please fix" on diff' do page.within('.diff-file:nth-of-type(5) .note') do - find('.more-actions').click - find('.more-actions .dropdown-menu li', match: :first) - find('.js-note-edit').click page.within('.current-note-edit-form', visible: true) do diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index 80187b83fee..492da38355c 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -11,8 +11,8 @@ module SharedNote note = find('.note') note.hover - note.find('.more-actions').click - note.find('.more-actions .dropdown-menu li', match: :first) + find('.more-actions').click + find('.more-actions .dropdown-menu li', match: :first) find(".js-note-delete").click end @@ -147,9 +147,6 @@ module SharedNote note = find('.note') note.hover - note.find('.more-actions').click - note.find('.more-actions .dropdown-menu li', match: :first) - note.find('.js-note-edit').click end diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb index 9f08ecc214b..62dbc3efb01 100644 --- a/spec/features/issues/note_polling_spec.rb +++ b/spec/features/issues/note_polling_spec.rb @@ -133,8 +133,6 @@ feature 'Issue notes polling', :js do def click_edit_action(note) note_element = find("#note_#{note.id}") - open_more_actions_dropdown(note) - note_element.find('.js-note-edit').click end end diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb index 2d9419d6124..c4f02311f13 100644 --- a/spec/features/merge_requests/diff_notes_avatars_spec.rb +++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb @@ -157,7 +157,7 @@ feature 'Diff note avatars', js: true do end page.within find("[id='#{position.line_code(project.repository)}']") do - find('.diff-notes-collapse').click + find('.diff-notes-collapse').trigger('click') expect(page).to have_selector('img.js-diff-comment-avatar', count: 3) expect(find('.diff-comments-more-count')).to have_content '+1' diff --git a/spec/features/merge_requests/user_posts_notes_spec.rb b/spec/features/merge_requests/user_posts_notes_spec.rb index 74d21822a59..d7cda73ab40 100644 --- a/spec/features/merge_requests/user_posts_notes_spec.rb +++ b/spec/features/merge_requests/user_posts_notes_spec.rb @@ -75,7 +75,6 @@ describe 'Merge requests > User posts notes', :js do describe 'editing the note' do before do find('.note').hover - open_more_actions_dropdown(note) find('.js-note-edit').click end @@ -104,7 +103,6 @@ describe 'Merge requests > User posts notes', :js do wait_for_requests find('.note').hover - open_more_actions_dropdown(note) find('.js-note-edit').click @@ -132,7 +130,6 @@ describe 'Merge requests > User posts notes', :js do describe 'deleting an attachment' do before do find('.note').hover - open_more_actions_dropdown(note) find('.js-note-edit').click end diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb index f1d0905738b..c0c293dee78 100644 --- a/spec/features/snippets/notes_on_personal_snippets_spec.rb +++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb @@ -91,11 +91,7 @@ describe 'Comments on personal snippets', :js do context 'when editing a note' do it 'changes the text' do - open_more_actions_dropdown(snippet_notes[0]) - - page.within("#notes-list li#note_#{snippet_notes[0].id}") do - click_on 'Edit comment' - end + find('.js-note-edit').click page.within('.current-note-edit-form') do fill_in 'note[note]', with: 'new content' diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb index 27e079c01dd..cb483ae9a5a 100644 --- a/spec/support/features/reportable_note_shared_examples.rb +++ b/spec/support/features/reportable_note_shared_examples.rb @@ -7,15 +7,18 @@ shared_examples 'reportable note' do let(:more_actions_selector) { '.more-actions.dropdown' } let(:abuse_report_path) { new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) } + it 'has an edit button' do + expect(comment).to have_selector('.js-note-edit') + end + it 'has a `More actions` dropdown' do expect(comment).to have_selector(more_actions_selector) end - it 'dropdown has Edit, Report and Delete links' do + it 'dropdown has Report and Delete links' do dropdown = comment.find(more_actions_selector) open_dropdown(dropdown) - expect(dropdown).to have_button('Edit comment') expect(dropdown).to have_link('Report as abuse', href: abuse_report_path) expect(dropdown).to have_link('Delete comment', href: note_url(note, project)) end diff --git a/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb index aea20d826d0..9c0be249a50 100644 --- a/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb +++ b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb @@ -24,18 +24,16 @@ describe 'projects/notes/_more_actions_dropdown' do expect(rendered).not_to have_selector('.dropdown.more-actions') end - it 'shows Report as abuse, Edit and Delete buttons if editable and not current users comment' do + it 'shows Report as abuse and Delete buttons if editable and not current users comment' do render 'projects/notes/more_actions_dropdown', current_user: not_author_user, note_editable: true, note: note expect(rendered).to have_link('Report as abuse') - expect(rendered).to have_button('Edit comment') expect(rendered).to have_link('Delete comment') end - it 'shows Edit and Delete buttons if editable and current users comment' do + it 'shows Delete button if editable and current users comment' do render 'projects/notes/more_actions_dropdown', current_user: author_user, note_editable: true, note: note - expect(rendered).to have_button('Edit comment') expect(rendered).to have_link('Delete comment') end end From 023d6749c24741dd1ac065b7c9d4413acb9aa320 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 10 Aug 2017 18:08:58 +0800 Subject: [PATCH 028/141] Just detect exit status rather than check ref Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13416#note_37193731 So we just try a cheaper way to detect it if it works or not --- app/models/repository.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index c032baa5d2c..f8139ff595e 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1024,7 +1024,7 @@ class Repository message, status = run_git(args) # Make sure ref was created, and raise Rugged::ReferenceError when not - raise Rugged::ReferenceError, message unless ref_exists?(target_ref) + raise Rugged::ReferenceError, message if status != 0 end def create_ref(ref, ref_path) From 7b10885046137633fa615ed5d6ba29d4d0d09cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kim=20=22BKC=22=20Carlb=C3=A4cker?= Date: Fri, 4 Aug 2017 06:16:02 +0200 Subject: [PATCH 029/141] Migrate Git::Repository.ls_files to Gitaly --- lib/gitlab/git/repository.rb | 58 ++++++++++++++-------- lib/gitlab/gitaly_client/commit_service.rb | 12 +++++ spec/lib/gitlab/git/repository_spec.rb | 2 +- 3 files changed, 49 insertions(+), 23 deletions(-) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 371f8797ff2..89041997a3a 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -603,29 +603,13 @@ module Gitlab # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/327 def ls_files(ref) - actual_ref = ref || root_ref - - begin - sha_from_ref(actual_ref) - rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError - # Return an empty array if the ref wasn't found - return [] + gitaly_migrate(:ls_files) do |is_enabled| + if is_enabled + gitaly_ls_files(ref) + else + git_ls_files(ref) + end end - - cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-tree) - cmd += %w(-r) - cmd += %w(--full-tree) - cmd += %w(--full-name) - cmd += %W(-- #{actual_ref}) - - raw_output = IO.popen(cmd, &:read).split("\n").map do |f| - stuff, path = f.split("\t") - _mode, type, _sha = stuff.split(" ") - path if type == "blob" - # Contain only blob type - end - - raw_output.compact end # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/328 @@ -973,6 +957,36 @@ module Gitlab raw_output.to_i end + + def gitaly_ls_files(ref) + gitaly_commit_client.ls_files(ref) + end + + def git_ls_files(ref) + actual_ref = ref || root_ref + + begin + sha_from_ref(actual_ref) + rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError + # Return an empty array if the ref wasn't found + return [] + end + + cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-tree) + cmd += %w(-r) + cmd += %w(--full-tree) + cmd += %w(--full-name) + cmd += %W(-- #{actual_ref}) + + raw_output = IO.popen(cmd, &:read).split("\n").map do |f| + stuff, path = f.split("\t") + _mode, type, _sha = stuff.split(" ") + path if type == "blob" + # Contain only blob type + end + + raw_output.compact + end end end end diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 692d7e02eef..93268d9f33c 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -10,6 +10,18 @@ module Gitlab @repository = repository end + def ls_files(revision) + request = Gitaly::ListFilesRequest.new( + repository: @gitaly_repo, + revision: GitalyClient.encode(revision) + ) + + response = GitalyClient.call(@repository.storage, :commit_service, :list_files, request) + response.flat_map do |msg| + msg.paths.map { |d| d.dup.force_encoding(Encoding::UTF_8) } + end + end + def is_ancestor(ancestor_id, child_id) request = Gitaly::CommitIsAncestorRequest.new( repository: @gitaly_repo, diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 858616117d5..b9c5373ee67 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1002,7 +1002,7 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(master_file_paths).to include("files/html/500.html") end - it "dose not read submodule directory and empty directory of master branch" do + it "does not read submodule directory and empty directory of master branch" do expect(master_file_paths).not_to include("six") end From 64e13d1958eac06185866e050ef83678ed2c4fd9 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 10 Aug 2017 22:19:55 +0800 Subject: [PATCH 030/141] Avoid ambiguity, which happened in a single test run --- app/controllers/projects/issues_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index e2ccabb22db..917ee3955e2 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -212,7 +212,7 @@ class Projects::IssuesController < Projects::ApplicationController end def create_merge_request - result = MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute + result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute if result[:status] == :success render json: MergeRequestCreateSerializer.new.represent(result[:merge_request]) From c772464b68ad87860206e1ad341cca69e301a483 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 10 Aug 2017 22:20:53 +0800 Subject: [PATCH 031/141] Introduce MergeRequest#write_ref and Repository#write_ref so that we don't have to fetch it for non-forks --- app/models/merge_request.rb | 20 ++++++++++++----- app/models/project.rb | 4 +--- app/models/repository.rb | 6 +++++- spec/factories/merge_requests.rb | 4 ++-- spec/lib/gitlab/git/repository_spec.rb | 5 +++-- spec/requests/api/v3/merge_requests_spec.rb | 24 +++++++++++---------- 6 files changed, 39 insertions(+), 24 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index f90194041b1..b6c9b8f21c7 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -792,11 +792,7 @@ class MergeRequest < ActiveRecord::Base end def fetch_ref - target_project.repository.fetch_ref( - source_project.repository.path_to_repo, - "refs/heads/#{source_branch}", - ref_path - ) + write_ref update_column(:ref_fetched, true) end @@ -939,4 +935,18 @@ class MergeRequest < ActiveRecord::Base true end + + private + + def write_ref + if for_fork? + target_project.repository.fetch_ref( + source_project.repository.path_to_repo, + "refs/heads/#{source_branch}", + ref_path + ) + else + source_project.repository.write_ref(ref_path, source_branch_sha) + end + end end diff --git a/app/models/project.rb b/app/models/project.rb index 7010664e1c8..7cdd00bc17b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1048,9 +1048,7 @@ class Project < ActiveRecord::Base def change_head(branch) if repository.branch_exists?(branch) repository.before_change_head - repository.rugged.references.create('HEAD', - "refs/heads/#{branch}", - force: true) + repository.write_ref('HEAD', "refs/heads/#{branch}") repository.copy_gitattributes(branch) repository.after_change_head reload_default_branch diff --git a/app/models/repository.rb b/app/models/repository.rb index f8139ff595e..04403cdd035 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -224,7 +224,7 @@ class Repository # This will still fail if the file is corrupted (e.g. 0 bytes) begin - rugged.references.create(keep_around_ref_name(sha), sha, force: true) + write_ref(keep_around_ref_name(sha), sha) rescue Rugged::ReferenceError => ex Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}" rescue Rugged::OSError => ex @@ -237,6 +237,10 @@ class Repository ref_exists?(keep_around_ref_name(sha)) end + def write_ref(ref_path, sha) + rugged.references.create(ref_path, sha, force: true) + end + def diverging_commit_counts(branch) root_ref_hash = raw_repository.rev_parse_target(root_ref).oid cache.fetch(:"diverging_commit_counts_#{branch.name}") do diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 04493981945..cbec716d6ea 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -72,10 +72,10 @@ FactoryGirl.define do target_project = merge_request.target_project source_project = merge_request.source_project - # Fake `fetch_ref` if we don't have repository + # Fake `write_ref` if we don't have repository # We have too many existing tests replying on this behaviour unless [target_project, source_project].all?(&:repository_exists?) - allow(target_project.repository).to receive(:fetch_ref) + allow(merge_request).to receive(:write_ref) end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 858616117d5..19f576d216c 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1196,8 +1196,9 @@ describe Gitlab::Git::Repository, seed_helper: true do def create_remote_branch(repository, remote_name, branch_name, source_branch_name) source_branch = repository.branches.find { |branch| branch.name == source_branch_name } - rugged = repository.rugged - rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha) + repository.write_ref( + "refs/remotes/#{remote_name}/#{branch_name}", + source_branch.dereferenced_target.sha) end # Build the options hash that's passed to Rugged::Commit#create diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb index ef7516fc28f..68052ef1035 100644 --- a/spec/requests/api/v3/merge_requests_spec.rb +++ b/spec/requests/api/v3/merge_requests_spec.rb @@ -315,15 +315,17 @@ describe API::MergeRequests do let!(:fork_project) { create(:project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) } let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) } - before :each do |each| - fork_project.team << [user2, :reporter] + before do + fork_project.add_reporter(user2) + + allow_any_instance_of(MergeRequest).to receive(:write_ref) end it "returns merge_request" do post v3_api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master", author: user2, target_project_id: project.id, description: 'Test description for Test merge_request' - expect(response).to have_http_status(201) + expect(response).to have_gitlab_http_status(201) expect(json_response['title']).to eq('Test merge_request') expect(json_response['description']).to eq('Test description for Test merge_request') end @@ -334,7 +336,7 @@ describe API::MergeRequests do expect(fork_project.forked_from_project).to eq(project) post v3_api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id - expect(response).to have_http_status(201) + expect(response).to have_gitlab_http_status(201) expect(json_response['title']).to eq('Test merge_request') end @@ -348,25 +350,25 @@ describe API::MergeRequests do author: user2, target_project_id: project.id - expect(response).to have_http_status(422) + expect(response).to have_gitlab_http_status(422) end it "returns 400 when source_branch is missing" do post v3_api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) end it "returns 400 when target_branch is missing" do post v3_api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) end it "returns 400 when title is missing" do post v3_api("/projects/#{fork_project.id}/merge_requests", user2), target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) end context 'when target_branch is specified' do @@ -377,7 +379,7 @@ describe API::MergeRequests do source_branch: 'markdown', author: user, target_project_id: fork_project.id - expect(response).to have_http_status(422) + expect(response).to have_gitlab_http_status(422) end it 'returns 422 if targeting a different fork' do @@ -387,14 +389,14 @@ describe API::MergeRequests do source_branch: 'markdown', author: user2, target_project_id: unrelated_project.id - expect(response).to have_http_status(422) + expect(response).to have_gitlab_http_status(422) end end it "returns 201 when target_branch is specified and for the same project" do post v3_api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id - expect(response).to have_http_status(201) + expect(response).to have_gitlab_http_status(201) end end end From 8730cd86570cefa6e964373dff0c2e22013d38fb Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 10 Aug 2017 22:21:22 +0800 Subject: [PATCH 032/141] Try to show exception information in the test --- lib/api/helpers.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 99b8b62691f..3582ed81b0f 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -257,7 +257,15 @@ module API message << " " << trace.join("\n ") API.logger.add Logger::FATAL, message - rack_response({ 'message' => '500 Internal Server Error' }.to_json, 500) + + response_message = + if Rails.env.test? + message + else + '500 Internal Server Error' + end + + rack_response({ 'message' => response_message }.to_json, 500) end # project helpers From 41a5adca7514ced023a2708ab26666db560b58a3 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 10 Aug 2017 23:53:55 +0800 Subject: [PATCH 033/141] Don't try to create diffs if one of the branch is missing Also fix a few tests --- app/models/merge_request.rb | 3 ++- .../projects/issues_controller_spec.rb | 2 +- .../merge_requests/filter_by_labels_spec.rb | 10 +++++----- .../user_lists_merge_requests_spec.rb | 16 +++++++--------- spec/finders/environments_finder_spec.rb | 2 +- spec/lib/gitlab/git/repository_spec.rb | 5 ++--- 6 files changed, 18 insertions(+), 20 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index b6c9b8f21c7..daee7c93995 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -443,7 +443,8 @@ class MergeRequest < ActiveRecord::Base end def reload_diff_if_branch_changed - if source_branch_changed? || target_branch_changed? + if (source_branch_changed? || target_branch_changed?) && + (source_branch_head && target_branch_head) reload_diff end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 7cda5c8e40e..fe407ffff4b 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -841,7 +841,7 @@ describe Projects::IssuesController do describe 'POST #toggle_award_emoji' do before do sign_in(user) - project.team << [user, :developer] + project.add_developer(user) end it "toggles the award emoji" do diff --git a/spec/features/merge_requests/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb index 1d52a4659ad..9912e8165e6 100644 --- a/spec/features/merge_requests/filter_by_labels_spec.rb +++ b/spec/features/merge_requests/filter_by_labels_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Merge Request filtering by Labels', js: true do +feature 'Merge Request filtering by Labels', :js do include FilteredSearchHelpers include MergeRequestHelpers @@ -12,9 +12,9 @@ feature 'Merge Request filtering by Labels', js: true do let!(:feature) { create(:label, project: project, title: 'feature') } let!(:enhancement) { create(:label, project: project, title: 'enhancement') } - let!(:mr1) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "bugfix1") } - let!(:mr2) { create(:merge_request, title: "Bugfix2", source_project: project, target_project: project, source_branch: "bugfix2") } - let!(:mr3) { create(:merge_request, title: "Feature1", source_project: project, target_project: project, source_branch: "feature1") } + let!(:mr1) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "fix") } + let!(:mr2) { create(:merge_request, title: "Bugfix2", source_project: project, target_project: project, source_branch: "wip") } + let!(:mr3) { create(:merge_request, title: "Feature1", source_project: project, target_project: project, source_branch: "improve/awesome") } before do mr1.labels << bug @@ -25,7 +25,7 @@ feature 'Merge Request filtering by Labels', js: true do mr3.title = "Feature1" mr3.labels << feature - project.team << [user, :master] + project.add_master(user) sign_in(user) visit project_merge_requests_path(project) diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb index d62b035b40b..20008b4e7f9 100644 --- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb @@ -24,12 +24,10 @@ describe 'Projects > Merge requests > User lists merge requests' do milestone: create(:milestone, due_date: '2013-12-12'), created_at: 2.minutes.ago, updated_at: 2.minutes.ago) - # lfs in itself is not a great choice for the title if one wants to match the whole body content later on - # just think about the scenario when faker generates 'Chester Runolfsson' as the user's name create(:merge_request, - title: 'merge_lfs', + title: 'merge-test', source_project: project, - source_branch: 'merge_lfs', + source_branch: 'merge-test', created_at: 3.minutes.ago, updated_at: 10.seconds.ago) end @@ -38,7 +36,7 @@ describe 'Projects > Merge requests > User lists merge requests' do visit_merge_requests(project, assignee_id: IssuableFinder::NONE) expect(current_path).to eq(project_merge_requests_path(project)) - expect(page).to have_content 'merge_lfs' + expect(page).to have_content 'merge-test' expect(page).not_to have_content 'fix' expect(page).not_to have_content 'markdown' expect(count_merge_requests).to eq(1) @@ -47,7 +45,7 @@ describe 'Projects > Merge requests > User lists merge requests' do it 'filters on a specific assignee' do visit_merge_requests(project, assignee_id: user.id) - expect(page).not_to have_content 'merge_lfs' + expect(page).not_to have_content 'merge-test' expect(page).to have_content 'fix' expect(page).to have_content 'markdown' expect(count_merge_requests).to eq(2) @@ -57,14 +55,14 @@ describe 'Projects > Merge requests > User lists merge requests' do visit_merge_requests(project, sort: sort_value_recently_created) expect(first_merge_request).to include('fix') - expect(last_merge_request).to include('merge_lfs') + expect(last_merge_request).to include('merge-test') expect(count_merge_requests).to eq(3) end it 'sorts by oldest' do visit_merge_requests(project, sort: sort_value_oldest_created) - expect(first_merge_request).to include('merge_lfs') + expect(first_merge_request).to include('merge-test') expect(last_merge_request).to include('fix') expect(count_merge_requests).to eq(3) end @@ -72,7 +70,7 @@ describe 'Projects > Merge requests > User lists merge requests' do it 'sorts by last updated' do visit_merge_requests(project, sort: sort_value_recently_updated) - expect(first_merge_request).to include('merge_lfs') + expect(first_merge_request).to include('merge-test') expect(count_merge_requests).to eq(3) end diff --git a/spec/finders/environments_finder_spec.rb b/spec/finders/environments_finder_spec.rb index 0c063f6d5ee..3a8a1e7de74 100644 --- a/spec/finders/environments_finder_spec.rb +++ b/spec/finders/environments_finder_spec.rb @@ -12,7 +12,7 @@ describe EnvironmentsFinder do context 'tagged deployment' do before do - create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id) + create(:deployment, environment: environment, ref: 'v1.1.0', tag: true, sha: project.commit.id) end it 'returns environment when with_tags is set' do diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 19f576d216c..858616117d5 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1196,9 +1196,8 @@ describe Gitlab::Git::Repository, seed_helper: true do def create_remote_branch(repository, remote_name, branch_name, source_branch_name) source_branch = repository.branches.find { |branch| branch.name == source_branch_name } - repository.write_ref( - "refs/remotes/#{remote_name}/#{branch_name}", - source_branch.dereferenced_target.sha) + rugged = repository.rugged + rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha) end # Build the options hash that's passed to Rugged::Commit#create From d59aed94e7ec441f44301a55e0529a9c34a01fd2 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 10 Aug 2017 13:05:04 -0500 Subject: [PATCH 034/141] Move syntax highlighting into a method Fix https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12198#note_37142936 --- .../javascripts/repo/components/repo_preview.vue | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue index d8de022335b..0caa3a4551a 100644 --- a/app/assets/javascripts/repo/components/repo_preview.vue +++ b/app/assets/javascripts/repo/components/repo_preview.vue @@ -4,7 +4,7 @@ import Store from '../stores/repo_store'; export default { data: () => Store, mounted() { - $(this.$el).find('.file-content').syntaxHighlight(); + this.highlightFile(); }, computed: { html() { @@ -12,10 +12,16 @@ export default { }, }, + methods: { + highlightFile() { + $(this.$el).find('.file-content').syntaxHighlight(); + }, + }, + watch: { html() { this.$nextTick(() => { - $(this.$el).find('.file-content').syntaxHighlight(); + this.highlightFile(); }); }, }, From 6cd9888f6fc8bb1e0b6ff11ace8aacb19aedb268 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Wed, 9 Aug 2017 16:43:10 +0200 Subject: [PATCH 035/141] store gpg return directory locally --- lib/gitlab/gpg.rb | 14 +++++++++----- spec/lib/gitlab/gpg_spec.rb | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb index e1d1724295a..653c56d925b 100644 --- a/lib/gitlab/gpg.rb +++ b/lib/gitlab/gpg.rb @@ -44,19 +44,23 @@ module Gitlab def using_tmp_keychain Dir.mktmpdir do |dir| - @original_dirs ||= [GPGME::Engine.dirinfo('homedir')] - @original_dirs.push(dir) + previous_dir = current_home_dir GPGME::Engine.home_dir = dir return_value = yield - @original_dirs.pop - - GPGME::Engine.home_dir = @original_dirs[-1] + GPGME::Engine.home_dir = previous_dir return_value end end + + # 1. Returns the custom home directory if one has been set by calling + # `GPGME::Engine.home_dir=` + # 2. Returns the default home directory otherwise + def current_home_dir + GPGME::Engine.info.first.home_dir || GPGME::Engine.dirinfo('homedir') + end end end diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index 8041518117d..95d371ea178 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -43,6 +43,28 @@ describe Gitlab::Gpg do ).to eq [] end end + + describe '.current_home_dir' do + let(:default_home_dir) { GPGME::Engine.dirinfo('homedir') } + + it 'returns the default value when no explicit home dir has been set' do + expect(described_class.current_home_dir).to eq default_home_dir + end + + it 'returns the explicitely set home dir' do + GPGME::Engine.home_dir = '/tmp/gpg' + + expect(described_class.current_home_dir).to eq '/tmp/gpg' + + GPGME::Engine.home_dir = GPGME::Engine.dirinfo('homedir') + end + + it 'returns the default value when explicitely setting the home dir to nil' do + GPGME::Engine.home_dir = nil + + expect(described_class.current_home_dir).to eq default_home_dir + end + end end describe Gitlab::Gpg::CurrentKeyChain do From ae7c52a06012f1663e784ebbc86e04e566d095b4 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 11 Aug 2017 01:44:17 +0800 Subject: [PATCH 036/141] Fix more tests --- .../projects/issues_controller_spec.rb | 2 + .../filter_merge_requests_spec.rb | 12 +- .../merge_requests/reset_filters_spec.rb | 6 +- spec/features/task_lists_spec.rb | 4 + spec/requests/api/merge_requests_spec.rb | 203 +++++++++--------- .../create_deployment_service_spec.rb | 4 + .../merge_requests/get_urls_service_spec.rb | 8 +- 7 files changed, 120 insertions(+), 119 deletions(-) diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index fe407ffff4b..0156e364df4 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -858,6 +858,8 @@ describe Projects::IssuesController do before do project.add_developer(user) sign_in(user) + + allow_any_instance_of(MergeRequest).to receive(:write_ref) end it 'creates a new merge request' do diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb index f0019be86ad..3686131fee4 100644 --- a/spec/features/merge_requests/filter_merge_requests_spec.rb +++ b/spec/features/merge_requests/filter_merge_requests_spec.rb @@ -12,7 +12,7 @@ describe 'Filter merge requests' do let!(:wontfix) { create(:label, project: project, title: "Won't fix") } before do - project.team << [user, :master] + project.add_master(user) group.add_developer(user) sign_in(user) create(:merge_request, source_project: project, target_project: project) @@ -170,7 +170,7 @@ describe 'Filter merge requests' do describe 'filter merge requests by text' do before do - create(:merge_request, title: "Bug", source_project: project, target_project: project, source_branch: "bug") + create(:merge_request, title: "Bug", source_project: project, target_project: project, source_branch: "wip") bug_label = create(:label, project: project, title: 'bug') milestone = create(:milestone, title: "8", project: project) @@ -179,7 +179,7 @@ describe 'Filter merge requests' do title: "Bug 2", source_project: project, target_project: project, - source_branch: "bug2", + source_branch: "fix", milestone: milestone, author: user, assignee: user) @@ -259,12 +259,12 @@ describe 'Filter merge requests' do end end - describe 'filter merge requests and sort', js: true do + describe 'filter merge requests and sort', :js do before do bug_label = create(:label, project: project, title: 'bug') - mr1 = create(:merge_request, title: "Frontend", source_project: project, target_project: project, source_branch: "Frontend") - mr2 = create(:merge_request, title: "Bug 2", source_project: project, target_project: project, source_branch: "bug2") + mr1 = create(:merge_request, title: "Frontend", source_project: project, target_project: project, source_branch: "wip") + mr2 = create(:merge_request, title: "Bug 2", source_project: project, target_project: project, source_branch: "fix") mr1.labels << bug_label mr2.labels << bug_label diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb index c1b90e5f875..eed95816bdf 100644 --- a/spec/features/merge_requests/reset_filters_spec.rb +++ b/spec/features/merge_requests/reset_filters_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Merge requests filter clear button', js: true do +feature 'Merge requests filter clear button', :js do include FilteredSearchHelpers include MergeRequestHelpers include IssueHelpers @@ -9,8 +9,8 @@ feature 'Merge requests filter clear button', js: true do let!(:user) { create(:user) } let!(:milestone) { create(:milestone, project: project) } let!(:bug) { create(:label, project: project, name: 'bug')} - let!(:mr1) { create(:merge_request, title: "Feature", source_project: project, target_project: project, source_branch: "Feature", milestone: milestone, author: user, assignee: user) } - let!(:mr2) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "Bugfix1") } + let!(:mr1) { create(:merge_request, title: "Feature", source_project: project, target_project: project, source_branch: "improve/awesome", milestone: milestone, author: user, assignee: user) } + let!(:mr2) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "fix") } let(:merge_request_css) { '.merge-request' } let(:clear_search_css) { '.filtered-search-box .clear-search' } diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index 35f025830f1..81d86d249fd 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -245,6 +245,10 @@ feature 'Task Lists' do visit project_merge_request_path(project, merge) end + before do + allow_any_instance_of(Repository).to receive(:write_ref) + end + describe 'multiple tasks' do let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index aa52d240e39..765c2d835b3 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -31,7 +31,7 @@ describe API::MergeRequests do it 'returns authentication error' do get api('/merge_requests') - expect(response).to have_http_status(401) + expect(response).to have_gitlab_http_status(401) end end @@ -43,7 +43,7 @@ describe API::MergeRequests do it 'returns an array of all merge requests' do get api('/merge_requests', user), scope: :all - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.map { |mr| mr['id'] }) @@ -56,7 +56,7 @@ describe API::MergeRequests do get api('/merge_requests', user), scope: :all - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.map { |mr| mr['id'] }) @@ -68,7 +68,7 @@ describe API::MergeRequests do get api('/merge_requests', user2) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(merge_request3.id) @@ -79,7 +79,7 @@ describe API::MergeRequests do get api('/merge_requests', user), author_id: user2.id, scope: :all - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(merge_request3.id) @@ -90,7 +90,7 @@ describe API::MergeRequests do get api('/merge_requests', user), assignee_id: user2.id, scope: :all - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(merge_request3.id) @@ -101,7 +101,7 @@ describe API::MergeRequests do get api('/merge_requests', user2), scope: 'assigned-to-me' - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(merge_request3.id) @@ -112,7 +112,7 @@ describe API::MergeRequests do get api('/merge_requests', user2), scope: 'created-by-me' - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(merge_request3.id) @@ -125,7 +125,7 @@ describe API::MergeRequests do it "returns authentication error" do get api("/projects/#{project.id}/merge_requests") - expect(response).to have_http_status(401) + expect(response).to have_gitlab_http_status(401) end end @@ -145,7 +145,7 @@ describe API::MergeRequests do it "returns an array of all merge_requests" do get api("/projects/#{project.id}/merge_requests", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) @@ -166,7 +166,7 @@ describe API::MergeRequests do it "returns an array of all merge_requests using simple mode" do get api("/projects/#{project.id}/merge_requests?view=simple", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at)) expect(json_response).to be_an Array @@ -182,7 +182,7 @@ describe API::MergeRequests do it "returns an array of all merge_requests" do get api("/projects/#{project.id}/merge_requests?state", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) @@ -192,7 +192,7 @@ describe API::MergeRequests do it "returns an array of open merge_requests" do get api("/projects/#{project.id}/merge_requests?state=opened", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) @@ -202,7 +202,7 @@ describe API::MergeRequests do it "returns an array of closed merge_requests" do get api("/projects/#{project.id}/merge_requests?state=closed", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) @@ -212,7 +212,7 @@ describe API::MergeRequests do it "returns an array of merged merge_requests" do get api("/projects/#{project.id}/merge_requests?state=merged", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) @@ -222,7 +222,7 @@ describe API::MergeRequests do it 'returns merge_request by "iids" array' do get api("/projects/#{project.id}/merge_requests", user), iids: [merge_request.iid, merge_request_closed.iid] - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response.first['title']).to eq merge_request_closed.title @@ -232,14 +232,14 @@ describe API::MergeRequests do it 'matches V4 response schema' do get api("/projects/#{project.id}/merge_requests", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to match_response_schema('public_api/v4/merge_requests') end it 'returns an empty array if no issue matches milestone' do get api("/projects/#{project.id}/merge_requests", user), milestone: '1.0.0' - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -247,7 +247,7 @@ describe API::MergeRequests do it 'returns an empty array if milestone does not exist' do get api("/projects/#{project.id}/merge_requests", user), milestone: 'foo' - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -262,7 +262,7 @@ describe API::MergeRequests do it 'returns an array of merge requests matching state in milestone' do get api("/projects/#{project.id}/merge_requests", user), milestone: '0.9', state: 'closed' - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(merge_request_closed.id) @@ -271,7 +271,7 @@ describe API::MergeRequests do it 'returns an array of labeled merge requests' do get api("/projects/#{project.id}/merge_requests?labels=#{label.title}", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['labels']).to eq([label2.title, label.title]) @@ -280,7 +280,7 @@ describe API::MergeRequests do it 'returns an array of labeled merge requests where all labels match' do get api("/projects/#{project.id}/merge_requests?labels=#{label.title},foo,bar", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -288,7 +288,7 @@ describe API::MergeRequests do it 'returns an empty array if no merge request matches labels' do get api("/projects/#{project.id}/merge_requests?labels=foo,bar", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(0) end @@ -307,7 +307,7 @@ describe API::MergeRequests do get api("/projects/#{project.id}/merge_requests?labels=#{bug_label.title}&milestone=#{milestone1.title}&state=merged", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(mr2.id) @@ -322,7 +322,7 @@ describe API::MergeRequests do it "returns an array of merge_requests in ascending order" do get api("/projects/#{project.id}/merge_requests?sort=asc", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) @@ -333,7 +333,7 @@ describe API::MergeRequests do it "returns an array of merge_requests in descending order" do get api("/projects/#{project.id}/merge_requests?sort=desc", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) @@ -344,7 +344,7 @@ describe API::MergeRequests do it "returns an array of merge_requests ordered by updated_at" do get api("/projects/#{project.id}/merge_requests?order_by=updated_at", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) @@ -355,7 +355,7 @@ describe API::MergeRequests do it "returns an array of merge_requests ordered by created_at" do get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(3) @@ -370,7 +370,7 @@ describe API::MergeRequests do it 'exposes known attributes' do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['id']).to eq(merge_request.id) expect(json_response['iid']).to eq(merge_request.iid) expect(json_response['project_id']).to eq(merge_request.project.id) @@ -398,7 +398,7 @@ describe API::MergeRequests do it "returns merge_request" do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['title']).to eq(merge_request.title) expect(json_response['iid']).to eq(merge_request.iid) expect(json_response['work_in_progress']).to eq(false) @@ -409,13 +409,13 @@ describe API::MergeRequests do it "returns a 404 error if merge_request_iid not found" do get api("/projects/#{project.id}/merge_requests/999", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it "returns a 404 error if merge_request `id` is used instead of iid" do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end context 'Work in Progress' do @@ -423,7 +423,7 @@ describe API::MergeRequests do it "returns merge_request" do get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.iid}", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['work_in_progress']).to eq(true) end end @@ -434,7 +434,7 @@ describe API::MergeRequests do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/commits", user) commit = merge_request.commits.first - expect(response.status).to eq 200 + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(merge_request.commits.size) @@ -444,13 +444,13 @@ describe API::MergeRequests do it 'returns a 404 when merge_request_iid not found' do get api("/projects/#{project.id}/merge_requests/999/commits", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it 'returns a 404 when merge_request id is used instead of iid' do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end end @@ -458,19 +458,19 @@ describe API::MergeRequests do it 'returns the change information of the merge_request' do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes", user) - expect(response.status).to eq 200 + expect(response).to have_gitlab_http_status(200) expect(json_response['changes'].size).to eq(merge_request.diffs.size) end it 'returns a 404 when merge_request_iid not found' do get api("/projects/#{project.id}/merge_requests/999/changes", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it 'returns a 404 when merge_request id is used instead of iid' do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end end @@ -485,7 +485,7 @@ describe API::MergeRequests do labels: 'label, label2', milestone_id: milestone.id - expect(response).to have_http_status(201) + expect(response).to have_gitlab_http_status(201) expect(json_response['title']).to eq('Test merge_request') expect(json_response['labels']).to eq(%w(label label2)) expect(json_response['milestone']['id']).to eq(milestone.id) @@ -495,25 +495,25 @@ describe API::MergeRequests do it "returns 422 when source_branch equals target_branch" do post api("/projects/#{project.id}/merge_requests", user), title: "Test merge_request", source_branch: "master", target_branch: "master", author: user - expect(response).to have_http_status(422) + expect(response).to have_gitlab_http_status(422) end it "returns 400 when source_branch is missing" do post api("/projects/#{project.id}/merge_requests", user), title: "Test merge_request", target_branch: "master", author: user - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) end it "returns 400 when target_branch is missing" do post api("/projects/#{project.id}/merge_requests", user), title: "Test merge_request", source_branch: "markdown", author: user - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) end it "returns 400 when title is missing" do post api("/projects/#{project.id}/merge_requests", user), target_branch: 'master', source_branch: 'markdown' - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) end it 'allows special label names' do @@ -523,7 +523,7 @@ describe API::MergeRequests do target_branch: 'master', author: user, labels: 'label, label?, label&foo, ?, &' - expect(response.status).to eq(201) + expect(response).to have_gitlab_http_status(201) expect(json_response['labels']).to include 'label' expect(json_response['labels']).to include 'label?' expect(json_response['labels']).to include 'label&foo' @@ -549,7 +549,7 @@ describe API::MergeRequests do target_branch: 'master', author: user end.to change { MergeRequest.count }.by(0) - expect(response).to have_http_status(409) + expect(response).to have_gitlab_http_status(409) end end @@ -583,17 +583,8 @@ describe API::MergeRequests do before do fork_project.add_reporter(user2) - Project.all.each(&method(:stub_project_repository_fetch_ref)) - end - - def stub_project_repository_fetch_ref(project) - allow(Project).to receive(:find_by).with(id: project.id.to_s) - .and_return(project) - - allow(Project).to receive(:find).with(project.id) - .and_return(project) - - allow(project.repository).to receive(:fetch_ref) + allow_any_instance_of(Repository).to receive(:fetch_ref) + allow_any_instance_of(Repository).to receive(:write_ref) end it "returns merge_request" do @@ -625,25 +616,25 @@ describe API::MergeRequests do author: user2, target_project_id: project.id - expect(response).to have_http_status(422) + expect(response).to have_gitlab_http_status(422) end it "returns 400 when source_branch is missing" do post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) end it "returns 400 when target_branch is missing" do post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) end it "returns 400 when title is missing" do post api("/projects/#{fork_project.id}/merge_requests", user2), target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) end context 'when target_branch is specified' do @@ -654,7 +645,7 @@ describe API::MergeRequests do source_branch: 'markdown', author: user, target_project_id: fork_project.id - expect(response).to have_http_status(422) + expect(response).to have_gitlab_http_status(422) end it 'returns 422 if targeting a different fork' do @@ -664,14 +655,14 @@ describe API::MergeRequests do source_branch: 'markdown', author: user2, target_project_id: unrelated_project.id - expect(response).to have_http_status(422) + expect(response).to have_gitlab_http_status(422) end end it "returns 201 when target_branch is specified and for the same project" do post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id - expect(response).to have_http_status(201) + expect(response).to have_gitlab_http_status(201) end end end @@ -686,7 +677,7 @@ describe API::MergeRequests do it "denies the deletion of the merge request" do delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", developer) - expect(response).to have_http_status(403) + expect(response).to have_gitlab_http_status(403) end end @@ -694,19 +685,19 @@ describe API::MergeRequests do it "destroys the merge request owners can destroy" do delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) - expect(response).to have_http_status(204) + expect(response).to have_gitlab_http_status(204) end it "returns 404 for an invalid merge request IID" do delete api("/projects/#{project.id}/merge_requests/12345", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it "returns 404 if the merge request id is used instead of iid" do delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end end end @@ -717,7 +708,7 @@ describe API::MergeRequests do it "returns merge_request in case of success" do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) end it "returns 406 if branch can't be merged" do @@ -726,21 +717,21 @@ describe API::MergeRequests do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) - expect(response).to have_http_status(406) + expect(response).to have_gitlab_http_status(406) expect(json_response['message']).to eq('Branch cannot be merged') end it "returns 405 if merge_request is not open" do merge_request.close put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) - expect(response).to have_http_status(405) + expect(response).to have_gitlab_http_status(405) expect(json_response['message']).to eq('405 Method Not Allowed') end it "returns 405 if merge_request is a work in progress" do merge_request.update_attribute(:title, "WIP: #{merge_request.title}") put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) - expect(response).to have_http_status(405) + expect(response).to have_gitlab_http_status(405) expect(json_response['message']).to eq('405 Method Not Allowed') end @@ -749,7 +740,7 @@ describe API::MergeRequests do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user) - expect(response).to have_http_status(405) + expect(response).to have_gitlab_http_status(405) expect(json_response['message']).to eq('405 Method Not Allowed') end @@ -757,21 +748,21 @@ describe API::MergeRequests do user2 = create(:user) project.team << [user2, :reporter] put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user2) - expect(response).to have_http_status(401) + expect(response).to have_gitlab_http_status(401) expect(json_response['message']).to eq('401 Unauthorized') end it "returns 409 if the SHA parameter doesn't match" do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), sha: merge_request.diff_head_sha.reverse - expect(response).to have_http_status(409) + expect(response).to have_gitlab_http_status(409) expect(json_response['message']).to start_with('SHA does not match HEAD of source branch') end it "succeeds if the SHA parameter matches" do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), sha: merge_request.diff_head_sha - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) end it "enables merge when pipeline succeeds if the pipeline is active" do @@ -780,7 +771,7 @@ describe API::MergeRequests do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), merge_when_pipeline_succeeds: true - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['title']).to eq('Test') expect(json_response['merge_when_pipeline_succeeds']).to eq(true) end @@ -792,7 +783,7 @@ describe API::MergeRequests do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), merge_when_pipeline_succeeds: true - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['title']).to eq('Test') expect(json_response['merge_when_pipeline_succeeds']).to eq(true) end @@ -800,13 +791,13 @@ describe API::MergeRequests do it "returns 404 for an invalid merge request IID" do put api("/projects/#{project.id}/merge_requests/12345/merge", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it "returns 404 if the merge request id is used instead of iid" do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end end @@ -815,39 +806,39 @@ describe API::MergeRequests do it "returns merge_request" do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: "close" - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['state']).to eq('closed') end end it "updates title and returns merge_request" do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), title: "New title" - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['title']).to eq('New title') end it "updates description and returns merge_request" do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), description: "New description" - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['description']).to eq('New description') end it "updates milestone_id and returns merge_request" do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), milestone_id: milestone.id - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['milestone']['id']).to eq(milestone.id) end it "returns merge_request with renamed target_branch" do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), target_branch: "wiki" - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['target_branch']).to eq('wiki') end it "returns merge_request that removes the source branch" do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), remove_source_branch: true - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(json_response['force_remove_source_branch']).to be_truthy end @@ -868,7 +859,7 @@ describe API::MergeRequests do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: 'close', title: nil merge_request.reload - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) expect(merge_request.state).to eq('opened') end @@ -876,20 +867,20 @@ describe API::MergeRequests do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: 'close', target_branch: nil merge_request.reload - expect(response).to have_http_status(400) + expect(response).to have_gitlab_http_status(400) expect(merge_request.state).to eq('opened') end it "returns 404 for an invalid merge request IID" do put api("/projects/#{project.id}/merge_requests/12345", user), state_event: "close" - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it "returns 404 if the merge request id is used instead of iid" do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close" - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end end @@ -902,7 +893,7 @@ describe API::MergeRequests do get api("/projects/#{project.id}/merge_requests/#{mr.iid}/closes_issues", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(1) @@ -912,7 +903,7 @@ describe API::MergeRequests do it 'returns an empty array when there are no issues to be closed' do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/closes_issues", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(0) @@ -928,7 +919,7 @@ describe API::MergeRequests do get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.iid}/closes_issues", user) - expect(response).to have_http_status(200) + expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.length).to eq(2) @@ -948,19 +939,19 @@ describe API::MergeRequests do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/closes_issues", guest) - expect(response).to have_http_status(403) + expect(response).to have_gitlab_http_status(403) end it "returns 404 for an invalid merge request IID" do get api("/projects/#{project.id}/merge_requests/12345/closes_issues", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it "returns 404 if the merge request id is used instead of iid" do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end end @@ -968,26 +959,26 @@ describe API::MergeRequests do it 'subscribes to a merge request' do post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", admin) - expect(response).to have_http_status(201) + expect(response).to have_gitlab_http_status(201) expect(json_response['subscribed']).to eq(true) end it 'returns 304 if already subscribed' do post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", user) - expect(response).to have_http_status(304) + expect(response).to have_gitlab_http_status(304) end it 'returns 404 if the merge request is not found' do post api("/projects/#{project.id}/merge_requests/123/subscribe", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it 'returns 404 if the merge request id is used instead of iid' do post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it 'returns 403 if user has no access to read code' do @@ -996,7 +987,7 @@ describe API::MergeRequests do post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", guest) - expect(response).to have_http_status(403) + expect(response).to have_gitlab_http_status(403) end end @@ -1004,26 +995,26 @@ describe API::MergeRequests do it 'unsubscribes from a merge request' do post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", user) - expect(response).to have_http_status(201) + expect(response).to have_gitlab_http_status(201) expect(json_response['subscribed']).to eq(false) end it 'returns 304 if not subscribed' do post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", admin) - expect(response).to have_http_status(304) + expect(response).to have_gitlab_http_status(304) end it 'returns 404 if the merge request is not found' do post api("/projects/#{project.id}/merge_requests/123/unsubscribe", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it 'returns 404 if the merge request id is used instead of iid' do post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", user) - expect(response).to have_http_status(404) + expect(response).to have_gitlab_http_status(404) end it 'returns 403 if user has no access to read code' do @@ -1032,7 +1023,7 @@ describe API::MergeRequests do post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", guest) - expect(response).to have_http_status(403) + expect(response).to have_gitlab_http_status(403) end end diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb index 049b082277a..08267d6e6a0 100644 --- a/spec/services/create_deployment_service_spec.rb +++ b/spec/services/create_deployment_service_spec.rb @@ -20,6 +20,10 @@ describe CreateDeploymentService do let(:service) { described_class.new(job) } + before do + allow_any_instance_of(Deployment).to receive(:create_ref) + end + describe '#execute' do subject { service.execute } diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb index 672d86e4028..25599dea19f 100644 --- a/spec/services/merge_requests/get_urls_service_spec.rb +++ b/spec/services/merge_requests/get_urls_service_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" describe MergeRequests::GetUrlsService do let(:project) { create(:project, :public, :repository) } let(:service) { described_class.new(project) } - let(:source_branch) { "my_branch" } + let(:source_branch) { "merge-test" } let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" } let(:show_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" } let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } @@ -111,9 +111,9 @@ describe MergeRequests::GetUrlsService do end context 'pushing new branch and existing branch (with merge request created) at once' do - let!(:merge_request) { create(:merge_request, source_project: project, source_branch: "existing_branch") } + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: "markdown") } let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" } - let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/existing_branch" } + let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/markdown" } let(:changes) { "#{new_branch_changes}\n#{existing_branch_changes}" } let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" } @@ -124,7 +124,7 @@ describe MergeRequests::GetUrlsService do url: new_merge_request_url, new_merge_request: true }, { - branch_name: "existing_branch", + branch_name: "markdown", url: show_merge_request_url, new_merge_request: false }]) From caa498fd315fde3d4189da0b0293961dbcfa5e92 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Thu, 10 Aug 2017 18:23:56 +0100 Subject: [PATCH 037/141] Use rspec-parameterized for table-based tests --- Gemfile | 1 + Gemfile.lock | 33 +++++++++++++++++++++++++++++++++ doc/development/testing.md | 37 +++++++++++++++++++++++++++++++++++++ spec/spec_helper.rb | 1 + 4 files changed, 72 insertions(+) diff --git a/Gemfile b/Gemfile index 2a4b4717153..accbb5f8b63 100644 --- a/Gemfile +++ b/Gemfile @@ -324,6 +324,7 @@ group :development, :test do gem 'spinach-rerun-reporter', '~> 0.0.2' gem 'rspec_profiling', '~> 0.0.5' gem 'rspec-set', '~> 0.1.3' + gem 'rspec-parameterized' # 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 79d1bc51358..5423fefb40a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,6 +2,7 @@ GEM remote: https://rubygems.org/ specs: RedCloth (4.3.2) + abstract_type (0.0.7) ace-rails-ap (4.1.2) actionmailer (4.2.8) actionpack (= 4.2.8) @@ -41,6 +42,9 @@ GEM tzinfo (~> 1.1) acts-as-taggable-on (4.0.0) activerecord (>= 4.0) + adamantium (0.2.0) + ice_nine (~> 0.11.0) + memoizable (~> 0.4.0) addressable (2.3.8) after_commit_queue (1.3.0) activerecord (>= 3.0) @@ -124,6 +128,9 @@ GEM coercible (1.0.0) descendants_tracker (~> 0.0.1) colorize (0.7.7) + concord (0.1.5) + adamantium (~> 0.2.0) + equalizer (~> 0.0.9) concurrent-ruby (1.0.5) concurrent-ruby-ext (1.0.5) concurrent-ruby (= 1.0.5) @@ -461,6 +468,8 @@ GEM mime-types (>= 1.16, < 4) mail_room (0.9.1) memoist (0.15.0) + memoizable (0.4.2) + thread_safe (~> 0.3, >= 0.3.1) method_source (0.8.2) mime-types (2.99.3) mimemagic (0.3.0) @@ -601,6 +610,11 @@ GEM premailer-rails (1.9.7) actionmailer (>= 3, < 6) premailer (~> 1.7, >= 1.7.9) + proc_to_ast (0.1.0) + coderay + parser + unparser + procto (0.0.3) prometheus-client-mmap (0.7.0.beta11) mmap2 (~> 2.2, >= 2.2.7) pry (0.10.4) @@ -709,6 +723,10 @@ GEM chunky_png rqrcode-rails3 (0.1.7) rqrcode (>= 0.4.2) + rspec (3.6.0) + rspec-core (~> 3.6.0) + rspec-expectations (~> 3.6.0) + rspec-mocks (~> 3.6.0) rspec-core (3.6.0) rspec-support (~> 3.6.0) rspec-expectations (3.6.0) @@ -717,6 +735,12 @@ GEM rspec-mocks (3.6.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.6.0) + rspec-parameterized (0.4.0) + binding_of_caller + parser + proc_to_ast + rspec (>= 2.13, < 4) + unparser rspec-rails (3.6.0) actionpack (>= 3.0) activesupport (>= 3.0) @@ -883,6 +907,14 @@ GEM get_process_mem (~> 0) unicorn (>= 4, < 6) uniform_notifier (1.10.0) + unparser (0.2.6) + abstract_type (~> 0.0.7) + adamantium (~> 0.2.0) + concord (~> 0.1.5) + diff-lcs (~> 1.3) + equalizer (~> 0.0.9) + parser (>= 2.3.1.2, < 2.5) + procto (~> 0.0.2) url_safe_base64 (0.2.2) validates_hostname (1.0.6) activerecord (>= 3.0) @@ -1085,6 +1117,7 @@ DEPENDENCIES responders (~> 2.0) rouge (~> 2.0) rqrcode-rails3 (~> 0.1.7) + rspec-parameterized rspec-rails (~> 3.6.0) rspec-retry (~> 0.4.5) rspec-set (~> 0.1.3) diff --git a/doc/development/testing.md b/doc/development/testing.md index 3d5aa3d45e9..56dc8abd38a 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -268,6 +268,43 @@ end - Avoid scenario titles that add no information, such as "successfully". - Avoid scenario titles that repeat the feature title. +### Table-based / Parameterized tests + +This style of testing is used to exercise one piece of code with a comprehensive +range of inputs. By specifying the test case once, alongside a table of inputs +and the expected output for each, your tests can be made easier to read and more +compact. + +We use the [rspec-parameterized](https://github.com/tomykaira/rspec-parameterized) +gem. A short example, using the table syntax and checking Ruby equality for a +range of inputs, might look like this: + +```ruby +describe "#==" do + using Rspec::Parameterized::TableSyntax + + let(:project1) { create(:project) } + let(:project2) { create(:project) } + where(:a, :b, :result) do + 1 | 1 | true + 1 | 2 | false + true | true | true + true | false | false + project1 | project1 | true + project2 | project2 | true + project 1 | project2 | false + end + + with_them do + it { expect(a == b).to eq(result) } + + it 'is isomorphic' do + expect(b == a).to eq(result) + end + end +end +``` + ### Matchers Custom matchers should be created to clarify the intent and/or hide the diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0ba6ed56314..55691f6716f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -8,6 +8,7 @@ require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' require 'shoulda/matchers' require 'rspec/retry' +require 'rspec-parameterized' rspec_profiling_is_configured = ENV['RSPEC_PROFILING_POSTGRES_URL'].present? || From 501be36c2e3e5c0191e68d23df0f330100bbb8be Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 11 Aug 2017 20:10:43 +0800 Subject: [PATCH 038/141] Just use the repo. Not sure why master could pass It keeps giving me no repo error from setting up autocrlf, which shouldn't have anything to do with this merge request. --- spec/controllers/projects/issues_controller_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 68f38c4c6ea..b571b11dcac 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -855,11 +855,11 @@ describe Projects::IssuesController do end describe 'POST create_merge_request' do + let(:project) { create(:project, :repository) } + before do project.add_developer(user) sign_in(user) - - allow_any_instance_of(MergeRequest).to receive(:write_ref) end it 'creates a new merge request' do From ca685f80e0fffa60827fe0da7cff1192cef701de Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 11 Aug 2017 20:12:17 +0800 Subject: [PATCH 039/141] Since now fetch_ref is reliable, we could just rely on it --- app/models/repository.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 04403cdd035..a761302b06b 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -989,12 +989,10 @@ class Repository if start_repository == self start_branch_name else - tmp_ref = "refs/tmp/#{SecureRandom.hex}/head" - - fetch_ref( + tmp_ref = fetch_ref( start_repository.path_to_repo, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", - tmp_ref + "refs/tmp/#{SecureRandom.hex}/head" ) start_repository.commit(start_branch_name).sha @@ -1003,7 +1001,7 @@ class Repository yield(commit(branch_name_or_sha)) ensure - rugged.references.delete(tmp_ref) if tmp_ref && ref_exists?(tmp_ref) + rugged.references.delete(tmp_ref) if tmp_ref end def add_remote(name, url) @@ -1029,6 +1027,8 @@ class Repository # Make sure ref was created, and raise Rugged::ReferenceError when not raise Rugged::ReferenceError, message if status != 0 + + target_ref end def create_ref(ref, ref_path) From 34741e932742c426896955f357f6b406bfd1d6f2 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 11 Aug 2017 20:21:32 +0800 Subject: [PATCH 040/141] Just use repository would fix the test --- spec/features/task_lists_spec.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index 81d86d249fd..580258f77eb 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -245,11 +245,8 @@ feature 'Task Lists' do visit project_merge_request_path(project, merge) end - before do - allow_any_instance_of(Repository).to receive(:write_ref) - end - describe 'multiple tasks' do + let(:project) { create(:project, :repository) } let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) } it 'renders for description' do From fab0c1eb80b3eda00024a4e1fb961ba5b8bcc7bb Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 10 Aug 2017 18:24:44 +0200 Subject: [PATCH 041/141] Use existing BUNDLE_PATH for gitaly in local tests --- lib/tasks/gitlab/gitaly.rake | 8 ++++++-- spec/tasks/gitlab/gitaly_rake_spec.rb | 21 ++++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index 1f504485e4c..e337c67a0f5 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -15,13 +15,17 @@ namespace :gitlab do checkout_or_clone_version(version: version, repo: args.repo, target_dir: args.dir) _, status = Gitlab::Popen.popen(%w[which gmake]) - command = status.zero? ? 'gmake' : 'make' + command = status.zero? ? ['gmake'] : ['make'] + + if Rails.env.test? + command += %W[BUNDLE_PATH=#{Bundler.bundle_path}] + end Dir.chdir(args.dir) do create_gitaly_configuration # In CI we run scripts/gitaly-test-build instead of this command unless ENV['CI'].present? - Bundler.with_original_env { run_command!(%w[/usr/bin/env -u RUBYOPT -u BUNDLE_GEMFILE] + [command]) } + Bundler.with_original_env { run_command!(%w[/usr/bin/env -u RUBYOPT -u BUNDLE_GEMFILE] + command) } end end end diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index 43ac1a72152..b29d63c7d67 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -54,17 +54,17 @@ describe 'gitlab:gitaly namespace rake task' do before do FileUtils.mkdir_p(clone_path) expect(Dir).to receive(:chdir).with(clone_path).and_call_original + allow(Bundler).to receive(:bundle_path).and_return('/fake/bundle_path') end context 'gmake is available' do before do expect(main_object).to receive(:checkout_or_clone_version) - allow(main_object).to receive(:run_command!).with(command_preamble + ['gmake']).and_return(true) end it 'calls gmake in the gitaly directory' do expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['/usr/bin/gmake', 0]) - expect(main_object).to receive(:run_command!).with(command_preamble + ['gmake']).and_return(true) + expect(main_object).to receive(:run_command!).with(command_preamble + %w[gmake BUNDLE_PATH=/fake/bundle_path]).and_return(true) run_rake_task('gitlab:gitaly:install', clone_path) end @@ -73,15 +73,26 @@ describe 'gitlab:gitaly namespace rake task' do context 'gmake is not available' do before do expect(main_object).to receive(:checkout_or_clone_version) - allow(main_object).to receive(:run_command!).with(command_preamble + ['make']).and_return(true) + expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['', 42]) end it 'calls make in the gitaly directory' do - expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['', 42]) - expect(main_object).to receive(:run_command!).with(command_preamble + ['make']).and_return(true) + expect(main_object).to receive(:run_command!).with(command_preamble + %w[make BUNDLE_PATH=/fake/bundle_path]).and_return(true) run_rake_task('gitlab:gitaly:install', clone_path) end + + context 'when Rails.env is not "test"' do + before do + allow(Rails.env).to receive(:test?).and_return(false) + end + + it 'calls make in the gitaly directory without BUNDLE_PATH' do + expect(main_object).to receive(:run_command!).with(command_preamble + ['make']).and_return(true) + + run_rake_task('gitlab:gitaly:install', clone_path) + end + end end end end From f05bfef70cb1e22942c7cdf0e3432a984281020a Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Fri, 11 Aug 2017 09:30:48 -0500 Subject: [PATCH 042/141] Increase z-index of pipeline dropdown --- app/assets/stylesheets/pages/pipelines.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 6185342b495..85d1905ad40 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -824,6 +824,7 @@ button.mini-pipeline-graph-dropdown-toggle { * Top arrow in the dropdown in the mini pipeline graph */ .mini-pipeline-graph-dropdown-menu { + z-index: 200; &::before, &::after { From ba8321a52af4f5258526ed4f864bbf3e7a752571 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 11 Aug 2017 23:27:42 +0800 Subject: [PATCH 043/141] Skip creating the merge request if repo is empty --- db/fixtures/development/10_merge_requests.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb index c304e0706dc..30244ee4431 100644 --- a/db/fixtures/development/10_merge_requests.rb +++ b/db/fixtures/development/10_merge_requests.rb @@ -28,6 +28,8 @@ Gitlab::Seeder.quiet do project = Project.find_by_full_path('gitlab-org/gitlab-test') + next if project.empty_repo? # We don't have repository on CI + params = { source_branch: 'feature', target_branch: 'master', From 969ccec7bbaef3d55c3fc515de2b1d5335d6ae12 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 2 Aug 2017 13:24:12 +0200 Subject: [PATCH 044/141] Don't rename the system namespace --- .../bvl-rollback-renamed-system-namespace.yml | 4 + ...20170316163800_rename_system_namespaces.rb | 231 ---------------- .../rename_system_namespaces_spec.rb | 254 ------------------ 3 files changed, 4 insertions(+), 485 deletions(-) create mode 100644 changelogs/unreleased/bvl-rollback-renamed-system-namespace.yml delete mode 100644 db/migrate/20170316163800_rename_system_namespaces.rb delete mode 100644 spec/migrations/rename_system_namespaces_spec.rb diff --git a/changelogs/unreleased/bvl-rollback-renamed-system-namespace.yml b/changelogs/unreleased/bvl-rollback-renamed-system-namespace.yml new file mode 100644 index 00000000000..a24cc7a1c43 --- /dev/null +++ b/changelogs/unreleased/bvl-rollback-renamed-system-namespace.yml @@ -0,0 +1,4 @@ +--- +title: Don't rename namespace called system when upgrading from 9.1.x to 9.5 +merge_request: 13228 +author: diff --git a/db/migrate/20170316163800_rename_system_namespaces.rb b/db/migrate/20170316163800_rename_system_namespaces.rb deleted file mode 100644 index 9e9fb5ac225..00000000000 --- a/db/migrate/20170316163800_rename_system_namespaces.rb +++ /dev/null @@ -1,231 +0,0 @@ -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. -class RenameSystemNamespaces < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - include Gitlab::ShellAdapter - disable_ddl_transaction! - - class User < ActiveRecord::Base - self.table_name = 'users' - end - - class Namespace < ActiveRecord::Base - self.table_name = 'namespaces' - belongs_to :parent, class_name: 'RenameSystemNamespaces::Namespace' - has_one :route, as: :source - has_many :children, class_name: 'RenameSystemNamespaces::Namespace', foreign_key: :parent_id - belongs_to :owner, class_name: 'RenameSystemNamespaces::User' - - # Overridden to have the correct `source_type` for the `route` relation - def self.name - 'Namespace' - end - - def full_path - if route && route.path.present? - @full_path ||= route.path - else - update_route if persisted? - - build_full_path - end - end - - def build_full_path - if parent && path - parent.full_path + '/' + path - else - path - end - end - - def update_route - prepare_route - route.save - end - - def prepare_route - route || build_route(source: self) - route.path = build_full_path - route.name = build_full_name - @full_path = nil - @full_name = nil - end - - def build_full_name - if parent && name - parent.human_name + ' / ' + name - else - name - end - end - - def human_name - owner&.name - end - end - - class Route < ActiveRecord::Base - self.table_name = 'routes' - belongs_to :source, polymorphic: true - end - - class Project < ActiveRecord::Base - self.table_name = 'projects' - - def repository_storage_path - Gitlab.config.repositories.storages[repository_storage]['path'] - end - end - - DOWNTIME = false - - def up - return unless system_namespace - - old_path = system_namespace.path - old_full_path = system_namespace.full_path - # Only remove the last occurrence of the path name to get the parent namespace path - namespace_path = remove_last_occurrence(old_full_path, old_path) - new_path = rename_path(namespace_path, old_path) - new_full_path = join_namespace_path(namespace_path, new_path) - - Namespace.where(id: system_namespace).update_all(path: new_path) # skips callbacks & validations - - replace_statement = replace_sql(Route.arel_table[:path], old_full_path, new_full_path) - route_matches = [old_full_path, "#{old_full_path}/%"] - - update_column_in_batches(:routes, :path, replace_statement) do |table, query| - query.where(Route.arel_table[:path].matches_any(route_matches)) - end - - clear_cache_for_namespace(system_namespace) - - # tasks here are based on `Namespace#move_dir` - move_repositories(system_namespace, old_full_path, new_full_path) - move_namespace_folders(uploads_dir, old_full_path, new_full_path) if file_storage? - move_namespace_folders(pages_dir, old_full_path, new_full_path) - end - - def down - # nothing to do - end - - def remove_last_occurrence(string, pattern) - string.reverse.sub(pattern.reverse, "").reverse - end - - def move_namespace_folders(directory, old_relative_path, new_relative_path) - old_path = File.join(directory, old_relative_path) - return unless File.directory?(old_path) - - new_path = File.join(directory, new_relative_path) - FileUtils.mv(old_path, new_path) - end - - def move_repositories(namespace, old_full_path, new_full_path) - repo_paths_for_namespace(namespace).each do |repository_storage_path| - # Ensure old directory exists before moving it - gitlab_shell.add_namespace(repository_storage_path, old_full_path) - - unless gitlab_shell.mv_namespace(repository_storage_path, old_full_path, new_full_path) - say "Exception moving path #{repository_storage_path} from #{old_full_path} to #{new_full_path}" - end - end - end - - def rename_path(namespace_path, path_was) - counter = 0 - path = "#{path_was}#{counter}" - - while route_exists?(join_namespace_path(namespace_path, path)) - counter += 1 - path = "#{path_was}#{counter}" - end - - path - end - - def route_exists?(full_path) - Route.where(Route.arel_table[:path].matches(full_path)).any? - end - - def join_namespace_path(namespace_path, path) - if namespace_path.present? - File.join(namespace_path, path) - else - path - end - end - - def system_namespace - @system_namespace ||= Namespace.where(parent_id: nil) - .where(arel_table[:path].matches(system_namespace_path)) - .first - end - - def system_namespace_path - "system" - end - - def clear_cache_for_namespace(namespace) - project_ids = projects_for_namespace(namespace).pluck(:id) - - update_column_in_batches(:projects, :description_html, nil) do |table, query| - query.where(table[:id].in(project_ids)) - end - - update_column_in_batches(:issues, :description_html, nil) do |table, query| - query.where(table[:project_id].in(project_ids)) - end - - update_column_in_batches(:merge_requests, :description_html, nil) do |table, query| - query.where(table[:target_project_id].in(project_ids)) - end - - update_column_in_batches(:notes, :note_html, nil) do |table, query| - query.where(table[:project_id].in(project_ids)) - end - - update_column_in_batches(:milestones, :description_html, nil) do |table, query| - query.where(table[:project_id].in(project_ids)) - end - end - - def projects_for_namespace(namespace) - namespace_ids = child_ids_for_parent(namespace, ids: [namespace.id]) - namespace_or_children = Project.arel_table[:namespace_id].in(namespace_ids) - Project.unscoped.where(namespace_or_children) - end - - # This won't scale to huge trees, but it should do for a handful of namespaces - # called `system`. - def child_ids_for_parent(namespace, ids: []) - namespace.children.each do |child| - ids << child.id - child_ids_for_parent(child, ids: ids) if child.children.any? - end - ids - end - - def repo_paths_for_namespace(namespace) - projects_for_namespace(namespace).distinct - .select(:repository_storage).map(&:repository_storage_path) - end - - def uploads_dir - File.join(Rails.root, "public", "uploads") - end - - def pages_dir - Settings.pages.path - end - - def file_storage? - CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File - end - - def arel_table - Namespace.arel_table - end -end diff --git a/spec/migrations/rename_system_namespaces_spec.rb b/spec/migrations/rename_system_namespaces_spec.rb deleted file mode 100644 index 747694cbe33..00000000000 --- a/spec/migrations/rename_system_namespaces_spec.rb +++ /dev/null @@ -1,254 +0,0 @@ -require "spec_helper" -require Rails.root.join("db", "migrate", "20170316163800_rename_system_namespaces.rb") - -describe RenameSystemNamespaces, truncate: true do - let(:migration) { described_class.new } - let(:test_dir) { File.join(Rails.root, "tmp", "tests", "rename_namespaces_test") } - let(:uploads_dir) { File.join(test_dir, "public", "uploads") } - let(:system_namespace) do - namespace = build(:namespace, path: "system") - namespace.save(validate: false) - namespace - end - - def save_invalid_routable(routable) - routable.__send__(:prepare_route) - routable.save(validate: false) - end - - before do - FileUtils.remove_dir(test_dir) if File.directory?(test_dir) - FileUtils.mkdir_p(uploads_dir) - FileUtils.remove_dir(TestEnv.repos_path) if File.directory?(TestEnv.repos_path) - allow(migration).to receive(:say) - allow(migration).to receive(:uploads_dir).and_return(uploads_dir) - end - - describe "#system_namespace" do - it "only root namespaces called with path `system`" do - system_namespace - system_namespace_with_parent = build(:namespace, path: 'system', parent: create(:namespace)) - system_namespace_with_parent.save(validate: false) - - expect(migration.system_namespace.id).to eq(system_namespace.id) - end - end - - describe "#up" do - before do - system_namespace - end - - it "doesn't break if there are no namespaces called system" do - Namespace.delete_all - - migration.up - end - - it "renames namespaces called system" do - migration.up - - expect(system_namespace.reload.path).to eq("system0") - end - - it "renames the route to the namespace" do - migration.up - - expect(system_namespace.reload.full_path).to eq("system0") - end - - it "renames the route for projects of the namespace" do - project = build(:project, :repository, path: "project-path", namespace: system_namespace) - save_invalid_routable(project) - - migration.up - - expect(project.route.reload.path).to eq("system0/project-path") - end - - it "doesn't touch routes of namespaces that look like system" do - namespace = create(:group, path: 'systemlookalike') - project = create(:project, :repository, namespace: namespace, path: 'the-project') - - migration.up - - expect(project.route.reload.path).to eq('systemlookalike/the-project') - expect(namespace.route.reload.path).to eq('systemlookalike') - end - - it "moves the the repository for a project in the namespace" do - project = build(:project, :repository, namespace: system_namespace, path: "system-project") - save_invalid_routable(project) - TestEnv.copy_repo(project, - bare_repo: TestEnv.factory_repo_path_bare, - refs: TestEnv::BRANCH_SHA) - expected_repo = File.join(TestEnv.repos_path, "system0", "system-project.git") - - migration.up - - expect(File.directory?(expected_repo)).to be(true) - end - - it "moves the uploads for the namespace" do - allow(migration).to receive(:move_namespace_folders).with(Settings.pages.path, "system", "system0") - expect(migration).to receive(:move_namespace_folders).with(uploads_dir, "system", "system0") - - migration.up - end - - it "moves the pages for the namespace" do - allow(migration).to receive(:move_namespace_folders).with(uploads_dir, "system", "system0") - expect(migration).to receive(:move_namespace_folders).with(Settings.pages.path, "system", "system0") - - migration.up - end - - describe "clears the markdown cache for projects in the system namespace" do - let!(:project) do - project = build(:project, :repository, namespace: system_namespace) - save_invalid_routable(project) - project - end - - it 'removes description_html from projects' do - migration.up - - expect(project.reload.description_html).to be_nil - end - - it 'removes issue descriptions' do - issue = create(:issue, project: project, description_html: 'Issue description') - - migration.up - - expect(issue.reload.description_html).to be_nil - end - - it 'removes merge request descriptions' do - merge_request = create(:merge_request, - source_project: project, - target_project: project, - description_html: 'MergeRequest description') - - migration.up - - expect(merge_request.reload.description_html).to be_nil - end - - it 'removes note html' do - note = create(:note, - project: project, - noteable: create(:issue, project: project), - note_html: 'note description') - - migration.up - - expect(note.reload.note_html).to be_nil - end - - it 'removes milestone description' do - milestone = create(:milestone, - project: project, - description_html: 'milestone description') - - migration.up - - expect(milestone.reload.description_html).to be_nil - end - end - - context "system namespace -> subgroup -> system0 project" do - it "updates the route of the project correctly" do - subgroup = build(:group, path: "subgroup", parent: system_namespace) - save_invalid_routable(subgroup) - project = build(:project, :repository, path: "system0", namespace: subgroup) - save_invalid_routable(project) - - migration.up - - expect(project.route.reload.path).to eq("system0/subgroup/system0") - end - end - end - - describe "#move_repositories" do - let(:namespace) { create(:group, name: "hello-group") } - it "moves a project for a namespace" do - create(:project, :repository, namespace: namespace, path: "hello-project") - expected_path = File.join(TestEnv.repos_path, "bye-group", "hello-project.git") - - migration.move_repositories(namespace, "hello-group", "bye-group") - - expect(File.directory?(expected_path)).to be(true) - end - - it "moves a namespace in a subdirectory correctly" do - child_namespace = create(:group, name: "sub-group", parent: namespace) - create(:project, :repository, namespace: child_namespace, path: "hello-project") - - expected_path = File.join(TestEnv.repos_path, "hello-group", "renamed-sub-group", "hello-project.git") - - migration.move_repositories(child_namespace, "hello-group/sub-group", "hello-group/renamed-sub-group") - - expect(File.directory?(expected_path)).to be(true) - end - - it "moves a parent namespace with subdirectories" do - child_namespace = create(:group, name: "sub-group", parent: namespace) - create(:project, :repository, namespace: child_namespace, path: "hello-project") - expected_path = File.join(TestEnv.repos_path, "renamed-group", "sub-group", "hello-project.git") - - migration.move_repositories(child_namespace, "hello-group", "renamed-group") - - expect(File.directory?(expected_path)).to be(true) - end - end - - describe "#move_namespace_folders" do - it "moves a namespace with files" do - source = File.join(uploads_dir, "parent-group", "sub-group") - FileUtils.mkdir_p(source) - destination = File.join(uploads_dir, "parent-group", "moved-group") - FileUtils.touch(File.join(source, "test.txt")) - expected_file = File.join(destination, "test.txt") - - migration.move_namespace_folders(uploads_dir, File.join("parent-group", "sub-group"), File.join("parent-group", "moved-group")) - - expect(File.exist?(expected_file)).to be(true) - end - - it "moves a parent namespace uploads" do - source = File.join(uploads_dir, "parent-group", "sub-group") - FileUtils.mkdir_p(source) - destination = File.join(uploads_dir, "moved-parent", "sub-group") - FileUtils.touch(File.join(source, "test.txt")) - expected_file = File.join(destination, "test.txt") - - migration.move_namespace_folders(uploads_dir, "parent-group", "moved-parent") - - expect(File.exist?(expected_file)).to be(true) - end - end - - describe "#child_ids_for_parent" do - it "collects child ids for all levels" do - parent = create(:group) - first_child = create(:group, parent: parent) - second_child = create(:group, parent: parent) - third_child = create(:group, parent: second_child) - all_ids = [parent.id, first_child.id, second_child.id, third_child.id] - - collected_ids = migration.child_ids_for_parent(parent, ids: [parent.id]) - - expect(collected_ids).to contain_exactly(*all_ids) - end - end - - describe "#remove_last_ocurrence" do - it "removes only the last occurance of a string" do - input = "this/is/system/namespace/with/system" - - expect(migration.remove_last_occurrence(input, "system")).to eq("this/is/system/namespace/with/") - end - end -end From f0f4506775d0e06b28ee55d591cb223115e3975a Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 2 Aug 2017 15:31:39 +0200 Subject: [PATCH 045/141] Don't update upload paths twice This will be done in 20170717150329_enqueue_migrate_system_uploads_to_new_folder.rb instead. --- ...317162059_update_upload_paths_to_system.rb | 57 ------------------- .../update_upload_paths_to_system_spec.rb | 53 ----------------- 2 files changed, 110 deletions(-) delete mode 100644 db/post_migrate/20170317162059_update_upload_paths_to_system.rb delete mode 100644 spec/migrations/update_upload_paths_to_system_spec.rb diff --git a/db/post_migrate/20170317162059_update_upload_paths_to_system.rb b/db/post_migrate/20170317162059_update_upload_paths_to_system.rb deleted file mode 100644 index ca2912f8dce..00000000000 --- a/db/post_migrate/20170317162059_update_upload_paths_to_system.rb +++ /dev/null @@ -1,57 +0,0 @@ -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. - -class UpdateUploadPathsToSystem < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - DOWNTIME = false - AFFECTED_MODELS = %w(User Project Note Namespace Appearance) - - disable_ddl_transaction! - - def up - update_column_in_batches(:uploads, :path, replace_sql(arel_table[:path], base_directory, new_upload_dir)) do |_table, query| - query.where(uploads_to_switch_to_new_path) - end - end - - def down - update_column_in_batches(:uploads, :path, replace_sql(arel_table[:path], new_upload_dir, base_directory)) do |_table, query| - query.where(uploads_to_switch_to_old_path) - end - end - - # "SELECT \"uploads\".* FROM \"uploads\" WHERE \"uploads\".\"model_type\" IN ('User', 'Project', 'Note', 'Namespace', 'Appearance') AND (\"uploads\".\"path\" ILIKE 'uploads/%' AND NOT (\"uploads\".\"path\" ILIKE 'uploads/system/%'))" - def uploads_to_switch_to_new_path - affected_uploads.and(starting_with_base_directory).and(starting_with_new_upload_directory.not) - end - - # "SELECT \"uploads\".* FROM \"uploads\" WHERE \"uploads\".\"model_type\" IN ('User', 'Project', 'Note', 'Namespace', 'Appearance') AND (\"uploads\".\"path\" ILIKE 'uploads/%' AND \"uploads\".\"path\" ILIKE 'uploads/system/%')" - def uploads_to_switch_to_old_path - affected_uploads.and(starting_with_new_upload_directory) - end - - def starting_with_base_directory - arel_table[:path].matches("#{base_directory}/%") - end - - def starting_with_new_upload_directory - arel_table[:path].matches("#{new_upload_dir}/%") - end - - def affected_uploads - arel_table[:model_type].in(AFFECTED_MODELS) - end - - def base_directory - "uploads" - end - - def new_upload_dir - File.join(base_directory, "system") - end - - def arel_table - Arel::Table.new(:uploads) - end -end diff --git a/spec/migrations/update_upload_paths_to_system_spec.rb b/spec/migrations/update_upload_paths_to_system_spec.rb deleted file mode 100644 index 11412005b72..00000000000 --- a/spec/migrations/update_upload_paths_to_system_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -require "spec_helper" -require Rails.root.join("db", "post_migrate", "20170317162059_update_upload_paths_to_system.rb") - -describe UpdateUploadPathsToSystem do - let(:migration) { described_class.new } - - before do - allow(migration).to receive(:say) - end - - describe "#uploads_to_switch_to_new_path" do - it "contains only uploads with the old path for the correct models" do - _upload_for_other_type = create(:upload, model: create(:ci_pipeline), path: "uploads/ci_pipeline/avatar.jpg") - _upload_with_system_path = create(:upload, model: create(:project), path: "uploads/system/project/avatar.jpg") - _upload_with_other_path = create(:upload, model: create(:project), path: "thelongsecretforafileupload/avatar.jpg") - old_upload = create(:upload, model: create(:project), path: "uploads/project/avatar.jpg") - group_upload = create(:upload, model: create(:group), path: "uploads/group/avatar.jpg") - - expect(Upload.where(migration.uploads_to_switch_to_new_path)).to contain_exactly(old_upload, group_upload) - end - end - - describe "#uploads_to_switch_to_old_path" do - it "contains only uploads with the new path for the correct models" do - _upload_for_other_type = create(:upload, model: create(:ci_pipeline), path: "uploads/ci_pipeline/avatar.jpg") - upload_with_system_path = create(:upload, model: create(:project), path: "uploads/system/project/avatar.jpg") - _upload_with_other_path = create(:upload, model: create(:project), path: "thelongsecretforafileupload/avatar.jpg") - _old_upload = create(:upload, model: create(:project), path: "uploads/project/avatar.jpg") - - expect(Upload.where(migration.uploads_to_switch_to_old_path)).to contain_exactly(upload_with_system_path) - end - end - - describe "#up", truncate: true do - it "updates old upload records to the new path" do - old_upload = create(:upload, model: create(:project), path: "uploads/project/avatar.jpg") - - migration.up - - expect(old_upload.reload.path).to eq("uploads/system/project/avatar.jpg") - end - end - - describe "#down", truncate: true do - it "updates the new system patsh to the old paths" do - new_upload = create(:upload, model: create(:project), path: "uploads/system/project/avatar.jpg") - - migration.down - - expect(new_upload.reload.path).to eq("uploads/project/avatar.jpg") - end - end -end From b8ae15397f0f21b87ea2f91efb470e7e51ba8964 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 2 Aug 2017 15:42:36 +0200 Subject: [PATCH 046/141] Update migrations to move directly into the `-/system` folder --- ...170316163845_move_uploads_to_system_dir.rb | 2 +- ...0170717074009_move_system_upload_folder.rb | 10 ++++ ...317162059_update_upload_paths_to_system.rb | 57 +++++++++++++++++++ .../20170406111121_clean_upload_symlinks.rb | 2 +- ...606202615_move_appearance_to_system_dir.rb | 2 +- ...0612071012_move_personal_snippets_files.rb | 4 +- spec/migrations/clean_upload_symlinks_spec.rb | 2 +- .../move_personal_snippets_files_spec.rb | 2 +- .../move_system_upload_folder_spec.rb | 18 ++++++ .../move_uploads_to_system_dir_spec.rb | 2 +- .../update_upload_paths_to_system_spec.rb | 53 +++++++++++++++++ 11 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 db/post_migrate/20170317162059_update_upload_paths_to_system.rb create mode 100644 spec/migrations/update_upload_paths_to_system_spec.rb diff --git a/db/migrate/20170316163845_move_uploads_to_system_dir.rb b/db/migrate/20170316163845_move_uploads_to_system_dir.rb index 564ee10b5ab..cfcb909ddaf 100644 --- a/db/migrate/20170316163845_move_uploads_to_system_dir.rb +++ b/db/migrate/20170316163845_move_uploads_to_system_dir.rb @@ -54,6 +54,6 @@ class MoveUploadsToSystemDir < ActiveRecord::Migration end def new_upload_dir - File.join(base_directory, "public", "uploads", "system") + File.join(base_directory, "public", "uploads", "-", "system") end end diff --git a/db/migrate/20170717074009_move_system_upload_folder.rb b/db/migrate/20170717074009_move_system_upload_folder.rb index cce31794115..d3caa53a7a4 100644 --- a/db/migrate/20170717074009_move_system_upload_folder.rb +++ b/db/migrate/20170717074009_move_system_upload_folder.rb @@ -15,6 +15,11 @@ class MoveSystemUploadFolder < ActiveRecord::Migration return end + if File.directory?(new_directory) + say "#{new_directory} already exists. No need to redo the move." + return + end + FileUtils.mkdir_p(File.join(base_directory, '-')) say "Moving #{old_directory} -> #{new_directory}" @@ -33,6 +38,11 @@ class MoveSystemUploadFolder < ActiveRecord::Migration return end + if !File.symlink?(old_directory) && File.directory?(old_directory) + say "#{old_directory} already exists and is not a symlink, no need to revert." + return + end + if File.symlink?(old_directory) say "Removing #{old_directory} -> #{new_directory} symlink" FileUtils.rm(old_directory) diff --git a/db/post_migrate/20170317162059_update_upload_paths_to_system.rb b/db/post_migrate/20170317162059_update_upload_paths_to_system.rb new file mode 100644 index 00000000000..92e33848bf0 --- /dev/null +++ b/db/post_migrate/20170317162059_update_upload_paths_to_system.rb @@ -0,0 +1,57 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class UpdateUploadPathsToSystem < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + AFFECTED_MODELS = %w(User Project Note Namespace Appearance) + + disable_ddl_transaction! + + def up + update_column_in_batches(:uploads, :path, replace_sql(arel_table[:path], base_directory, new_upload_dir)) do |_table, query| + query.where(uploads_to_switch_to_new_path) + end + end + + def down + update_column_in_batches(:uploads, :path, replace_sql(arel_table[:path], new_upload_dir, base_directory)) do |_table, query| + query.where(uploads_to_switch_to_old_path) + end + end + + # "SELECT \"uploads\".* FROM \"uploads\" WHERE \"uploads\".\"model_type\" IN ('User', 'Project', 'Note', 'Namespace', 'Appearance') AND (\"uploads\".\"path\" ILIKE 'uploads/%' AND NOT (\"uploads\".\"path\" ILIKE 'uploads/system/%'))" + def uploads_to_switch_to_new_path + affected_uploads.and(starting_with_base_directory).and(starting_with_new_upload_directory.not) + end + + # "SELECT \"uploads\".* FROM \"uploads\" WHERE \"uploads\".\"model_type\" IN ('User', 'Project', 'Note', 'Namespace', 'Appearance') AND (\"uploads\".\"path\" ILIKE 'uploads/%' AND \"uploads\".\"path\" ILIKE 'uploads/system/%')" + def uploads_to_switch_to_old_path + affected_uploads.and(starting_with_new_upload_directory) + end + + def starting_with_base_directory + arel_table[:path].matches("#{base_directory}/%") + end + + def starting_with_new_upload_directory + arel_table[:path].matches("#{new_upload_dir}/%") + end + + def affected_uploads + arel_table[:model_type].in(AFFECTED_MODELS) + end + + def base_directory + "uploads" + end + + def new_upload_dir + File.join(base_directory, "-", "system") + end + + def arel_table + Arel::Table.new(:uploads) + end +end diff --git a/db/post_migrate/20170406111121_clean_upload_symlinks.rb b/db/post_migrate/20170406111121_clean_upload_symlinks.rb index fc3a4acc0bb..f2ce25d4524 100644 --- a/db/post_migrate/20170406111121_clean_upload_symlinks.rb +++ b/db/post_migrate/20170406111121_clean_upload_symlinks.rb @@ -47,6 +47,6 @@ class CleanUploadSymlinks < ActiveRecord::Migration end def new_upload_dir - File.join(base_directory, "public", "uploads", "system") + File.join(base_directory, "public", "uploads", "-", "system") end end diff --git a/db/post_migrate/20170606202615_move_appearance_to_system_dir.rb b/db/post_migrate/20170606202615_move_appearance_to_system_dir.rb index 561de59ec69..07935ab8a52 100644 --- a/db/post_migrate/20170606202615_move_appearance_to_system_dir.rb +++ b/db/post_migrate/20170606202615_move_appearance_to_system_dir.rb @@ -52,6 +52,6 @@ class MoveAppearanceToSystemDir < ActiveRecord::Migration end def new_upload_dir - File.join(base_directory, "public", "uploads", "system") + File.join(base_directory, "public", "uploads", "-", "system") end end diff --git a/db/post_migrate/20170612071012_move_personal_snippets_files.rb b/db/post_migrate/20170612071012_move_personal_snippets_files.rb index 33043364bde..2b79a87ccd8 100644 --- a/db/post_migrate/20170612071012_move_personal_snippets_files.rb +++ b/db/post_migrate/20170612071012_move_personal_snippets_files.rb @@ -10,7 +10,7 @@ class MovePersonalSnippetsFiles < ActiveRecord::Migration return unless file_storage? @source_relative_location = File.join('/uploads', 'personal_snippet') - @destination_relative_location = File.join('/uploads', 'system', 'personal_snippet') + @destination_relative_location = File.join('/uploads', '-', 'system', 'personal_snippet') move_personal_snippet_files end @@ -18,7 +18,7 @@ class MovePersonalSnippetsFiles < ActiveRecord::Migration def down return unless file_storage? - @source_relative_location = File.join('/uploads', 'system', 'personal_snippet') + @source_relative_location = File.join('/uploads', '-', 'system', 'personal_snippet') @destination_relative_location = File.join('/uploads', 'personal_snippet') move_personal_snippet_files diff --git a/spec/migrations/clean_upload_symlinks_spec.rb b/spec/migrations/clean_upload_symlinks_spec.rb index cecb3ddac53..26653b9c008 100644 --- a/spec/migrations/clean_upload_symlinks_spec.rb +++ b/spec/migrations/clean_upload_symlinks_spec.rb @@ -5,7 +5,7 @@ describe CleanUploadSymlinks do let(:migration) { described_class.new } let(:test_dir) { File.join(Rails.root, "tmp", "tests", "move_uploads_test") } let(:uploads_dir) { File.join(test_dir, "public", "uploads") } - let(:new_uploads_dir) { File.join(uploads_dir, "system") } + let(:new_uploads_dir) { File.join(uploads_dir, "-", "system") } let(:original_path) { File.join(new_uploads_dir, 'user') } let(:symlink_path) { File.join(uploads_dir, 'user') } diff --git a/spec/migrations/move_personal_snippets_files_spec.rb b/spec/migrations/move_personal_snippets_files_spec.rb index 8505c7bf3e3..c17e453fe68 100644 --- a/spec/migrations/move_personal_snippets_files_spec.rb +++ b/spec/migrations/move_personal_snippets_files_spec.rb @@ -5,7 +5,7 @@ describe MovePersonalSnippetsFiles do let(:migration) { described_class.new } let(:test_dir) { File.join(Rails.root, "tmp", "tests", "move_snippet_files_test") } let(:uploads_dir) { File.join(test_dir, 'uploads') } - let(:new_uploads_dir) { File.join(uploads_dir, 'system') } + let(:new_uploads_dir) { File.join(uploads_dir, '-', 'system') } before do allow(CarrierWave).to receive(:root).and_return(test_dir) diff --git a/spec/migrations/move_system_upload_folder_spec.rb b/spec/migrations/move_system_upload_folder_spec.rb index b622b4e9536..d3180477db3 100644 --- a/spec/migrations/move_system_upload_folder_spec.rb +++ b/spec/migrations/move_system_upload_folder_spec.rb @@ -33,6 +33,15 @@ describe MoveSystemUploadFolder do expect(File.symlink?(File.join(test_base, 'system'))).to be_truthy expect(File.exist?(File.join(test_base, 'system', 'file'))).to be_truthy end + + it 'does not move if the target directory already exists' do + FileUtils.mkdir_p(File.join(test_base, '-', 'system')) + + expect(FileUtils).not_to receive(:mv) + expect(migration).to receive(:say).with(/already exists. No need to redo the move/) + + migration.up + end end describe '#down' do @@ -58,5 +67,14 @@ describe MoveSystemUploadFolder do expect(File.directory?(File.join(test_base, 'system'))).to be_truthy expect(File.symlink?(File.join(test_base, 'system'))).to be_falsey end + + it 'does not move if the old directory already exists' do + FileUtils.mkdir_p(File.join(test_base, 'system')) + + expect(FileUtils).not_to receive(:mv) + expect(migration).to receive(:say).with(/already exists and is not a symlink, no need to revert/) + + migration.down + end end end diff --git a/spec/migrations/move_uploads_to_system_dir_spec.rb b/spec/migrations/move_uploads_to_system_dir_spec.rb index 37d66452447..ca11a2004c5 100644 --- a/spec/migrations/move_uploads_to_system_dir_spec.rb +++ b/spec/migrations/move_uploads_to_system_dir_spec.rb @@ -5,7 +5,7 @@ describe MoveUploadsToSystemDir do let(:migration) { described_class.new } let(:test_dir) { File.join(Rails.root, "tmp", "move_uploads_test") } let(:uploads_dir) { File.join(test_dir, "public", "uploads") } - let(:new_uploads_dir) { File.join(uploads_dir, "system") } + let(:new_uploads_dir) { File.join(uploads_dir, "-", "system") } before do FileUtils.remove_dir(test_dir) if File.directory?(test_dir) diff --git a/spec/migrations/update_upload_paths_to_system_spec.rb b/spec/migrations/update_upload_paths_to_system_spec.rb new file mode 100644 index 00000000000..0a45c5ea32d --- /dev/null +++ b/spec/migrations/update_upload_paths_to_system_spec.rb @@ -0,0 +1,53 @@ +require "spec_helper" +require Rails.root.join("db", "post_migrate", "20170317162059_update_upload_paths_to_system.rb") + +describe UpdateUploadPathsToSystem do + let(:migration) { described_class.new } + + before do + allow(migration).to receive(:say) + end + + describe "#uploads_to_switch_to_new_path" do + it "contains only uploads with the old path for the correct models" do + _upload_for_other_type = create(:upload, model: create(:ci_pipeline), path: "uploads/ci_pipeline/avatar.jpg") + _upload_with_system_path = create(:upload, model: create(:project), path: "uploads/-/system/project/avatar.jpg") + _upload_with_other_path = create(:upload, model: create(:project), path: "thelongsecretforafileupload/avatar.jpg") + old_upload = create(:upload, model: create(:project), path: "uploads/project/avatar.jpg") + group_upload = create(:upload, model: create(:group), path: "uploads/group/avatar.jpg") + + expect(Upload.where(migration.uploads_to_switch_to_new_path)).to contain_exactly(old_upload, group_upload) + end + end + + describe "#uploads_to_switch_to_old_path" do + it "contains only uploads with the new path for the correct models" do + _upload_for_other_type = create(:upload, model: create(:ci_pipeline), path: "uploads/ci_pipeline/avatar.jpg") + upload_with_system_path = create(:upload, model: create(:project), path: "uploads/-/system/project/avatar.jpg") + _upload_with_other_path = create(:upload, model: create(:project), path: "thelongsecretforafileupload/avatar.jpg") + _old_upload = create(:upload, model: create(:project), path: "uploads/project/avatar.jpg") + + expect(Upload.where(migration.uploads_to_switch_to_old_path)).to contain_exactly(upload_with_system_path) + end + end + + describe "#up", truncate: true do + it "updates old upload records to the new path" do + old_upload = create(:upload, model: create(:project), path: "uploads/project/avatar.jpg") + + migration.up + + expect(old_upload.reload.path).to eq("uploads/-/system/project/avatar.jpg") + end + end + + describe "#down", truncate: true do + it "updates the new system patsh to the old paths" do + new_upload = create(:upload, model: create(:project), path: "uploads/-/system/project/avatar.jpg") + + migration.down + + expect(new_upload.reload.path).to eq("uploads/project/avatar.jpg") + end + end +end From 180de2d20127f79773bf661f88cd7556b191d0b9 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Mon, 7 Aug 2017 19:43:11 +0200 Subject: [PATCH 047/141] Make sure uploads for personal snippets are correctly rendered --- app/uploaders/personal_file_uploader.rb | 2 +- config/routes/uploads.rb | 4 ++-- spec/controllers/snippets_controller_spec.rb | 8 ++++---- spec/controllers/uploads_controller_spec.rb | 4 ++-- .../features/snippets/user_creates_snippet_spec.rb | 6 +++--- spec/features/snippets/user_edits_snippet_spec.rb | 2 +- spec/uploaders/file_mover_spec.rb | 14 +++++++------- spec/uploaders/personal_file_uploader_spec.rb | 4 ++-- 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/app/uploaders/personal_file_uploader.rb b/app/uploaders/personal_file_uploader.rb index ef70871624b..3298ad104ec 100644 --- a/app/uploaders/personal_file_uploader.rb +++ b/app/uploaders/personal_file_uploader.rb @@ -4,7 +4,7 @@ class PersonalFileUploader < FileUploader end def self.base_dir - File.join(root_dir, 'system') + File.join(root_dir, '-', 'system') end private diff --git a/config/routes/uploads.rb b/config/routes/uploads.rb index e9c9aa8b2f9..d7bca8310e4 100644 --- a/config/routes/uploads.rb +++ b/config/routes/uploads.rb @@ -5,12 +5,12 @@ scope path: :uploads do constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ } # show uploads for models, snippets (notes) available for now - get 'system/:model/:id/:secret/:filename', + get '-/system/:model/:id/:secret/:filename', to: 'uploads#show', constraints: { model: /personal_snippet/, id: /\d+/, filename: /[^\/]+/ } # show temporary uploads - get 'system/temp/:secret/:filename', + get '-/system/temp/:secret/:filename', to: 'uploads#show', constraints: { filename: /[^\/]+/ } diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index 475ceda11fe..7c5d059760f 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -186,8 +186,8 @@ describe SnippetsController do end context 'when the snippet description contains a file' do - let(:picture_file) { '/system/temp/secret56/picture.jpg' } - let(:text_file) { '/system/temp/secret78/text.txt' } + let(:picture_file) { '/-/system/temp/secret56/picture.jpg' } + let(:text_file) { '/-/system/temp/secret78/text.txt' } let(:description) do "Description with picture: ![picture](/uploads#{picture_file}) and "\ "text: [text.txt](/uploads#{text_file})" @@ -208,8 +208,8 @@ describe SnippetsController do snippet = subject expected_description = "Description with picture: "\ - "![picture](/uploads/system/personal_snippet/#{snippet.id}/secret56/picture.jpg) and "\ - "text: [text.txt](/uploads/system/personal_snippet/#{snippet.id}/secret78/text.txt)" + "![picture](/uploads/-/system/personal_snippet/#{snippet.id}/secret56/picture.jpg) and "\ + "text: [text.txt](/uploads/-/system/personal_snippet/#{snippet.id}/secret78/text.txt)" expect(snippet.description).to eq(expected_description) end diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index b3a40f5d15c..b29f3d861be 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -102,7 +102,7 @@ describe UploadsController do subject expect(response.body).to match '\"alt\":\"rails_sample\"' - expect(response.body).to match "\"url\":\"/uploads/system/temp" + expect(response.body).to match "\"url\":\"/uploads/-/system/temp" end it 'does not create an Upload record' do @@ -119,7 +119,7 @@ describe UploadsController do subject expect(response.body).to match '\"alt\":\"doc_sample.txt\"' - expect(response.body).to match "\"url\":\"/uploads/system/temp" + expect(response.body).to match "\"url\":\"/uploads/-/system/temp" end it 'does not create an Upload record' do diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb index a919f5fa20b..d732383a1e1 100644 --- a/spec/features/snippets/user_creates_snippet_spec.rb +++ b/spec/features/snippets/user_creates_snippet_spec.rb @@ -41,7 +41,7 @@ feature 'User creates snippet', :js do expect(page).to have_content('My Snippet') link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] - expect(link).to match(%r{/uploads/system/temp/\h{32}/banana_sample\.gif\z}) + expect(link).to match(%r{/uploads/-/system/temp/\h{32}/banana_sample\.gif\z}) visit(link) expect(page.status_code).to eq(200) @@ -59,7 +59,7 @@ feature 'User creates snippet', :js do wait_for_requests link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] - expect(link).to match(%r{/uploads/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z}) + expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z}) visit(link) expect(page.status_code).to eq(200) @@ -84,7 +84,7 @@ feature 'User creates snippet', :js do end expect(page).to have_content('Hello World!') link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] - expect(link).to match(%r{/uploads/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z}) + expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z}) visit(link) expect(page.status_code).to eq(200) diff --git a/spec/features/snippets/user_edits_snippet_spec.rb b/spec/features/snippets/user_edits_snippet_spec.rb index 26070e508e2..71de6b6bd1c 100644 --- a/spec/features/snippets/user_edits_snippet_spec.rb +++ b/spec/features/snippets/user_edits_snippet_spec.rb @@ -33,7 +33,7 @@ feature 'User edits snippet', :js do wait_for_requests link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] - expect(link).to match(%r{/uploads/system/personal_snippet/#{snippet.id}/\h{32}/banana_sample\.gif\z}) + expect(link).to match(%r{/uploads/-/system/personal_snippet/#{snippet.id}/\h{32}/banana_sample\.gif\z}) end it 'updates the snippet to make it internal' do diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb index d7c1b390f9a..0cf462e9553 100644 --- a/spec/uploaders/file_mover_spec.rb +++ b/spec/uploaders/file_mover_spec.rb @@ -4,11 +4,11 @@ describe FileMover do let(:filename) { 'banana_sample.gif' } let(:file) { fixture_file_upload(Rails.root.join('spec', 'fixtures', filename)) } let(:temp_description) do - 'test ![banana_sample](/uploads/system/temp/secret55/banana_sample.gif) same ![banana_sample]'\ - '(/uploads/system/temp/secret55/banana_sample.gif)' + 'test ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif) same ![banana_sample]'\ + '(/uploads/-/system/temp/secret55/banana_sample.gif)' end let(:temp_file_path) { File.join('secret55', filename).to_s } - let(:file_path) { File.join('uploads', 'system', 'personal_snippet', snippet.id.to_s, 'secret55', filename).to_s } + let(:file_path) { File.join('uploads', '-', 'system', 'personal_snippet', snippet.id.to_s, 'secret55', filename).to_s } let(:snippet) { create(:personal_snippet, description: temp_description) } @@ -28,8 +28,8 @@ describe FileMover do expect(snippet.reload.description) .to eq( - "test ![banana_sample](/uploads/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)"\ - " same ![banana_sample](/uploads/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)" + "test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)"\ + " same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)" ) end @@ -50,8 +50,8 @@ describe FileMover do expect(snippet.reload.description) .to eq( - "test ![banana_sample](/uploads/system/temp/secret55/banana_sample.gif)"\ - " same ![banana_sample](/uploads/system/temp/secret55/banana_sample.gif)" + "test ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif)"\ + " same ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif)" ) end diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb index e505edc75ce..cbafa9f478d 100644 --- a/spec/uploaders/personal_file_uploader_spec.rb +++ b/spec/uploaders/personal_file_uploader_spec.rb @@ -10,7 +10,7 @@ describe PersonalFileUploader do dynamic_segment = "personal_snippet/#{snippet.id}" - expect(described_class.absolute_path(upload)).to end_with("/system/#{dynamic_segment}/secret/foo.jpg") + expect(described_class.absolute_path(upload)).to end_with("/-/system/#{dynamic_segment}/secret/foo.jpg") end end @@ -19,7 +19,7 @@ describe PersonalFileUploader do uploader = described_class.new(snippet, 'secret') allow(uploader).to receive(:file).and_return(double(extension: 'txt', filename: 'file_name')) - expected_url = "/uploads/system/personal_snippet/#{snippet.id}/secret/file_name" + expected_url = "/uploads/-/system/personal_snippet/#{snippet.id}/secret/file_name" expect(uploader.to_h).to eq( alt: 'file_name', From 2ea8442ff3398a788b1005a825c1d13f61f91c2d Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Mon, 7 Aug 2017 20:01:45 +0200 Subject: [PATCH 048/141] Move the personal snippet uploads from `system` to `-/system` Update the markdown unconditionally since the move might have been done before, but the markdown not updated. --- ...sonal_snippet_files_into_correct_folder.rb | 29 +++++++ .../move_personal_snippet_files.rb | 79 +++++++++++++++++++ .../move_personal_snippet_files_spec.rb | 72 +++++++++++++++++ .../move_personal_snippets_files_spec.rb | 8 +- 4 files changed, 184 insertions(+), 4 deletions(-) create mode 100644 db/post_migrate/20170807190736_move_personal_snippet_files_into_correct_folder.rb create mode 100644 lib/gitlab/background_migration/move_personal_snippet_files.rb create mode 100644 spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb diff --git a/db/post_migrate/20170807190736_move_personal_snippet_files_into_correct_folder.rb b/db/post_migrate/20170807190736_move_personal_snippet_files_into_correct_folder.rb new file mode 100644 index 00000000000..e3d2446b897 --- /dev/null +++ b/db/post_migrate/20170807190736_move_personal_snippet_files_into_correct_folder.rb @@ -0,0 +1,29 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MovePersonalSnippetFilesIntoCorrectFolder < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + DOWNTIME = false + NEW_DIRECTORY = File.join('/uploads', '-', 'system', 'personal_snippet') + OLD_DIRECTORY = File.join('/uploads', 'system', 'personal_snippet') + + def up + return unless file_storage? + + BackgroundMigrationWorker.perform_async('MovePersonalSnippetFiles', + [OLD_DIRECTORY, NEW_DIRECTORY]) + end + + def down + return unless file_storage? + + BackgroundMigrationWorker.perform_async('MovePersonalSnippetFiles', + [NEW_DIRECTORY, OLD_DIRECTORY]) + end + + def file_storage? + CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File + end +end diff --git a/lib/gitlab/background_migration/move_personal_snippet_files.rb b/lib/gitlab/background_migration/move_personal_snippet_files.rb new file mode 100644 index 00000000000..07cec96bcc3 --- /dev/null +++ b/lib/gitlab/background_migration/move_personal_snippet_files.rb @@ -0,0 +1,79 @@ +module Gitlab + module BackgroundMigration + class MovePersonalSnippetFiles + delegate :select_all, :execute, :quote_string, to: :connection + + def perform(relative_source, relative_destination) + @source_relative_location = relative_source + @destination_relative_location = relative_destination + + move_personal_snippet_files + end + + def move_personal_snippet_files + query = "SELECT uploads.path, uploads.model_id FROM uploads "\ + "INNER JOIN snippets ON snippets.id = uploads.model_id WHERE uploader = 'PersonalFileUploader'" + select_all(query).each do |upload| + secret = upload['path'].split('/')[0] + file_name = upload['path'].split('/')[1] + + move_file(upload['model_id'], secret, file_name) + update_markdown(upload['model_id'], secret, file_name) + end + end + + def move_file(snippet_id, secret, file_name) + source_dir = File.join(base_directory, @source_relative_location, snippet_id.to_s, secret) + destination_dir = File.join(base_directory, @destination_relative_location, snippet_id.to_s, secret) + + source_file_path = File.join(source_dir, file_name) + destination_file_path = File.join(destination_dir, file_name) + + unless File.exist?(source_file_path) + say "Source file `#{source_file_path}` doesn't exist. Skipping." + return + end + + say "Moving file #{source_file_path} -> #{destination_file_path}" + + FileUtils.mkdir_p(destination_dir) + FileUtils.move(source_file_path, destination_file_path) + end + + def update_markdown(snippet_id, secret, file_name) + source_markdown_path = File.join(@source_relative_location, snippet_id.to_s, secret, file_name) + destination_markdown_path = File.join(@destination_relative_location, snippet_id.to_s, secret, file_name) + + source_markdown = "](#{source_markdown_path})" + destination_markdown = "](#{destination_markdown_path})" + quoted_source = quote_string(source_markdown) + quoted_destination = quote_string(destination_markdown) + + execute("UPDATE snippets "\ + "SET description = replace(snippets.description, '#{quoted_source}', '#{quoted_destination}'), description_html = NULL "\ + "WHERE id = #{snippet_id}") + + query = "SELECT id, note FROM notes WHERE noteable_id = #{snippet_id} "\ + "AND noteable_type = 'Snippet' AND note IS NOT NULL" + select_all(query).each do |note| + text = note['note'].gsub(source_markdown, destination_markdown) + quoted_text = quote_string(text) + + execute("UPDATE notes SET note = '#{quoted_text}', note_html = NULL WHERE id = #{note['id']}") + end + end + + def base_directory + File.join(Rails.root, 'public') + end + + def connection + ActiveRecord::Base.connection + end + + def say(message) + Rails.logger.debug(message) + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb b/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb new file mode 100644 index 00000000000..ee60e498b59 --- /dev/null +++ b/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::MovePersonalSnippetFiles do + let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'move_snippet_files_test') } + let(:old_uploads_dir) { File.join('uploads', 'system', 'personal_snippet') } + let(:new_uploads_dir) { File.join('uploads', '-', 'system', 'personal_snippet') } + let(:snippet) do + snippet = create(:personal_snippet) + create_upload_for_snippet(snippet) + snippet.update_attributes!(description: markdown_linking_file(snippet)) + snippet + end + + let(:migration) { described_class.new } + + before do + allow(migration).to receive(:base_directory) { test_dir } + end + + describe '#perform' do + it 'moves the file on the disk' do + expected_path = File.join(test_dir, new_uploads_dir, snippet.id.to_s, "secret#{snippet.id}", 'upload.txt') + + migration.perform(old_uploads_dir, new_uploads_dir) + + expect(File.exist?(expected_path)).to be_truthy + end + + it 'updates the markdown of the snippet' do + expected_path = File.join(new_uploads_dir, snippet.id.to_s, "secret#{snippet.id}", 'upload.txt') + expected_markdown = "[an upload](#{expected_path})" + + migration.perform(old_uploads_dir, new_uploads_dir) + + expect(snippet.reload.description).to eq(expected_markdown) + end + + it 'updates the markdown of notes' do + expected_path = File.join(new_uploads_dir, snippet.id.to_s, "secret#{snippet.id}", 'upload.txt') + expected_markdown = "with [an upload](#{expected_path})" + + note = create(:note_on_personal_snippet, noteable: snippet, note: "with #{markdown_linking_file(snippet)}") + + migration.perform(old_uploads_dir, new_uploads_dir) + + expect(note.reload.note).to eq(expected_markdown) + end + end + + def create_upload_for_snippet(snippet) + snippet_path = path_for_file_in_snippet(snippet) + path = File.join(old_uploads_dir, snippet.id.to_s, snippet_path) + absolute_path = File.join(test_dir, path) + + FileUtils.mkdir_p(File.dirname(absolute_path)) + FileUtils.touch(absolute_path) + + create(:upload, model: snippet, path: snippet_path, uploader: PersonalFileUploader) + end + + def path_for_file_in_snippet(snippet) + secret = "secret#{snippet.id}" + filename = 'upload.txt' + + File.join(secret, filename) + end + + def markdown_linking_file(snippet) + path = File.join(old_uploads_dir, snippet.id.to_s, path_for_file_in_snippet(snippet)) + "[an upload](#{path})" + end +end diff --git a/spec/migrations/move_personal_snippets_files_spec.rb b/spec/migrations/move_personal_snippets_files_spec.rb index c17e453fe68..1a319eccc0d 100644 --- a/spec/migrations/move_personal_snippets_files_spec.rb +++ b/spec/migrations/move_personal_snippets_files_spec.rb @@ -42,7 +42,7 @@ describe MovePersonalSnippetsFiles do describe 'updating the markdown' do it 'includes the new path when the file exists' do secret = "secret#{snippet.id}" - file_location = "/uploads/system/personal_snippet/#{snippet.id}/#{secret}/picture.jpg" + file_location = "/uploads/-/system/personal_snippet/#{snippet.id}/#{secret}/picture.jpg" migration.up @@ -60,7 +60,7 @@ describe MovePersonalSnippetsFiles do it 'updates the note markdown' do secret = "secret#{snippet.id}" - file_location = "/uploads/system/personal_snippet/#{snippet.id}/#{secret}/picture.jpg" + file_location = "/uploads/-/system/personal_snippet/#{snippet.id}/#{secret}/picture.jpg" markdown = markdown_linking_file('picture.jpg', snippet) note = create(:note_on_personal_snippet, noteable: snippet, note: "with #{markdown}") @@ -108,7 +108,7 @@ describe MovePersonalSnippetsFiles do it 'keeps the markdown as is when the file is missing' do secret = "secret#{snippet_with_missing_file.id}" - file_location = "/uploads/system/personal_snippet/#{snippet_with_missing_file.id}/#{secret}/picture.jpg" + file_location = "/uploads/-/system/personal_snippet/#{snippet_with_missing_file.id}/#{secret}/picture.jpg" migration.down @@ -167,7 +167,7 @@ describe MovePersonalSnippetsFiles do def markdown_linking_file(filename, snippet, in_new_path: false) markdown = "![#{filename.split('.')[0]}]" markdown += '(/uploads' - markdown += '/system' if in_new_path + markdown += '/-/system' if in_new_path markdown += "/#{model_file_path(filename, snippet)})" markdown end From 64073185adcb3eec40eda05e11f9bf47f646bf9d Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 11 Aug 2017 13:27:38 -0400 Subject: [PATCH 049/141] Remove `username` from `User#sanitize_attrs` callback This attribute is since validated against `DynamicPathValidator`, which has strict requirements for the characters allowed, and should no longer need to be sanitized in a callback before saving. This has additional benefits in our test suite, where every creation of a `User` record was calling `Sanitize.clean` on a username value that was always clean, since we're the ones generating it. --- app/models/user.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 7935b89662b..42a1ac40c6c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -726,9 +726,9 @@ class User < ActiveRecord::Base end def sanitize_attrs - %w[username skype linkedin twitter].each do |attr| - value = public_send(attr) # rubocop:disable GitlabSecurity/PublicSend - public_send("#{attr}=", Sanitize.clean(value)) if value.present? # rubocop:disable GitlabSecurity/PublicSend + %i[skype linkedin twitter].each do |attr| + value = self[attr] + self[attr] = Sanitize.clean(value) if value.present? end end From 4e8a2feb718face21a6024540c7bfd2bccd6ea25 Mon Sep 17 00:00:00 2001 From: Mehdi Lahmam Date: Thu, 10 Aug 2017 08:41:41 +0200 Subject: [PATCH 050/141] Add feature specs for Cycle Analytics pipeline summary --- spec/features/cycle_analytics_spec.rb | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb index 5c60cca10b9..1e48cab98a4 100644 --- a/spec/features/cycle_analytics_spec.rb +++ b/spec/features/cycle_analytics_spec.rb @@ -24,6 +24,12 @@ feature 'Cycle Analytics', js: true do expect(page).to have_content('Introducing Cycle Analytics') end + it 'shows pipeline summary' do + expect(new_issues_counter).to have_content('-') + expect(commits_counter).to have_content('-') + expect(deploys_counter).to have_content('-') + end + it 'shows active stage with empty message' do expect(page).to have_selector('.stage-nav-item.active', text: 'Issue') expect(page).to have_content("We don't have enough data to show this stage.") @@ -42,6 +48,12 @@ feature 'Cycle Analytics', js: true do visit project_cycle_analytics_path(project) end + it 'shows pipeline summary' do + expect(new_issues_counter).to have_content('1') + expect(commits_counter).to have_content('2') + expect(deploys_counter).to have_content('1') + end + it 'shows data on each stage' do expect_issue_to_be_present @@ -109,6 +121,18 @@ feature 'Cycle Analytics', js: true do end end + def new_issues_counter + find(:xpath, "//p[contains(text(),'New Issue')]/preceding-sibling::h3") + end + + def commits_counter + find(:xpath, "//p[contains(text(),'Commits')]/preceding-sibling::h3") + end + + def deploys_counter + find(:xpath, "//p[contains(text(),'Deploy')]/preceding-sibling::h3") + end + def expect_issue_to_be_present expect(find('.stage-events')).to have_content(issue.title) expect(find('.stage-events')).to have_content(issue.author.name) From f531c92544e4e69143df394be7b98c404fda578e Mon Sep 17 00:00:00 2001 From: Victor Wu Date: Fri, 11 Aug 2017 20:01:46 +0000 Subject: [PATCH 051/141] JIRA docs --- doc/user/project/integrations/jira.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md index 4f583879a4e..93aec56f8dc 100644 --- a/doc/user/project/integrations/jira.md +++ b/doc/user/project/integrations/jira.md @@ -10,7 +10,12 @@ JIRA](https://www.programmableweb.com/news/how-and-why-to-integrate-gitlab-jira/ ## Configuration -Each GitLab project can be configured to connect to a different JIRA instance. +Each GitLab project can be configured to connect to a different JIRA instance. That +means one GitLab project maps to _all_ JIRA projects in that JIRA instance once +the configuration is set up. Therefore, you don't have to explicitly associate +one GitLab project to any JIRA project. Once the configuration is set up, any JIRA +projects in the JIRA instance are already mapped to the GitLab project. + If you have one JIRA instance you can pre-fill the settings page with a default template, see the [Services Templates][services-templates] docs. @@ -103,7 +108,6 @@ in the table below. | ----- | ----------- | | `Web URL` | The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., `https://jira.example.com`. | | `JIRA API URL` | The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. | -| `Project key` | Put a JIRA project key (in uppercase), e.g. `MARS` in this field. This is only for testing the configuration settings. JIRA integration in GitLab works with _all_ JIRA projects in your JIRA instance. This field will be removed in a future release. | | `Username` | The user name created in [configuring JIRA step](#configuring-jira). | | `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | | `Transition ID` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** | From 6e3ca79dedbe27f64d12dbf391d0823137dcc610 Mon Sep 17 00:00:00 2001 From: Mehdi Lahmam Date: Thu, 10 Aug 2017 08:42:28 +0200 Subject: [PATCH 052/141] Add a `Last 7 days` option for Cycle Analytics view --- app/controllers/concerns/cycle_analytics_params.rb | 9 ++++++++- app/views/projects/cycle_analytics/show.html.haml | 3 +++ .../unreleased/seven-days-cycle-analytics.yml | 5 +++++ spec/features/cycle_analytics_spec.rb | 14 ++++++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/seven-days-cycle-analytics.yml diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb index 52e06f4945a..1ab107168c0 100644 --- a/app/controllers/concerns/cycle_analytics_params.rb +++ b/app/controllers/concerns/cycle_analytics_params.rb @@ -6,6 +6,13 @@ module CycleAnalyticsParams end def start_date(params) - params[:start_date] == '30' ? 30.days.ago : 90.days.ago + case params[:start_date] + when '7' + 7.days.ago + when '30' + 30.days.ago + else + 90.days.ago + end end end diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index c704635ead3..3467e357c49 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -39,6 +39,9 @@ %span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }} %i.fa.fa-chevron-down %ul.dropdown-menu.dropdown-menu-align-right + %li + %a{ "href" => "#", "data-value" => "7" } + {{ n__('Last %d day', 'Last %d days', 7) }} %li %a{ "href" => "#", "data-value" => "30" } {{ n__('Last %d day', 'Last %d days', 30) }} diff --git a/changelogs/unreleased/seven-days-cycle-analytics.yml b/changelogs/unreleased/seven-days-cycle-analytics.yml new file mode 100644 index 00000000000..ff660bdd603 --- /dev/null +++ b/changelogs/unreleased/seven-days-cycle-analytics.yml @@ -0,0 +1,5 @@ +--- +title: Add a `Last 7 days` option for Cycle Analytics view +merge_request: 13443 +author: Mehdi Lahmam (@mehlah) +type: added diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb index 1e48cab98a4..bfe9dac3bd4 100644 --- a/spec/features/cycle_analytics_spec.rb +++ b/spec/features/cycle_analytics_spec.rb @@ -75,6 +75,20 @@ feature 'Cycle Analytics', js: true do click_stage('Production') expect_issue_to_be_present end + + context "when I change the time period observed" do + before do + _two_weeks_old_issue = create(:issue, project: project, created_at: 2.weeks.ago) + + click_button('Last 30 days') + click_link('Last 7 days') + wait_for_requests + end + + it 'shows only relevant data' do + expect(new_issues_counter).to have_content('1') + end + end end context "when my preferred language is Spanish" do From 9d9fdaf26c5f3147a76d9dfda008f62b894acedb Mon Sep 17 00:00:00 2001 From: Victor Wu Date: Fri, 11 Aug 2017 20:03:02 +0000 Subject: [PATCH 053/141] Replace jira_service_page.png --- .../integrations/img/jira_service_page.png | Bin 83466 -> 193364 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/user/project/integrations/img/jira_service_page.png b/doc/user/project/integrations/img/jira_service_page.png index e69376f74c4b267e52f969d5e79ae769bdfd53b5..63aa0e99a5055e19f0a5fe309b10b58c84a2d74a 100644 GIT binary patch literal 193364 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 D;k-zt literal 83466 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 C!$zw$}qZp#@$;#@H`KEPu#KeMmP| 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 z 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 HI& Date: Fri, 4 Aug 2017 11:53:17 -0700 Subject: [PATCH 054/141] make NotificationRecipient a little more customizable --- app/models/notification_recipient.rb | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index 418b42d8f1d..30fab33e5e0 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -5,12 +5,18 @@ class NotificationRecipient custom_action: nil, target: nil, acting_user: nil, - project: nil + project: nil, + group: nil ) + unless NotificationSetting.levels.key?(type) || type == :subscription + raise ArgumentError, "invalid type: #{type.inspect}" + end + @custom_action = custom_action @acting_user = acting_user @target = target - @project = project || @target&.project + @project = project || default_project + @group = group || @project&.group @user = user @type = type end @@ -111,12 +117,18 @@ class NotificationRecipient end end + def default_project + return nil if @target.nil? + return @target if @target.is_a?(Project) + return @target.project if @target.respond_to?(:project) + end + def find_notification_setting project_setting = @project && user.notification_settings_for(@project) return project_setting unless project_setting.nil? || project_setting.global? - group_setting = @project&.group && user.notification_settings_for(@project.group) + group_setting = @group && user.notification_settings_for(@group) return group_setting unless group_setting.nil? || group_setting.global? From d5054abfcc1b27c664bff46ae6dc1482c591e5a9 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Fri, 4 Aug 2017 11:53:36 -0700 Subject: [PATCH 055/141] add Member#notifiable?(type, opts) --- app/models/member.rb | 4 ++++ app/models/members/group_member.rb | 4 ++++ app/models/members/project_member.rb | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/app/models/member.rb b/app/models/member.rb index dc9247bc9a0..b5f75c9bff0 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -332,4 +332,8 @@ class Member < ActiveRecord::Base def notification_service NotificationService.new end + + def notifiable?(type, opts={}) + raise 'abstract' + end end diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 47040f95533..cf434ec070b 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -30,6 +30,10 @@ class GroupMember < Member 'Group' end + def notifiable?(type, opts={}) + NotificationRecipientService.notifiable?(user, type, { group: group }.merge(opts)) + end + private def send_invite diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index c0e17f4bfc8..47e47caad82 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -87,6 +87,10 @@ class ProjectMember < Member project.owner == user end + def notifiable?(type, opts={}) + NotificationRecipientService.notifiable?(user, type, { project: project }.merge(opts)) + end + private def delete_member_todos From 0268fc2ffc7cdc12a6e1a0bf565fe70ff8541398 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Fri, 4 Aug 2017 11:54:19 -0700 Subject: [PATCH 056/141] check notifiability for more emails --- app/services/notification_service.rb | 51 +++++++++++++++++++--- spec/services/notification_service_spec.rb | 36 +++++++++++---- 2 files changed, 72 insertions(+), 15 deletions(-) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index df04b1a4fe3..d92282ac896 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -10,9 +10,11 @@ class NotificationService # only if ssh key is not deploy key # # This is security email so it will be sent - # even if user disabled notifications + # even if user disabled notifications. However, + # it won't be sent to internal users like the + # ghost user or the EE support bot. def new_key(key) - if key.user + if key.user&.can?(:receive_notifications) mailer.new_ssh_key_email(key.id).deliver_later end end @@ -22,14 +24,14 @@ class NotificationService # This is a security email so it will be sent even if the user user disabled # notifications def new_gpg_key(gpg_key) - if gpg_key.user + if gpg_key.user&.can?(:receive_notifications) mailer.new_gpg_key_email(gpg_key.id).deliver_later end end # Always notify user about email added to profile def new_email(email) - if email.user + if email.user&.can?(:receive_notifications) mailer.new_email_email(email.id).deliver_later end end @@ -185,6 +187,8 @@ class NotificationService # Notify new user with email after creation def new_user(user, token = nil) + return true unless notifiable?(user, :mention) + # Don't email omniauth created users mailer.new_user_email(user.id, token).deliver_later unless user.identities.any? end @@ -206,19 +210,27 @@ class NotificationService # Members def new_access_request(member) + return true unless member.notifiable?(:subscription) + mailer.member_access_requested_email(member.real_source_type, member.id).deliver_later end def decline_access_request(member) + return true unless member.notifiable?(:subscription) + mailer.member_access_denied_email(member.real_source_type, member.source_id, member.user_id).deliver_later end # Project invite def invite_project_member(project_member, token) + return true unless project_member.notifiable?(:subscription) + mailer.member_invited_email(project_member.real_source_type, project_member.id, token).deliver_later end def accept_project_invite(project_member) + return true unless project_member.notifiable?(:subscription) + mailer.member_invite_accepted_email(project_member.real_source_type, project_member.id).deliver_later end @@ -232,10 +244,14 @@ class NotificationService end def new_project_member(project_member) + return true unless project_member.notifiable?(:mention) + mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later end def update_project_member(project_member) + return true unless project_member.notifiable?(:mention) + mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later end @@ -249,6 +265,9 @@ class NotificationService end def decline_group_invite(group_member) + # always send this one, since it's a response to the user's own + # action + mailer.member_invite_declined_email( group_member.real_source_type, group_member.group.id, @@ -258,15 +277,19 @@ class NotificationService end def new_group_member(group_member) + return true unless group_member.notifiable?(:mention) + mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later end def update_group_member(group_member) + return true unless group_member.notifiable?(:mention) + mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later end def project_was_moved(project, old_path_with_namespace) - recipients = NotificationRecipientService.notifiable_users(project.team.members, :mention, project: project) + recipients = notifiable_users(project.team.members, :mention, project: project) recipients.each do |recipient| mailer.project_was_moved_email( @@ -288,10 +311,14 @@ class NotificationService end def project_exported(project, current_user) + return true unless notifiable?(current_user, :mention, project: project) + mailer.project_was_exported_email(current_user, project).deliver_later end def project_not_exported(project, current_user, errors) + return true unless notifiable?(current_user, :mention, project: project) + mailer.project_was_not_exported_email(current_user, project, errors).deliver_later end @@ -300,7 +327,7 @@ class NotificationService return unless mailer.respond_to?(email_template) - recipients ||= NotificationRecipientService.notifiable_users( + recipients ||= notifiable_users( [pipeline.user], :watch, custom_action: :"#{pipeline.status}_pipeline", target: pipeline @@ -369,7 +396,7 @@ class NotificationService def relabeled_resource_email(target, labels, current_user, method) recipients = labels.flat_map { |l| l.subscribers(target.project) } - recipients = NotificationRecipientService.notifiable_users( + recipients = notifiable_users( recipients, :subscription, target: target, acting_user: current_user @@ -401,4 +428,14 @@ class NotificationService object.previous_changes[attribute].first end end + + private + + def notifiable?(*args) + NotificationRecipientService.notifiable?(*args) + end + + def notifiable_users(*args) + NotificationRecipientService.notifiable_users(*args) + end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 64981c199e4..b6110da83b5 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -1173,19 +1173,39 @@ describe NotificationService, :mailer do end end - describe '#project_exported' do - it do - notification.project_exported(project, @u_disabled) + context 'user with notifications disabled' do + describe '#project_exported' do + it do + notification.project_exported(project, @u_disabled) - should_only_email(@u_disabled) + should_not_email_anyone + end + end + + describe '#project_not_exported' do + it do + notification.project_not_exported(project, @u_disabled, ['error']) + + should_not_email_anyone + end end end - describe '#project_not_exported' do - it do - notification.project_not_exported(project, @u_disabled, ['error']) + context 'user with notifications enabled' do + describe '#project_exported' do + it do + notification.project_exported(project, @u_participating) - should_only_email(@u_disabled) + should_only_email(@u_participating) + end + end + + describe '#project_not_exported' do + it do + notification.project_not_exported(project, @u_participating, ['error']) + + should_only_email(@u_participating) + end end end end From 90dd3fb32c1205ddd31c1c2c89b297e9528e0da8 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Fri, 4 Aug 2017 13:56:33 -0700 Subject: [PATCH 057/141] a membership with no user is always notifiable since this is for user invites and the like. --- app/models/member.rb | 12 ++++++++++-- app/models/members/group_member.rb | 4 ++-- app/models/members/project_member.rb | 4 ++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/models/member.rb b/app/models/member.rb index b5f75c9bff0..57f85a9adaf 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -276,6 +276,14 @@ class Member < ActiveRecord::Base @notification_setting ||= user.notification_settings_for(source) end + def notifiable?(type, opts={}) + # always notify when there isn't a user yet + return true if user.blank? + + NotificationRecipientService.notifiable?(user, type, notifiable_options.merge(opts)) + end + + private def send_invite @@ -333,7 +341,7 @@ class Member < ActiveRecord::Base NotificationService.new end - def notifiable?(type, opts={}) - raise 'abstract' + def notifiable_options + {} end end diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index cf434ec070b..661e668dbf9 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -30,8 +30,8 @@ class GroupMember < Member 'Group' end - def notifiable?(type, opts={}) - NotificationRecipientService.notifiable?(user, type, { group: group }.merge(opts)) + def notifiable_options + { group: group } end private diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 47e47caad82..b6f1dd272cd 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -87,8 +87,8 @@ class ProjectMember < Member project.owner == user end - def notifiable?(type, opts={}) - NotificationRecipientService.notifiable?(user, type, { project: project }.merge(opts)) + def notifiable_options + { project: project } end private From 7416fbb398e21cf6931265f7f95f97f5fe73e187 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Mon, 7 Aug 2017 17:36:35 -0700 Subject: [PATCH 058/141] rubocop fix --- app/models/member.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/models/member.rb b/app/models/member.rb index 57f85a9adaf..b26b5017183 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -276,14 +276,13 @@ class Member < ActiveRecord::Base @notification_setting ||= user.notification_settings_for(source) end - def notifiable?(type, opts={}) + def notifiable?(type, opts = {}) # always notify when there isn't a user yet return true if user.blank? NotificationRecipientService.notifiable?(user, type, notifiable_options.merge(opts)) end - private def send_invite From b5809822b13ac916ef3ea7691757725be147ade8 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Wed, 9 Aug 2017 23:48:37 -0700 Subject: [PATCH 059/141] add a spec for never emailing the ghost user --- spec/services/notification_service_spec.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index b6110da83b5..bed74b4a900 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -87,6 +87,14 @@ describe NotificationService, :mailer do it 'sends email to key owner' do expect { notification.new_key(key) }.to change { ActionMailer::Base.deliveries.size }.by(1) end + + it 'never emails the ghost user' do + key.user = User.ghost + + notification.new_key(key) + + should_not_email_anyone + end end end From b0600b01563a7bfb09ae01f70c0f052c200b8f4c Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Wed, 9 Aug 2017 23:48:51 -0700 Subject: [PATCH 060/141] add a spec for new_group_member --- spec/services/notification_service_spec.rb | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index bed74b4a900..63639fd7db1 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -472,6 +472,36 @@ describe NotificationService, :mailer do end end + describe 'Members' do + let(:group) { create(:group) } + let(:project) { create(:project, :public, namespace: group) } + let(:added_user) { create(:user) } + + def create_member! + GroupMember.create( + group: group, + user: added_user, + access_level: Gitlab::Access::GUEST + ) + end + + it 'sends a notification' do + create_member! + should_only_email(added_user) + end + + describe 'when notifications are disabled' do + before do + create_global_setting_for(added_user, :disabled) + end + + it 'does not send a notification' do + create_member! + should_not_email_anyone + end + end + end + describe 'Issues' do let(:group) { create(:group) } let(:project) { create(:project, :public, namespace: group) } From 4a3b18cbd87a49464d2a7619113a7b192f08a98b Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Thu, 10 Aug 2017 10:40:55 -0700 Subject: [PATCH 061/141] move the member spec to be with the other ones and add one --- spec/services/notification_service_spec.rb | 82 ++++++++++++++-------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 63639fd7db1..8886c71aa5b 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -472,36 +472,6 @@ describe NotificationService, :mailer do end end - describe 'Members' do - let(:group) { create(:group) } - let(:project) { create(:project, :public, namespace: group) } - let(:added_user) { create(:user) } - - def create_member! - GroupMember.create( - group: group, - user: added_user, - access_level: Gitlab::Access::GUEST - ) - end - - it 'sends a notification' do - create_member! - should_only_email(added_user) - end - - describe 'when notifications are disabled' do - before do - create_global_setting_for(added_user, :disabled) - end - - it 'does not send a notification' do - create_member! - should_not_email_anyone - end - end - end - describe 'Issues' do let(:group) { create(:group) } let(:project) { create(:project, :public, namespace: group) } @@ -1267,6 +1237,35 @@ describe NotificationService, :mailer do end.to change { ActionMailer::Base.deliveries.size }.by(1) end end + + describe '#new_group_member' do + let(:group) { create(:group) } + let(:added_user) { create(:user) } + + def create_member! + GroupMember.create( + group: group, + user: added_user, + access_level: Gitlab::Access::GUEST + ) + end + + it 'sends a notification' do + create_member! + should_only_email(added_user) + end + + describe 'when notifications are disabled' do + before do + create_global_setting_for(added_user, :disabled) + end + + it 'does not send a notification' do + create_member! + should_not_email_anyone + end + end + end end describe 'ProjectMember' do @@ -1286,6 +1285,29 @@ describe NotificationService, :mailer do end.to change { ActionMailer::Base.deliveries.size }.by(1) end end + + describe '#new_project_member' do + let(:project) { create(:project) } + let(:added_user) { create(:user) } + + def create_member! + create(:project_member, user: added_user, project: project) + end + + it do + create_member! + should_only_email(added_user) + end + + describe 'when notifications are disabled' do + before { create_global_setting_for(added_user, :disabled) } + + it do + create_member! + should_not_email_anyone + end + end + end end context 'guest user in private project' do From 38737345abdf91ac2cb0b1cdff4eb7132b4104ee Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Thu, 10 Aug 2017 10:41:25 -0700 Subject: [PATCH 062/141] skip the :read_project check for new_project_member since we're just adding them as a member, the permission may still return false. --- app/models/notification_recipient.rb | 7 ++++++- app/services/notification_service.rb | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index 30fab33e5e0..dc862565a71 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -6,7 +6,8 @@ class NotificationRecipient target: nil, acting_user: nil, project: nil, - group: nil + group: nil, + skip_read_ability: false ) unless NotificationSetting.levels.key?(type) || type == :subscription raise ArgumentError, "invalid type: #{type.inspect}" @@ -19,6 +20,7 @@ class NotificationRecipient @group = group || @project&.group @user = user @type = type + @skip_read_ability = skip_read_ability end def notification_setting @@ -83,6 +85,8 @@ class NotificationRecipient def has_access? DeclarativePolicy.subject_scope do return false unless user.can?(:receive_notifications) + return true if @skip_read_ability + return false if @project && !user.can?(:read_project, @project) return true unless read_ability @@ -102,6 +106,7 @@ class NotificationRecipient private def read_ability + return nil if @skip_read_ability return @read_ability if instance_variable_defined?(:@read_ability) @read_ability = diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index d92282ac896..4267879b03d 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -244,7 +244,7 @@ class NotificationService end def new_project_member(project_member) - return true unless project_member.notifiable?(:mention) + return true unless project_member.notifiable?(:mention, skip_read_ability: true) mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later end From cc4f77c632130097f6c9667a421d10ce1f47c6c5 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Thu, 10 Aug 2017 12:58:26 -0700 Subject: [PATCH 063/141] reset_delivered_emails before testing #new_key since the mere act of creating the key sends an email :| --- spec/services/notification_service_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 8886c71aa5b..d7ac0746df4 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -91,6 +91,7 @@ describe NotificationService, :mailer do it 'never emails the ghost user' do key.user = User.ghost + reset_delivered_emails! notification.new_key(key) should_not_email_anyone From 8f6205d1442cb6d88e992bff7a07c088ae92d93a Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Thu, 10 Aug 2017 13:13:09 -0700 Subject: [PATCH 064/141] don't send devise notifications to the ghost user --- app/models/user.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/user.rb b/app/models/user.rb index 7935b89662b..a4615436245 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1069,6 +1069,7 @@ class User < ActiveRecord::Base # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration def send_devise_notification(notification, *args) + return true unless can?(:receive_notifications) devise_mailer.send(notification, self, *args).deliver_later end From 02b5d359b5ad9deeb0ac2d04487eb43a3089bc99 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Thu, 10 Aug 2017 13:13:52 -0700 Subject: [PATCH 065/141] restructure the #new_key notification spec --- spec/services/notification_service_spec.rb | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index d7ac0746df4..44d3f441382 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -80,21 +80,16 @@ describe NotificationService, :mailer do describe 'Keys' do describe '#new_key' do - let!(:key) { create(:personal_key) } + let(:key_options) { {} } + let!(:key) { create(:personal_key, key_options) } it { expect(notification.new_key(key)).to be_truthy } + it { should_email(key.user) } - it 'sends email to key owner' do - expect { notification.new_key(key) }.to change { ActionMailer::Base.deliveries.size }.by(1) - end + describe 'never emails the ghost user' do + let(:key_options) { { user: User.ghost } } - it 'never emails the ghost user' do - key.user = User.ghost - - reset_delivered_emails! - notification.new_key(key) - - should_not_email_anyone + it { should_not_email_anyone } end end end From d1c1a1ead67937c9fd2aee4fc75cb7834d6a584d Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Thu, 10 Aug 2017 23:12:29 -0700 Subject: [PATCH 066/141] switch to multi-line before block --- spec/services/notification_service_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 44d3f441382..44b2d28d1d4 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -1296,7 +1296,9 @@ describe NotificationService, :mailer do end describe 'when notifications are disabled' do - before { create_global_setting_for(added_user, :disabled) } + before do + create_global_setting_for(added_user, :disabled) + end it do create_member! From 5c65c60b0c83c1c643008d0f241c677854a4f897 Mon Sep 17 00:00:00 2001 From: "http://jneen.net/" Date: Fri, 11 Aug 2017 16:08:38 -0700 Subject: [PATCH 067/141] add a changelog entry --- .../13325-bugfix-silence-on-disabled-notifications.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 changelogs/unreleased/13325-bugfix-silence-on-disabled-notifications.yml diff --git a/changelogs/unreleased/13325-bugfix-silence-on-disabled-notifications.yml b/changelogs/unreleased/13325-bugfix-silence-on-disabled-notifications.yml new file mode 100644 index 00000000000..90b169390d2 --- /dev/null +++ b/changelogs/unreleased/13325-bugfix-silence-on-disabled-notifications.yml @@ -0,0 +1,6 @@ +--- +title: disabling notifications globally now properly turns off group/project added + emails +merge_request: 13325 +author: @jneen +type: fixed From 640cbb00e30162e11f775d03175e931a4fd1ad70 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 4 Aug 2017 15:32:44 +0100 Subject: [PATCH 068/141] Add dynamic navigation tunnel to fly-out menus --- app/assets/javascripts/fly_out_nav.js | 137 ++++++++++++++++--- app/assets/stylesheets/new_sidebar.scss | 24 +--- spec/javascripts/fly_out_nav_spec.js | 167 +++++++++++++++++++----- 3 files changed, 253 insertions(+), 75 deletions(-) diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index 56744a440e7..9012f8b1a14 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -1,6 +1,19 @@ import Cookies from 'js-cookie'; import bp from './breakpoints'; +const IS_OVER_CLASS = 'is-over'; +const IS_ABOVE_CLASS = 'is-above'; +const IS_SHOWING_FLY_OUT_CLASS = 'is-showing-fly-out'; +let currentOpenMenu = null; +let menuCornerLocs; +let timeoutId; + +export const mousePos = []; + +export const setOpenMenu = (menu) => { currentOpenMenu = menu; }; + +export const slope = (a, b) => (b.y - a.y) / (b.x - a.x); + export const canShowActiveSubItems = (el) => { const isHiddenByMedia = bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md'; @@ -10,8 +23,28 @@ export const canShowActiveSubItems = (el) => { return true; }; + export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg'; +export const getHideSubItemsInterval = () => { + if (!currentOpenMenu) return 0; + + const currentMousePos = mousePos[mousePos.length - 1]; + const prevMousePos = mousePos[0]; + const currentMousePosY = currentMousePos.y; + const [menuTop, menuBottom] = menuCornerLocs; + + if (currentMousePosY < menuTop.y || + currentMousePosY > menuBottom.y) return 0; + + if (slope(prevMousePos, menuBottom) < slope(currentMousePos, menuBottom) && + slope(prevMousePos, menuTop) > slope(currentMousePos, menuTop)) { + return 300; + } + + return 0; +}; + export const calculateTop = (boundingRect, outerHeight) => { const windowHeight = window.innerHeight; const bottomOverflow = windowHeight - (boundingRect.top + outerHeight); @@ -20,45 +53,111 @@ export const calculateTop = (boundingRect, outerHeight) => { boundingRect.top; }; -export const showSubLevelItems = (el) => { - const subItems = el.querySelector('.sidebar-sub-level-items'); +export const hideMenu = (el) => { + if (!el) return; - if (!subItems || !canShowSubItems() || !canShowActiveSubItems(el)) return; + const parentEl = el.parentNode; - subItems.style.display = 'block'; - el.classList.add('is-showing-fly-out'); - el.classList.add('is-over'); + el.style.display = ''; // eslint-disable-line no-param-reassign + el.style.transform = ''; // eslint-disable-line no-param-reassign + el.classList.remove(IS_ABOVE_CLASS); + parentEl.classList.remove(IS_OVER_CLASS); + parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS); + setOpenMenu(null); +}; + +export const moveSubItemsToPosition = (el, subItems) => { const boundingRect = el.getBoundingClientRect(); const top = calculateTop(boundingRect, subItems.offsetHeight); const isAbove = top < boundingRect.top; subItems.classList.add('fly-out-list'); - subItems.style.transform = `translate3d(0, ${Math.floor(top)}px, 0)`; + subItems.style.transform = `translate3d(0, ${Math.floor(top)}px, 0)`; // eslint-disable-line no-param-reassign + + const subItemsRect = subItems.getBoundingClientRect(); + + menuCornerLocs = [ + { + x: subItemsRect.left, // left position of the sub items + y: subItemsRect.top, // top position of the sub items + }, + { + x: subItemsRect.left, // left position of the sub items + y: subItemsRect.top + subItemsRect.height, // bottom position of the sub items + }, + ]; if (isAbove) { - subItems.classList.add('is-above'); + subItems.classList.add(IS_ABOVE_CLASS); } }; -export const hideSubLevelItems = (el) => { +export const showSubLevelItems = (el) => { const subItems = el.querySelector('.sidebar-sub-level-items'); - if (!subItems || !canShowSubItems() || !canShowActiveSubItems(el)) return; + if (!canShowSubItems() || !canShowActiveSubItems(el)) return; - el.classList.remove('is-showing-fly-out'); - el.classList.remove('is-over'); - subItems.style.display = ''; - subItems.style.transform = ''; - subItems.classList.remove('is-above'); + el.classList.add(IS_OVER_CLASS); + + if (!subItems) return; + + subItems.style.display = 'block'; + el.classList.add(IS_SHOWING_FLY_OUT_CLASS); + + setOpenMenu(subItems); + moveSubItemsToPosition(el, subItems); +}; + +export const mouseEnterTopItems = (el) => { + clearTimeout(timeoutId); + + timeoutId = setTimeout(() => { + if (currentOpenMenu) hideMenu(currentOpenMenu); + + showSubLevelItems(el); + }, getHideSubItemsInterval()); +}; + +export const mouseLeaveTopItem = (el) => { + const subItems = el.querySelector('.sidebar-sub-level-items'); + + if (!canShowSubItems() || !canShowActiveSubItems(el) || (subItems && subItems === currentOpenMenu)) return; + + el.classList.remove(IS_OVER_CLASS); +}; + +export const documentMouseMove = (e) => { + mousePos.push({ x: e.clientX, y: e.clientY }); + + if (mousePos.length > 6) mousePos.shift(); }; export default () => { - const items = [...document.querySelectorAll('.sidebar-top-level-items > li')] - .filter(el => el.querySelector('.sidebar-sub-level-items')); + const sidebar = document.querySelector('.sidebar-top-level-items'); + const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')]; + + sidebar.addEventListener('mouseleave', () => { + clearTimeout(timeoutId); + + timeoutId = setTimeout(() => { + if (currentOpenMenu) hideMenu(currentOpenMenu); + }, getHideSubItemsInterval()); + }); items.forEach((el) => { - el.addEventListener('mouseenter', e => showSubLevelItems(e.currentTarget)); - el.addEventListener('mouseleave', e => hideSubLevelItems(e.currentTarget)); + const subItems = el.querySelector('.sidebar-sub-level-items'); + + if (subItems) { + subItems.addEventListener('mouseleave', () => { + clearTimeout(timeoutId); + hideMenu(currentOpenMenu); + }); + } + + el.addEventListener('mouseenter', e => mouseEnterTopItems(e.currentTarget)); + el.addEventListener('mouseleave', e => mouseLeaveTopItem(e.currentTarget)); }); + + document.addEventListener('mousemove', documentMouseMove); }; diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index d49f23b4f5a..faedd207e01 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/new_sidebar.scss @@ -250,32 +250,13 @@ $new-sidebar-collapsed-width: 50px; position: absolute; top: -30px; bottom: -30px; - left: 0; + left: -10px; right: -30px; z-index: -1; } - &::after { - content: ""; - position: absolute; - top: 44px; - left: -30px; - right: 35px; - bottom: 0; - height: 100%; - max-height: 150px; - z-index: -1; - transform: skew(33deg); - } - &.is-above { margin-top: 1px; - - &::after { - top: auto; - bottom: 44px; - transform: skew(-30deg); - } } > .active { @@ -322,8 +303,7 @@ $new-sidebar-collapsed-width: 50px; } } - &:not(.active):hover > a, - > a:hover, + &.active > a:hover, &.is-over > a { background-color: $white-light; } diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js index e44d874ad2b..dca58cb2eb2 100644 --- a/spec/javascripts/fly_out_nav_spec.js +++ b/spec/javascripts/fly_out_nav_spec.js @@ -1,14 +1,19 @@ import Cookies from 'js-cookie'; import { calculateTop, - hideSubLevelItems, showSubLevelItems, canShowSubItems, canShowActiveSubItems, + mouseEnterTopItems, + mouseLeaveTopItem, + setOpenMenu, + mousePos, + getHideSubItemsInterval, + documentMouseMove, } from '~/fly_out_nav'; import bp from '~/breakpoints'; -describe('Fly out sidebar navigation', () => { +fdescribe('Fly out sidebar navigation', () => { let el; let breakpointSize = 'lg'; @@ -18,11 +23,14 @@ describe('Fly out sidebar navigation', () => { document.body.appendChild(el); spyOn(bp, 'getBreakpointSize').and.callFake(() => breakpointSize); + + setOpenMenu(null); }); afterEach(() => { el.remove(); breakpointSize = 'lg'; + mousePos.length = 0; }); describe('calculateTop', () => { @@ -49,61 +57,152 @@ describe('Fly out sidebar navigation', () => { }); }); - describe('hideSubLevelItems', () => { + describe('getHideSubItemsInterval', () => { beforeEach(() => { - el.innerHTML = ''; + el.innerHTML = ''; }); - it('hides subitems', () => { - hideSubLevelItems(el); + it('returns 0 if currentOpenMenu is nil', () => { + expect( + getHideSubItemsInterval(), + ).toBe(0); + }); + + it('returns 0 when mouse above sub-items', () => { + showSubLevelItems(el); + documentMouseMove({ + clientX: el.getBoundingClientRect().left, + clientY: el.getBoundingClientRect().top, + }); + documentMouseMove({ + clientX: el.getBoundingClientRect().left, + clientY: el.getBoundingClientRect().top - 50, + }); expect( - el.querySelector('.sidebar-sub-level-items').style.display, - ).toBe(''); + getHideSubItemsInterval(), + ).toBe(0); }); - it('does not hude subitems on mobile', () => { - breakpointSize = 'xs'; + it('returns 0 when mouse is below sub-items', () => { + const subItems = el.querySelector('.sidebar-sub-level-items'); - hideSubLevelItems(el); + showSubLevelItems(el); + documentMouseMove({ + clientX: el.getBoundingClientRect().left, + clientY: el.getBoundingClientRect().top, + }); + documentMouseMove({ + clientX: el.getBoundingClientRect().left, + clientY: (el.getBoundingClientRect().top - subItems.getBoundingClientRect().height) + 50, + }); expect( - el.querySelector('.sidebar-sub-level-items').style.display, - ).not.toBe('none'); + getHideSubItemsInterval(), + ).toBe(0); }); - it('removes is-over class', () => { + it('returns 300 when mouse is moved towards sub-items', () => { + documentMouseMove({ + clientX: el.getBoundingClientRect().left, + clientY: el.getBoundingClientRect().top, + }); + showSubLevelItems(el); + documentMouseMove({ + clientX: el.getBoundingClientRect().left + 20, + clientY: el.getBoundingClientRect().top + 10, + }); + + expect( + getHideSubItemsInterval(), + ).toBe(300); + }); + }); + + describe('mouseLeaveTopItem', () => { + beforeEach(() => { spyOn(el.classList, 'remove'); + }); - hideSubLevelItems(el); + it('removes is-over class if currentOpenMenu is null', () => { + mouseLeaveTopItem(el); expect( el.classList.remove, ).toHaveBeenCalledWith('is-over'); }); - it('removes is-above class from sub-items', () => { - const subItems = el.querySelector('.sidebar-sub-level-items'); + it('removes is-over class if currentOpenMenu is null & there are sub-items', () => { + el.innerHTML = ''; - spyOn(subItems.classList, 'remove'); - - hideSubLevelItems(el); - - expect( - subItems.classList.remove, - ).toHaveBeenCalledWith('is-above'); - }); - - it('does nothing if el has no sub-items', () => { - el.innerHTML = ''; - - spyOn(el.classList, 'remove'); - - hideSubLevelItems(el); + mouseLeaveTopItem(el); expect( el.classList.remove, - ).not.toHaveBeenCalledWith(); + ).toHaveBeenCalledWith('is-over'); + }); + + it('does not remove is-over class if currentOpenMenu is the passed in sub-items', () => { + el.innerHTML = ''; + + setOpenMenu(el.querySelector('.sidebar-sub-level-items')); + mouseLeaveTopItem(el); + + expect( + el.classList.remove, + ).not.toHaveBeenCalled(); + }); + }); + + describe('mouseEnterTopItems', () => { + beforeEach(() => { + jasmine.clock().install(); + + el.innerHTML = ''; + }); + + afterEach(() => { + jasmine.clock().uninstall(); + }); + + it('shows sub-items after 0ms if no menu is open', () => { + mouseEnterTopItems(el); + + expect( + getHideSubItemsInterval(), + ).toBe(0); + + jasmine.clock().tick(0); + + expect( + el.querySelector('.sidebar-sub-level-items').style.display, + ).toBe('block'); + }); + + it('shows sub-items after 300ms if a menu is currently open', () => { + documentMouseMove({ + clientX: el.getBoundingClientRect().left, + clientY: el.getBoundingClientRect().top, + }); + + setOpenMenu(el.querySelector('.sidebar-sub-level-items')); + + documentMouseMove({ + clientX: el.getBoundingClientRect().left + 20, + clientY: el.getBoundingClientRect().top + 10, + }); + + mouseEnterTopItems(el); + + expect( + getHideSubItemsInterval(), + ).toBe(300); + + jasmine.clock().tick(300); + + expect( + el.querySelector('.sidebar-sub-level-items').style.display, + ).toBe('block'); }); }); @@ -132,7 +231,7 @@ describe('Fly out sidebar navigation', () => { ).not.toBe('block'); }); - it('does not shows sub-items', () => { + it('shows sub-items', () => { showSubLevelItems(el); expect( From 80c788bbeb875e724230a6ce64f09f98bbfe6f40 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 8 Aug 2017 11:25:36 +0100 Subject: [PATCH 069/141] fixed JS error when no sidebar exists --- app/assets/javascripts/fly_out_nav.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index 9012f8b1a14..9a2ab7f82b1 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -135,6 +135,9 @@ export const documentMouseMove = (e) => { export default () => { const sidebar = document.querySelector('.sidebar-top-level-items'); + + if (!sidebar) return; + const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')]; sidebar.addEventListener('mouseleave', () => { From 449a84fe4063408b432d7bd59a399747a570fff1 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 10 Aug 2017 13:24:03 +0100 Subject: [PATCH 070/141] fixed up specs caused by left over elements in the body --- app/assets/javascripts/fly_out_nav.js | 3 ++- spec/javascripts/fly_out_nav_spec.js | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index 9a2ab7f82b1..7d128cbf5fb 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -122,7 +122,8 @@ export const mouseEnterTopItems = (el) => { export const mouseLeaveTopItem = (el) => { const subItems = el.querySelector('.sidebar-sub-level-items'); - if (!canShowSubItems() || !canShowActiveSubItems(el) || (subItems && subItems === currentOpenMenu)) return; + if (!canShowSubItems() || !canShowActiveSubItems(el) || + (subItems && subItems === currentOpenMenu)) return; el.classList.remove(IS_OVER_CLASS); }; diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js index dca58cb2eb2..65a7459c5ed 100644 --- a/spec/javascripts/fly_out_nav_spec.js +++ b/spec/javascripts/fly_out_nav_spec.js @@ -13,7 +13,7 @@ import { } from '~/fly_out_nav'; import bp from '~/breakpoints'; -fdescribe('Fly out sidebar navigation', () => { +describe('Fly out sidebar navigation', () => { let el; let breakpointSize = 'lg'; @@ -28,7 +28,7 @@ fdescribe('Fly out sidebar navigation', () => { }); afterEach(() => { - el.remove(); + document.body.innerHTML = ''; breakpointSize = 'lg'; mousePos.length = 0; }); @@ -59,7 +59,7 @@ fdescribe('Fly out sidebar navigation', () => { describe('getHideSubItemsInterval', () => { beforeEach(() => { - el.innerHTML = ''; + el.innerHTML = ''; }); it('returns 0 if currentOpenMenu is nil', () => { From 56d114921caef8588891fa2090b00823ddc4ce9f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 14 Aug 2017 08:58:22 +0100 Subject: [PATCH 071/141] moved timeout to a variable --- app/assets/javascripts/fly_out_nav.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index 7d128cbf5fb..cbc3ad23990 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -1,6 +1,7 @@ import Cookies from 'js-cookie'; import bp from './breakpoints'; +const HIDE_INTERVAL_TIMEOUT = 300; const IS_OVER_CLASS = 'is-over'; const IS_ABOVE_CLASS = 'is-above'; const IS_SHOWING_FLY_OUT_CLASS = 'is-showing-fly-out'; @@ -10,7 +11,7 @@ let timeoutId; export const mousePos = []; -export const setOpenMenu = (menu) => { currentOpenMenu = menu; }; +export const setOpenMenu = (menu = null) => { currentOpenMenu = menu; }; export const slope = (a, b) => (b.y - a.y) / (b.x - a.x); @@ -39,7 +40,7 @@ export const getHideSubItemsInterval = () => { if (slope(prevMousePos, menuBottom) < slope(currentMousePos, menuBottom) && slope(prevMousePos, menuTop) > slope(currentMousePos, menuTop)) { - return 300; + return HIDE_INTERVAL_TIMEOUT; } return 0; @@ -64,7 +65,7 @@ export const hideMenu = (el) => { parentEl.classList.remove(IS_OVER_CLASS); parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS); - setOpenMenu(null); + setOpenMenu(); }; export const moveSubItemsToPosition = (el, subItems) => { @@ -129,7 +130,10 @@ export const mouseLeaveTopItem = (el) => { }; export const documentMouseMove = (e) => { - mousePos.push({ x: e.clientX, y: e.clientY }); + mousePos.push({ + x: e.clientX, + y: e.clientY, + }); if (mousePos.length > 6) mousePos.shift(); }; From d9b6fd4fba1fa996ba6c71358ad933ba2328ba18 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 11 Aug 2017 18:23:47 +0200 Subject: [PATCH 072/141] Handle missing .gitmodules when getting submodule urls --- lib/gitlab/git/repository.rb | 2 ++ spec/lib/gitlab/git/commit_spec.rb | 4 ++-- spec/lib/gitlab/git/repository_spec.rb | 10 ++++++++-- .../3e/20715310a699808282e772720b9c04a0696bcc | Bin 0 -> 566 bytes .../95/96bc54a6f0c0c98248fe97077eb5ccf48a98d0 | 2 ++ spec/support/gitlab-git-test.git/packed-refs | 1 + spec/support/seed_repo.rb | 1 + 7 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 spec/support/gitlab-git-test.git/objects/3e/20715310a699808282e772720b9c04a0696bcc create mode 100644 spec/support/gitlab-git-test.git/objects/95/96bc54a6f0c0c98248fe97077eb5ccf48a98d0 diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 7000b173075..081423eb0db 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -827,6 +827,8 @@ module Gitlab return unless commit_object && commit_object.type == :COMMIT gitmodules = gitaly_commit_client.tree_entry(ref, '.gitmodules', Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE) + return unless gitmodules + found_module = GitmodulesParser.new(gitmodules.data).parse[path] found_module && found_module['url'] diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index c531d4b055f..ac33cd8a2c9 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -310,8 +310,8 @@ describe Gitlab::Git::Commit, seed_helper: true do commits.map(&:id) end - it 'has 33 elements' do - expect(subject.size).to eq(33) + it 'has 34 elements' do + expect(subject.size).to eq(34) end it 'includes the expected commits' do diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 04c86a2c1a7..7ca5a2c2b93 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -289,7 +289,13 @@ describe Gitlab::Git::Repository, seed_helper: true do it { expect(submodule_url('six')).to eq('git://github.com/randx/six.git') } end - context 'no submodules at commit' do + context 'no .gitmodules at commit' do + let(:ref) { '9596bc54a6f0c0c98248fe97077eb5ccf48a98d0' } + + it { expect(submodule_url('six')).to eq(nil) } + end + + context 'no gitlink entry' do let(:ref) { '6d39438' } it { expect(submodule_url('six')).to eq(nil) } @@ -986,7 +992,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe '#branch_count' do it 'returns the number of branches' do - expect(repository.branch_count).to eq(9) + expect(repository.branch_count).to eq(10) end end diff --git a/spec/support/gitlab-git-test.git/objects/3e/20715310a699808282e772720b9c04a0696bcc b/spec/support/gitlab-git-test.git/objects/3e/20715310a699808282e772720b9c04a0696bcc new file mode 100644 index 0000000000000000000000000000000000000000..86bf37ac887ca2510f51242c3e5a1090dc2d986f GIT binary patch literal 566 zcmV-60?GY&0V^p=O;s>6v|unaFfcPQQP4}zEXhpI%P&f0xWCGN`YGc&1yQdVYvyu4 z&%G;urU|Ob*~8J#-POn6o#D7+>H3S}6*7Bfn57uc;*Xpiz1a_@$los{$kQn_#M94R zFE@qZU`1Q_&d3>yK9(=}k(;rMU#mf|9;(zmH8(9YCsnU1vw)#*Z%U-OT>3_{SYwS2 z<$gKGPv>=@%6vSXUHyVx88)xF9JX1%Z?FT z?3{Y@!_w!wS^ef-o*vq{Snl>tHi&aT11AXWQI3sjHrBt84T?4zuOC^?7mbe#dDdX$3Y1uhb-72!tvMa}5gi^!Hq%A1 zc%WvlmUU3<@U&U#t=9wDZ@($DhZ۫pz?Y3XBB̰GB4 p?kv۞y~W])[a<CP_ \ No newline at end of file diff --git a/spec/support/gitlab-git-test.git/packed-refs b/spec/support/gitlab-git-test.git/packed-refs index ce5ab1f705b..507e4ce785a 100644 --- a/spec/support/gitlab-git-test.git/packed-refs +++ b/spec/support/gitlab-git-test.git/packed-refs @@ -8,6 +8,7 @@ 46e1395e609395de004cacd4b142865ab0e52a29 refs/heads/gitattributes-updated 4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6 refs/heads/master 5937ac0a7beb003549fc5fd26fc247adbce4a52e refs/heads/merge-test +9596bc54a6f0c0c98248fe97077eb5ccf48a98d0 refs/heads/missing-gitmodules f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8 refs/tags/v1.0.0 ^6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b refs/tags/v1.1.0 diff --git a/spec/support/seed_repo.rb b/spec/support/seed_repo.rb index cfe7fc980a8..b4868e82cd7 100644 --- a/spec/support/seed_repo.rb +++ b/spec/support/seed_repo.rb @@ -97,6 +97,7 @@ module SeedRepo gitattributes-updated master merge-test + missing-gitmodules ].freeze TAGS = %w[ v1.0.0 From 35147a7e4466678cc7c5044e0a0b5c5060fac16c Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Mon, 14 Aug 2017 11:02:28 +0200 Subject: [PATCH 073/141] Update charlock_holmes The compiler needs to be pinned to c++11, which is done by https://github.com/brianmario/charlock_holmes/commit/51d8a2f0eb1e9d3e476482727d4122b3d11b2bc6 Without this patch it won't install charlock_holmes on newer compilers. --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index d443209349d..ab01a556561 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -113,7 +113,7 @@ GEM activesupport (>= 4.0.0) mime-types (>= 1.16) cause (0.1) - charlock_holmes (0.7.3) + charlock_holmes (0.7.4) chronic (0.10.2) chronic_duration (0.10.6) numerizer (~> 0.1.1) From 9be6df3b3927fd058702218526343a9b8409b02c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 14 Aug 2017 11:58:27 +0200 Subject: [PATCH 074/141] Don't run the `flaky-examples-check` job for docs branches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 024f2929252..cd19b6f47ff 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -226,6 +226,7 @@ update-tests-metadata: flaky-examples-check: <<: *dedicated-runner + <<: *except-docs image: ruby:2.3-alpine services: [] before_script: [] From edcc488b75d8c6fcad3994bcda30a82756496969 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Thu, 10 Aug 2017 20:33:24 +0200 Subject: [PATCH 075/141] use mutex for keychain interaction setting of the gpg home directory is not thread safe, as the directoy gets stored on the class. if multiple threads change the directory at the same time, one of the threads will be working in the wrong directory. --- lib/gitlab/gpg.rb | 34 ++++++++++++++++++++++++++-------- spec/lib/gitlab/gpg_spec.rb | 30 ++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb index 653c56d925b..78ebd8866a1 100644 --- a/lib/gitlab/gpg.rb +++ b/lib/gitlab/gpg.rb @@ -2,6 +2,8 @@ module Gitlab module Gpg extend self + MUTEX = Mutex.new + module CurrentKeyChain extend self @@ -42,7 +44,30 @@ module Gitlab end end - def using_tmp_keychain + # Allows thread safe switching of temporary keychain files + # + # 1. The current thread may use nesting of temporary keychain + # 2. Another thread needs to wait for the lock to be released + def using_tmp_keychain(&block) + if MUTEX.locked? && MUTEX.owned? + optimistic_using_tmp_keychain(&block) + else + MUTEX.synchronize do + optimistic_using_tmp_keychain(&block) + end + end + end + + # 1. Returns the custom home directory if one has been set by calling + # `GPGME::Engine.home_dir=` + # 2. Returns the default home directory otherwise + def current_home_dir + GPGME::Engine.info.first.home_dir || GPGME::Engine.dirinfo('homedir') + end + + private + + def optimistic_using_tmp_keychain Dir.mktmpdir do |dir| previous_dir = current_home_dir @@ -55,12 +80,5 @@ module Gitlab return_value end end - - # 1. Returns the custom home directory if one has been set by calling - # `GPGME::Engine.home_dir=` - # 2. Returns the default home directory otherwise - def current_home_dir - GPGME::Engine.info.first.home_dir || GPGME::Engine.dirinfo('homedir') - end end end diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index 95d371ea178..30ad033b204 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -65,6 +65,36 @@ describe Gitlab::Gpg do expect(described_class.current_home_dir).to eq default_home_dir end end + + describe '.using_tmp_keychain' do + it "the second thread does not change the first thread's directory" do + thread1 = Thread.new do + described_class.using_tmp_keychain do + dir = described_class.current_home_dir + sleep 0.1 + expect(described_class.current_home_dir).to eq dir + end + end + + thread2 = Thread.new do + described_class.using_tmp_keychain do + sleep 0.2 + end + end + + thread1.join + thread2.join + end + + it 'allows recursive execution in the same thread' do + expect do + described_class.using_tmp_keychain do + described_class.using_tmp_keychain do + end + end + end.not_to raise_error(ThreadError) + end + end end describe Gitlab::Gpg::CurrentKeyChain do From 2c17853ac718dd516875d44da567665ed1081746 Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Fri, 11 Aug 2017 20:23:37 +0200 Subject: [PATCH 076/141] add changelog --- changelogs/unreleased/fix-thread-safe-gpgme-tmp-directory.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-thread-safe-gpgme-tmp-directory.yml diff --git a/changelogs/unreleased/fix-thread-safe-gpgme-tmp-directory.yml b/changelogs/unreleased/fix-thread-safe-gpgme-tmp-directory.yml new file mode 100644 index 00000000000..66b5b6b4f47 --- /dev/null +++ b/changelogs/unreleased/fix-thread-safe-gpgme-tmp-directory.yml @@ -0,0 +1,4 @@ +--- +title: Make GPGME temporary directory handling thread safe +merge_request: 13481 +author: Alexis Reigel From a175966677edc385156eb9dab79d129ece0bb87f Mon Sep 17 00:00:00 2001 From: Alexis Reigel Date: Mon, 14 Aug 2017 12:38:08 +0200 Subject: [PATCH 077/141] reset original directory in ensure --- lib/gitlab/gpg.rb | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb index 78ebd8866a1..45e9f9d65ae 100644 --- a/lib/gitlab/gpg.rb +++ b/lib/gitlab/gpg.rb @@ -68,17 +68,13 @@ module Gitlab private def optimistic_using_tmp_keychain + previous_dir = current_home_dir Dir.mktmpdir do |dir| - previous_dir = current_home_dir - GPGME::Engine.home_dir = dir - - return_value = yield - - GPGME::Engine.home_dir = previous_dir - - return_value + yield end + ensure + GPGME::Engine.home_dir = previous_dir end end end From dcca25e98a49c2925dafeac5a79bff4cd99da472 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones Date: Mon, 14 Aug 2017 12:31:20 +0100 Subject: [PATCH 078/141] Update CHANGELOG.md for 9.4.5 [ci skip] --- CHANGELOG.md | 21 +++++++++++++++++++ changelogs/unreleased/34492-firefox-job.yml | 4 ---- ...pting-to-upload-or-replace-from-the-ui.yml | 4 ---- .../unreleased/35232-next-unresolved.yml | 4 ---- ...e-project-error-in-admin-interface-fix.yml | 4 ---- ...allow-logged-in-user-to-read-user-list.yml | 4 ---- .../unreleased/36158-new-issue-button.yml | 4 ---- ...oup-milestone-link-in-issuable-sidebar.yml | 4 ---- .../unreleased/fix-oauth-checkboxes.yml | 4 ---- ...ot-connect-to-ci-server-error-messages.yml | 5 ----- ...ineschedule-have-nullified-next_run_at.yml | 4 ---- changelogs/unreleased/mattermost_fixes.yml | 4 ---- ...fix-case-insensitive-redirect-matching.yml | 4 ---- .../unreleased/mk-fix-deploy-key-deletion.yml | 4 ---- ...me-change-with-container-registry-tags.yml | 4 ---- .../project-foreign-keys-without-errors.yml | 4 ---- changelogs/unreleased/search-flickering.yml | 4 ---- ...c-fix-wildcard-protected-delete-merged.yml | 4 ---- .../unreleased/zj-ref-path-monospace.yml | 4 ---- 19 files changed, 21 insertions(+), 73 deletions(-) delete mode 100644 changelogs/unreleased/34492-firefox-job.yml delete mode 100644 changelogs/unreleased/35052-please-select-a-file-when-attempting-to-upload-or-replace-from-the-ui.yml delete mode 100644 changelogs/unreleased/35232-next-unresolved.yml delete mode 100644 changelogs/unreleased/35435-pending-delete-project-error-in-admin-interface-fix.yml delete mode 100644 changelogs/unreleased/35697-allow-logged-in-user-to-read-user-list.yml delete mode 100644 changelogs/unreleased/36158-new-issue-button.yml delete mode 100644 changelogs/unreleased/fix-group-milestone-link-in-issuable-sidebar.yml delete mode 100644 changelogs/unreleased/fix-oauth-checkboxes.yml delete mode 100644 changelogs/unreleased/fix-sm-34547-cannot-connect-to-ci-server-error-messages.yml delete mode 100644 changelogs/unreleased/fix-sm-35931-active-ci-pipelineschedule-have-nullified-next_run_at.yml delete mode 100644 changelogs/unreleased/mattermost_fixes.yml delete mode 100644 changelogs/unreleased/mk-fix-case-insensitive-redirect-matching.yml delete mode 100644 changelogs/unreleased/mk-fix-deploy-key-deletion.yml delete mode 100644 changelogs/unreleased/mk-validate-username-change-with-container-registry-tags.yml delete mode 100644 changelogs/unreleased/project-foreign-keys-without-errors.yml delete mode 100644 changelogs/unreleased/search-flickering.yml delete mode 100644 changelogs/unreleased/tc-fix-wildcard-protected-delete-merged.yml delete mode 100644 changelogs/unreleased/zj-ref-path-monospace.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a9c751937e..3ecedd44c89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,27 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 9.4.5 (2017-08-14) + +- Fix deletion of deploy keys linked to other projects. !13162 +- Allow any logged in users to read_users_list even if it's restricted. !13201 +- Make Delete Merged Branches handle wildcard protected branches correctly. !13251 +- Fix an order of operations for CI connection error message in merge request widget. !13252 +- Fix pipeline_schedules pages when active schedule has an abnormal state. !13286 +- Add missing validation error for username change with container registry tags. !13356 +- Fix destroy of case-insensitive conflicting redirects. !13357 +- Project pending delete no longer return 500 error in admins projects view. !13389 +- Fix search box losing focus when typing. +- Use jQuery to control scroll behavior in job log for cross browser consistency. +- Use project_ref_path to create the link to a branch to fix links that 404. +- improve file upload/replace experience. +- fix jump to next discussion button. +- Fixes new issue button for failed job returning 404. +- Fix links to group milestones from issue and merge request sidebar. +- Fixed sign-in restrictions buttons not toggling active state. +- Fix Mattermost integration. +- Change project FK migration to skip existing FKs. + ## 9.4.4 (2017-08-09) - Remove hidden symlinks from project import files. diff --git a/changelogs/unreleased/34492-firefox-job.yml b/changelogs/unreleased/34492-firefox-job.yml deleted file mode 100644 index 881b8f649ea..00000000000 --- a/changelogs/unreleased/34492-firefox-job.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use jQuery to control scroll behavior in job log for cross browser consistency -merge_request: -author: diff --git a/changelogs/unreleased/35052-please-select-a-file-when-attempting-to-upload-or-replace-from-the-ui.yml b/changelogs/unreleased/35052-please-select-a-file-when-attempting-to-upload-or-replace-from-the-ui.yml deleted file mode 100644 index 5925da14f89..00000000000 --- a/changelogs/unreleased/35052-please-select-a-file-when-attempting-to-upload-or-replace-from-the-ui.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: improve file upload/replace experience -merge_request: -author: diff --git a/changelogs/unreleased/35232-next-unresolved.yml b/changelogs/unreleased/35232-next-unresolved.yml deleted file mode 100644 index 45f3fb429a8..00000000000 --- a/changelogs/unreleased/35232-next-unresolved.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: fix jump to next discussion button -merge_request: -author: diff --git a/changelogs/unreleased/35435-pending-delete-project-error-in-admin-interface-fix.yml b/changelogs/unreleased/35435-pending-delete-project-error-in-admin-interface-fix.yml deleted file mode 100644 index 8539615faac..00000000000 --- a/changelogs/unreleased/35435-pending-delete-project-error-in-admin-interface-fix.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Project pending delete no longer return 500 error in admins projects view -merge_request: 13389 -author: diff --git a/changelogs/unreleased/35697-allow-logged-in-user-to-read-user-list.yml b/changelogs/unreleased/35697-allow-logged-in-user-to-read-user-list.yml deleted file mode 100644 index 54b2e71bef9..00000000000 --- a/changelogs/unreleased/35697-allow-logged-in-user-to-read-user-list.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow any logged in users to read_users_list even if it's restricted -merge_request: 13201 -author: diff --git a/changelogs/unreleased/36158-new-issue-button.yml b/changelogs/unreleased/36158-new-issue-button.yml deleted file mode 100644 index df61fa06af7..00000000000 --- a/changelogs/unreleased/36158-new-issue-button.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixes new issue button for failed job returning 404 -merge_request: -author: diff --git a/changelogs/unreleased/fix-group-milestone-link-in-issuable-sidebar.yml b/changelogs/unreleased/fix-group-milestone-link-in-issuable-sidebar.yml deleted file mode 100644 index 1558e575e6d..00000000000 --- a/changelogs/unreleased/fix-group-milestone-link-in-issuable-sidebar.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix links to group milestones from issue and merge request sidebar -merge_request: -author: diff --git a/changelogs/unreleased/fix-oauth-checkboxes.yml b/changelogs/unreleased/fix-oauth-checkboxes.yml deleted file mode 100644 index 2839ccc42cb..00000000000 --- a/changelogs/unreleased/fix-oauth-checkboxes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed sign-in restrictions buttons not toggling active state -merge_request: -author: diff --git a/changelogs/unreleased/fix-sm-34547-cannot-connect-to-ci-server-error-messages.yml b/changelogs/unreleased/fix-sm-34547-cannot-connect-to-ci-server-error-messages.yml deleted file mode 100644 index ddaec4f19f9..00000000000 --- a/changelogs/unreleased/fix-sm-34547-cannot-connect-to-ci-server-error-messages.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix an order of operations for CI connection error message in merge request - widget -merge_request: 13252 -author: diff --git a/changelogs/unreleased/fix-sm-35931-active-ci-pipelineschedule-have-nullified-next_run_at.yml b/changelogs/unreleased/fix-sm-35931-active-ci-pipelineschedule-have-nullified-next_run_at.yml deleted file mode 100644 index 07840205b6e..00000000000 --- a/changelogs/unreleased/fix-sm-35931-active-ci-pipelineschedule-have-nullified-next_run_at.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix pipeline_schedules pages when active schedule has an abnormal state -merge_request: 13286 -author: diff --git a/changelogs/unreleased/mattermost_fixes.yml b/changelogs/unreleased/mattermost_fixes.yml deleted file mode 100644 index 667109a0bb4..00000000000 --- a/changelogs/unreleased/mattermost_fixes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix Mattermost integration -merge_request: -author: diff --git a/changelogs/unreleased/mk-fix-case-insensitive-redirect-matching.yml b/changelogs/unreleased/mk-fix-case-insensitive-redirect-matching.yml deleted file mode 100644 index c539480c65f..00000000000 --- a/changelogs/unreleased/mk-fix-case-insensitive-redirect-matching.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix destroy of case-insensitive conflicting redirects -merge_request: 13357 -author: diff --git a/changelogs/unreleased/mk-fix-deploy-key-deletion.yml b/changelogs/unreleased/mk-fix-deploy-key-deletion.yml deleted file mode 100644 index 9ff2e49b14c..00000000000 --- a/changelogs/unreleased/mk-fix-deploy-key-deletion.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix deletion of deploy keys linked to other projects -merge_request: 13162 -author: diff --git a/changelogs/unreleased/mk-validate-username-change-with-container-registry-tags.yml b/changelogs/unreleased/mk-validate-username-change-with-container-registry-tags.yml deleted file mode 100644 index 425d5231e14..00000000000 --- a/changelogs/unreleased/mk-validate-username-change-with-container-registry-tags.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add missing validation error for username change with container registry tags -merge_request: 13356 -author: diff --git a/changelogs/unreleased/project-foreign-keys-without-errors.yml b/changelogs/unreleased/project-foreign-keys-without-errors.yml deleted file mode 100644 index 63c53c8ad8f..00000000000 --- a/changelogs/unreleased/project-foreign-keys-without-errors.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Change project FK migration to skip existing FKs -merge_request: -author: diff --git a/changelogs/unreleased/search-flickering.yml b/changelogs/unreleased/search-flickering.yml deleted file mode 100644 index 951a5a0292a..00000000000 --- a/changelogs/unreleased/search-flickering.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix search box losing focus when typing -merge_request: -author: diff --git a/changelogs/unreleased/tc-fix-wildcard-protected-delete-merged.yml b/changelogs/unreleased/tc-fix-wildcard-protected-delete-merged.yml deleted file mode 100644 index 9ca5f81cf79..00000000000 --- a/changelogs/unreleased/tc-fix-wildcard-protected-delete-merged.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make Delete Merged Branches handle wildcard protected branches correctly -merge_request: 13251 -author: diff --git a/changelogs/unreleased/zj-ref-path-monospace.yml b/changelogs/unreleased/zj-ref-path-monospace.yml deleted file mode 100644 index 638a29eb90e..00000000000 --- a/changelogs/unreleased/zj-ref-path-monospace.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use project_ref_path to create the link to a branch to fix links that 404 -merge_request: -author: From 4d97843f3cfe94a3061bcb03aa0da6876b026bea Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Mon, 14 Aug 2017 13:35:28 +0200 Subject: [PATCH 079/141] Remove `\n` from translations They seem to confuse some translation tools and aren't rendered in HTML anyway. --- app/helpers/groups_helper.rb | 2 +- app/helpers/projects_helper.rb | 2 +- locale/bg/gitlab.po | 19 ++--- locale/en/gitlab.po | 127 +++++++++++++++++++++++++++++---- locale/eo/gitlab.po | 28 ++------ locale/es/gitlab.po | 20 ++---- locale/fr/gitlab.po | 19 ++--- locale/gitlab.pot | 10 +-- locale/it/gitlab.po | 20 ++---- locale/ja/gitlab.po | 19 ++--- locale/ko/gitlab.po | 18 ++--- locale/pt_BR/gitlab.po | 26 ++----- locale/ru/gitlab.po | 20 ++---- locale/uk/gitlab.po | 20 ++---- locale/zh_CN/gitlab.po | 14 +--- locale/zh_HK/gitlab.po | 14 +--- locale/zh_TW/gitlab.po | 14 +--- 17 files changed, 169 insertions(+), 223 deletions(-) diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 8cd61f738e1..4123a96911f 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -59,7 +59,7 @@ module GroupsHelper end def remove_group_message(group) - _("You are going to remove %{group_name}.\nRemoved groups CANNOT be restored!\nAre you ABSOLUTELY sure?") % + _("You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") % { group_name: group.name } end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index a268413e84f..09cfd06dad3 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -80,7 +80,7 @@ module ProjectsHelper end def remove_project_message(project) - _("You are going to remove %{project_name_with_namespace}.\nRemoved project CANNOT be restored!\nAre you ABSOLUTELY sure?") % + _("You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") % { project_name_with_namespace: project.name_with_namespace } end diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po index 85d806e6f20..5c531f0cd7d 100644 --- a/locale/bg/gitlab.po +++ b/locale/bg/gitlab.po @@ -1163,22 +1163,11 @@ msgstr "Няма достатъчно данни за този етап." msgid "Withdraw Access Request" msgstr "Оттегляне на заявката за достъп" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"На път сте да премахнете „%{group_name}“.\n" -"Ако я премахнете, групата НЕ може да бъде възстановена!\n" -"НАИСТИНА ли искате това?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "На път сте да премахнете „%{group_name}“. Ако я премахнете, групата НЕ може да бъде възстановена! НАИСТИНА ли искате това?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"На път сте да премахнете „%{project_name_with_namespace}“.\n" -"Ако го премахнете, той НЕ може да бъде възстановен!\n" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "На път сте да премахнете „%{project_name_with_namespace}“. Ако го премахнете, той НЕ може да бъде възстановен!" "НАИСТИНА ли искате това?" msgid "" diff --git a/locale/en/gitlab.po b/locale/en/gitlab.po index 46bf4e33997..0ac591d4927 100644 --- a/locale/en/gitlab.po +++ b/locale/en/gitlab.po @@ -17,19 +17,36 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n != 1;\n" "\n" -msgid "%s additional commit has been omitted to prevent performance issues." -msgid_plural "%s additional commits have been omitted to prevent performance issues." -msgstr[0] "" -msgstr[1] "" - msgid "%d commit" msgid_plural "%d commits" msgstr[0] "" msgstr[1] "" +msgid "%s additional commit has been omitted to prevent performance issues." +msgid_plural "%s additional commits have been omitted to prevent performance issues." +msgstr[0] "" +msgstr[1] "" + msgid "%{commit_author_link} committed %{commit_timeago}" msgstr "" +msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt." +msgstr "" + +msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will block access for %{number_of_seconds} seconds." +msgstr "" + +msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved." +msgstr "" + +msgid "%{storage_name}: failed storage access attempt on host:" +msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:" +msgstr[0] "" +msgstr[1] "" + +msgid "(checkout the %{link} for information on how to install it)." +msgstr "" + msgid "1 pipeline" msgid_plural "%d pipelines" msgstr[0] "" @@ -41,6 +58,9 @@ msgstr "" msgid "About auto deploy" msgstr "" +msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again." +msgstr "" + msgid "Active" msgstr "" @@ -68,6 +88,18 @@ msgstr "" msgid "Are you sure you want to delete this pipeline schedule?" msgstr "" +msgid "Are you sure you want to discard your changes?" +msgstr "" + +msgid "Are you sure you want to reset registration token?" +msgstr "" + +msgid "Are you sure you want to reset the health check token?" +msgstr "" + +msgid "Are you sure?" +msgstr "" + msgid "Attach a file by drag & drop or %{upload_link}" msgstr "" @@ -109,6 +141,9 @@ msgstr "" msgid "Cancel" msgstr "" +msgid "Cancel edit" +msgstr "" + msgid "ChangeTypeActionLabel|Pick into branch" msgstr "" @@ -234,6 +269,9 @@ msgstr "" msgid "Create New Directory" msgstr "" +msgid "Create a new branch" +msgstr "" + msgid "Create a personal access token on your account to pull or push via %{protocol}." msgstr "" @@ -311,9 +349,15 @@ msgstr[1] "" msgid "Description" msgstr "" +msgid "Details" +msgstr "" + msgid "Directory name" msgstr "" +msgid "Discard changes" +msgstr "" + msgid "Don't show again" msgstr "" @@ -397,12 +441,36 @@ msgstr "" msgid "From merge request merge until deploy to production" msgstr "" +msgid "Git storage health information has been reset" +msgstr "" + +msgid "GitLab Runner section" +msgstr "" + msgid "Go to your fork" msgstr "" msgid "GoToYourFork|Fork" msgstr "" +msgid "Health Check" +msgstr "" + +msgid "Health information can be retrieved from the following endpoints. More information is available" +msgstr "" + +msgid "HealthCheck|Access token is" +msgstr "" + +msgid "HealthCheck|Healthy" +msgstr "" + +msgid "HealthCheck|No Health Problems Detected" +msgstr "" + +msgid "HealthCheck|Unhealthy" +msgstr "" + msgid "Home" msgstr "" @@ -412,6 +480,9 @@ msgstr "" msgid "Import repository" msgstr "" +msgid "Install a Runner compatible with GitLab CI" +msgstr "" + msgid "Interval Pattern" msgstr "" @@ -470,6 +541,9 @@ msgstr "" msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "" +msgid "More information is available|here" +msgstr "" + msgid "New Issue" msgid_plural "New Issues" msgstr[0] "" @@ -682,6 +756,9 @@ msgstr "" msgid "Project access must be granted explicitly to each user." msgstr "" +msgid "Project details" +msgstr "" + msgid "Project export could not be deleted." msgstr "" @@ -754,9 +831,21 @@ msgstr "" msgid "Remove project" msgstr "" +msgid "Repository" +msgstr "" + msgid "Request Access" msgstr "" +msgid "Reset git storage health information" +msgstr "" + +msgid "Reset health check access token" +msgstr "" + +msgid "Reset runners registration token" +msgstr "" + msgid "Revert this commit" msgstr "" @@ -781,6 +870,9 @@ msgstr "" msgid "Select a timezone" msgstr "" +msgid "Select existing branch" +msgstr "" + msgid "Select target branch" msgstr "" @@ -807,12 +899,18 @@ msgstr[1] "" msgid "Source code" msgstr "" +msgid "Specify the following URL during the Runner setup:" +msgstr "" + msgid "StarProject|Star" msgstr "" msgid "Start a %{new_merge_request} with these changes" msgstr "" +msgid "Start the Runner!" +msgstr "" + msgid "Switch branch/tag" msgstr "" @@ -875,6 +973,9 @@ msgstr "" msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6." msgstr "" +msgid "There are problems accessing Git storage: " +msgstr "" + msgid "This means you can not push code until you create an empty repository or import existing one." msgstr "" @@ -1044,6 +1145,9 @@ msgstr "" msgid "UploadLink|click to upload" msgstr "" +msgid "Use the following registration token during setup:" +msgstr "" + msgid "Use your global notification setting" msgstr "" @@ -1059,6 +1163,9 @@ msgstr "" msgid "VisibilityLevel|Public" msgstr "" +msgid "VisibilityLevel|Unknown" +msgstr "" + msgid "Want to see the data? Please ask an administrator for access." msgstr "" @@ -1068,16 +1175,10 @@ msgstr "" msgid "Withdraw Access Request" msgstr "" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" msgstr "" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" msgstr "" msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?" diff --git a/locale/eo/gitlab.po b/locale/eo/gitlab.po index d688478972d..94ae131186b 100644 --- a/locale/eo/gitlab.po +++ b/locale/eo/gitlab.po @@ -1165,30 +1165,14 @@ msgstr "Ne estas sufiĉe da datenoj por montri ĉi tiun etapon." msgid "Withdraw Access Request" msgstr "Nuligi la peton pri atingeblo" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Vi forigos „%{group_name}“.\n" -"Oni NE POVAS malfari la forigon de grupo!\n" -"Ĉu vi estas ABSOLUTE certa?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Vi forigos „%{group_name}“. Oni NE POVAS malfari la forigon de grupo! Ĉu vi estas ABSOLUTE certa?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Vi forigos „%{project_name_with_namespace}“.\n" -"Oni NE POVAS malfari la forigon de projekto!\n" -"Ĉu vi estas ABSOLUTE certa?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Vi forigos „%{project_name_with_namespace}“. Oni NE POVAS malfari la forigon de projekto! Ĉu vi estas ABSOLUTE certa?" -msgid "" -"You are going to remove the fork relationship to source project " -"%{forked_from_project}. Are you ABSOLUTELY sure?" -msgstr "" -"Vi forigos la rilaton de la disbranĉigo al la originala projekto, " -"„%{forked_from_project}“. Ĉu vi estas ABSOLUTE certa?" +msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?" +msgstr "Vi forigos la rilaton de la disbranĉigo al la originala projekto, „%{forked_from_project}“. Ĉu vi estas ABSOLUTE certa?" msgid "" "You are going to transfer %{project_name_with_namespace} to another owner. " diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po index 5c669d51a68..e43fd5fea15 100644 --- a/locale/es/gitlab.po +++ b/locale/es/gitlab.po @@ -1071,23 +1071,11 @@ msgstr "No hay suficientes datos para mostrar en esta etapa." msgid "Withdraw Access Request" msgstr "Retirar Solicitud de Acceso" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Va a eliminar %{group_name}.\n" -"¡El grupo eliminado NO puede ser restaurado!\n" -"¿Estás TOTALMENTE seguro?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Va a eliminar %{group_name}. ¡El grupo eliminado NO puede ser restaurado! ¿Estás TOTALMENTE seguro?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Va a eliminar %{project_name_with_namespace}.\n" -"¡El proyecto eliminado NO puede ser restaurado!\n" -"¿Estás TOTALMENTE seguro?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Va a eliminar %{project_name_with_namespace}. ¡El proyecto eliminado NO puede ser restaurado! ¿Estás TOTALMENTE seguro?" msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?" msgstr "Vas a eliminar el enlace de la bifurcación con el proyecto original %{forked_from_project}. ¿Estás TOTALMENTE seguro?" diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po index 90e2462039c..83f31f7a3b2 100644 --- a/locale/fr/gitlab.po +++ b/locale/fr/gitlab.po @@ -1175,22 +1175,11 @@ msgstr "Nous n'avons pas suffisamment de données pour afficher cette étape." msgid "Withdraw Access Request" msgstr "Retirer la demande d'accès" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Vous êtes sur le point de supprimer %{group_name}. Les groupes supprimés NE " -"PEUVENT PAS être restaurés ! Êtes vous ABSOLUMENT sûr ?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Vous êtes sur le point de supprimer %{group_name}. Les groupes supprimés NE PEUVENT PAS être restaurés ! Êtes vous ABSOLUMENT sûr ?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Vous êtes sur le point de supprimer %{project_name_with_namespace}.\n" -"Les projets supprimés NE PEUVENT PAS être restaurés !\n" -"Êtes vous ABSOLUMENT sûr ? " +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Vous êtes sur le point de supprimer %{project_name_with_namespace}. Les projets supprimés NE PEUVENT PAS être restaurés ! Êtes vous ABSOLUMENT sûr ?" msgid "" "You are going to remove the fork relationship to source project " diff --git a/locale/gitlab.pot b/locale/gitlab.pot index babef3ed0af..e60504e1395 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1072,16 +1072,10 @@ msgstr "" msgid "Withdraw Access Request" msgstr "" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" msgstr "" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" msgstr "" msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?" diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po index 7ba23d84405..e719a3988e3 100644 --- a/locale/it/gitlab.po +++ b/locale/it/gitlab.po @@ -1169,23 +1169,11 @@ msgstr "Non ci sono sufficienti dati da mostrare su questo stadio" msgid "Withdraw Access Request" msgstr "Ritira richiesta d'accesso" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Stai per rimuovere il gruppo %{group_name}.\n" -"I gruppi rimossi NON possono esser ripristinati!\n" -"Sei ASSOLUTAMENTE sicuro?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Stai per rimuovere il gruppo %{group_name}. I gruppi rimossi NON POSSONO esser ripristinati! Sei ASSOLUTAMENTE sicuro?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Stai per rimuovere %{project_name_with_namespace}.\n" -"I progetti rimossi NON POSSONO essere ripristinati\n" -"Sei assolutamente sicuro?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Stai per rimuovere %{project_name_with_namespace}. I progetti rimossi NON POSSONO essere ripristinati! Sei assolutamente sicuro?" msgid "" "You are going to remove the fork relationship to source project " diff --git a/locale/ja/gitlab.po b/locale/ja/gitlab.po index 0b1db651c11..bfa97aa21d7 100644 --- a/locale/ja/gitlab.po +++ b/locale/ja/gitlab.po @@ -1119,22 +1119,11 @@ msgstr "データ不足のため、このステージの表示はできません msgid "Withdraw Access Request" msgstr "アクセスリクエストを取り消す" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "%{group_name} グループを削除しようとしています。\n" -"削除されたグループは絶対に元に戻せません!\n" -"本当によろしいですか?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "%{group_name} グループを削除しようとしています。 削除されたグループは絶対に元に戻せません!本当によろしいですか?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"%{project_name_with_namespace} プロジェクトを削除しようとしています。\n" -"削除されたプロジェクトは絶対に元には戻せません!\n" -"本当によろしいですか?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "%{project_name_with_namespace} プロジェクトを削除しようとしています。削除されたプロジェクトは絶対に元には戻せません!本当によろしいですか?" msgid "" "You are going to remove the fork relationship to source project " diff --git a/locale/ko/gitlab.po b/locale/ko/gitlab.po index 0a6fbac0880..340c8955d20 100644 --- a/locale/ko/gitlab.po +++ b/locale/ko/gitlab.po @@ -1121,21 +1121,11 @@ msgstr "이 단계를 보여주기에 충분한 데이터가 없습니다." msgid "Withdraw Access Request" msgstr "액세스 요청 철회" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "%{group_name} 그룹을 제거하려고합니다.\n" -"\"정말로\" 확실합니까?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "%{group_name} 그룹을 제거하려고합니다. \"정말로\" 확실합니까?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"%{project_name_with_namespace} 프로젝트를 삭제하려고합니다.\n" -"삭제된 프로젝트를 복원 할 수 없습니다!\n" -"\"정말로\" 확실합니까?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "%{project_name_with_namespace} 프로젝트를 삭제하려고합니다. "삭제된 프로젝트를 복원 할 수 없습니다! \"정말로\" 확실합니까?" msgid "" "You are going to remove the fork relationship to source project " diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po index 2eaadb64124..a2df8ea549c 100644 --- a/locale/pt_BR/gitlab.po +++ b/locale/pt_BR/gitlab.po @@ -1164,30 +1164,12 @@ msgstr "Esta etapa não possui dados suficientes para exibição." msgid "Withdraw Access Request" msgstr "Remover Requisição de Acesso" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Você vai remover %{group_name}.\n" -"Grupos removidos NÃO PODEM ser restaurados!\n" -"Você está ABSOLUTAMENTE certo?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Você irá remover %{project_name_with_namespace}.\n" -"O projeto removido NÃO PODE ser restaurado!\n" -"Tem certeza ABSOLUTA?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Você vai remover %{group_name}. Grupos removidos NÃO PODEM ser restaurados! Você está ABSOLUTAMENTE certo?" -msgid "" -"You are going to remove the fork relationship to source project " -"%{forked_from_project}. Are you ABSOLUTELY sure?" -msgstr "" -"Você ira remover o relacionamento de fork com o projeto original " -"%{forked_from_project}. Tem certeza ABSOLUTA?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Você irá remover %{project_name_with_namespace}. O projeto removido NÃO PODE ser restaurado! Tem certeza ABSOLUTA?" msgid "" "You are going to transfer %{project_name_with_namespace} to another owner. " diff --git a/locale/ru/gitlab.po b/locale/ru/gitlab.po index 78f7b059077..6661232850a 100644 --- a/locale/ru/gitlab.po +++ b/locale/ru/gitlab.po @@ -1179,23 +1179,11 @@ msgstr "Информация по этапу отсутствует." msgid "Withdraw Access Request" msgstr "Отменить запрос доступа" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Вы собираетесь удалить %{group_name}.\n" -"Удаленные группы НЕ МОГУТ быть восстановлены!\n" -"Вы АБСОЛЮТНО уверены?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Вы собираетесь удалить %{group_name}. Удаленные группы НЕ МОГУТ быть восстановлены! Вы АБСОЛЮТНО уверены?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Вы хотите удалить %{project_name_with_namespace}.\n" -"Удаленный проект НЕ МОЖЕТ быть восстановлен!\n" -"Вы АБСОЛЮТНО уверены?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Вы хотите удалить %{project_name_with_namespace}. Удаленный проект НЕ МОЖЕТ быть восстановлен! Вы АБСОЛЮТНО уверены?" msgid "" "You are going to remove the fork relationship to source project " diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po index 78144d3755d..0ac0499e315 100644 --- a/locale/uk/gitlab.po +++ b/locale/uk/gitlab.po @@ -1173,23 +1173,11 @@ msgstr "Ми не маємо достатньо даних для показу msgid "Withdraw Access Request" msgstr "Скасувати запит доступу" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Ви хочете видалити %{group_name}.\n" -"Видалені групи НЕ МОЖНА буду відновити!\n" -"Ви АБСОЛЮТНО впевнені?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Ви хочете видалити %{group_name}. Видалені групи НЕ МОЖНА буду відновити! Ви АБСОЛЮТНО впевнені?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "" -"Ви хочете видалити %{project_name_with_namespace}.\n" -"Видалений проект НЕ МОЖЕ бути відновлений!\n" -"Ви АБСОЛЮТНО впевнені?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "Ви хочете видалити %{project_name_with_namespace}. Видалений проект НЕ МОЖЕ бути відновлений! Ви АБСОЛЮТНО впевнені?" msgid "" "You are going to remove the fork relationship to source project " diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po index 4a550db55d2..a3d0027212c 100644 --- a/locale/zh_CN/gitlab.po +++ b/locale/zh_CN/gitlab.po @@ -1101,18 +1101,10 @@ msgstr "该阶段的数据不足,无法显示。" msgid "Withdraw Access Request" msgstr "取消权限申请" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "即将删除 %{group_name}。\n" -"已删除的群组无法恢复!\n" -"确定继续吗?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "即将删除 %{group_name}。已删除的群组无法恢复!确定继续吗?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" msgstr "即将要删除 %{project_name_with_namespace}。已删除的项目无法恢复!确定继续吗?" msgid "" diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po index 69b2bf80dbf..f4d33862a36 100644 --- a/locale/zh_HK/gitlab.po +++ b/locale/zh_HK/gitlab.po @@ -1100,18 +1100,10 @@ msgstr "該階段的數據不足,無法顯示。" msgid "Withdraw Access Request" msgstr "取消權限申请" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "即將刪除 %{group_name}。\n" -"已刪除的群組無法恢復!\n" -"確定繼續嗎?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "即將刪除 %{group_name}。已刪除的群組無法恢復!確定繼續嗎?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" msgstr "即將要刪除 %{project_name_with_namespace}。已刪除的項目無法恢複!確定繼續嗎?" msgid "" diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po index 4fd728659c6..205d4712316 100644 --- a/locale/zh_TW/gitlab.po +++ b/locale/zh_TW/gitlab.po @@ -1111,18 +1111,10 @@ msgstr "因該階段的資料不足而無法顯示相關資訊" msgid "Withdraw Access Request" msgstr "取消權限申請" -msgid "" -"You are going to remove %{group_name}.\n" -"Removed groups CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" -msgstr "即將要刪除 %{group_name}。\n" -"被刪除的群組完全無法救回來喔!\n" -"真的「100%確定」要這麼做嗎?" +msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" +msgstr "即將要刪除 %{group_name}。被刪除的群組完全無法救回來喔!真的「100%確定」要這麼做嗎?" -msgid "" -"You are going to remove %{project_name_with_namespace}.\n" -"Removed project CANNOT be restored!\n" -"Are you ABSOLUTELY sure?" +msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?" msgstr "即將要刪除 %{project_name_with_namespace}。被刪除的專案完全無法救回來喔!真的「100%確定」要這麼做嗎?" msgid "" From d7b03c37f8346e29f21f0196fd3a532effa60ec3 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Fri, 11 Aug 2017 15:19:11 +0100 Subject: [PATCH 080/141] Speed up Group#user_ids_for_project_authorizations --- app/mailers/emails/members.rb | 4 +-- app/models/group.rb | 30 +++++++++++++++---- app/models/member.rb | 15 ++++++++-- app/models/namespace.rb | 14 +++++++++ ...-speed-up-group-project-authorizations.yml | 5 ++++ spec/models/namespace_spec.rb | 30 +++++++++++++++++++ 6 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 changelogs/unreleased/34533-speed-up-group-project-authorizations.yml diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb index 7b617b359ea..d76c61c369f 100644 --- a/app/mailers/emails/members.rb +++ b/app/mailers/emails/members.rb @@ -11,11 +11,11 @@ module Emails @member_source_type = member_source_type @member_id = member_id - admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email) + admins = member_source.members.owners_and_masters.pluck(:notification_email) # A project in a group can have no explicit owners/masters, in that case # we fallbacks to the group's owners/masters. if admins.empty? && member_source.respond_to?(:group) && member_source.group - admins = member_source.group.members.owners_and_masters.includes(:user).pluck(:notification_email) + admins = member_source.group.members.owners_and_masters.pluck(:notification_email) end mail(to: admins, diff --git a/app/models/group.rb b/app/models/group.rb index bd5735ed82e..2816a68257c 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -212,21 +212,39 @@ class Group < Namespace end def user_ids_for_project_authorizations - users_with_parents.pluck(:id) + members_with_parents.pluck(:user_id) end def members_with_parents - GroupMember.active.where(source_id: ancestors.pluck(:id).push(id)).where.not(user_id: nil) + # Avoids an unnecessary SELECT when the group has no parents + source_ids = + if parent_id + self_and_ancestors.reorder(nil).select(:id) + else + id + end + + GroupMember + .active_without_invites + .where(source_id: source_ids) + end + + def members_with_descendants + GroupMember + .active_without_invites + .where(source_id: self_and_descendants.reorder(nil).select(:id)) end def users_with_parents - User.where(id: members_with_parents.select(:user_id)) + User + .where(id: members_with_parents.select(:user_id)) + .reorder(nil) end def users_with_descendants - members_with_descendants = GroupMember.non_request.where(source_id: descendants.pluck(:id).push(id)) - - User.where(id: members_with_descendants.select(:user_id)) + User + .where(id: members_with_descendants.select(:user_id)) + .reorder(nil) end def max_member_access_for_user(user) diff --git a/app/models/member.rb b/app/models/member.rb index dc9247bc9a0..17e343c84d8 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -41,9 +41,20 @@ class Member < ActiveRecord::Base is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil)) user_is_active = User.arel_table[:state].eq(:active) - includes(:user).references(:users) - .where(is_external_invite.or(user_is_active)) + user_ok = Arel::Nodes::Grouping.new(is_external_invite).or(user_is_active) + + left_join_users + .where(user_ok) .where(requested_at: nil) + .reorder(nil) + end + + # Like active, but without invites. For when a User is required. + scope :active_without_invites, -> do + left_join_users + .where(users: { state: 'active' }) + .where(requested_at: nil) + .reorder(nil) end scope :invite, -> { where.not(invite_token: nil) } diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 6073fb94a3f..e7bc1d1b080 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -156,6 +156,14 @@ class Namespace < ActiveRecord::Base .base_and_ancestors end + def self_and_ancestors + return self.class.where(id: id) unless parent_id + + Gitlab::GroupHierarchy + .new(self.class.where(id: id)) + .base_and_ancestors + end + # Returns all the descendants of the current namespace. def descendants Gitlab::GroupHierarchy @@ -163,6 +171,12 @@ class Namespace < ActiveRecord::Base .base_and_descendants end + def self_and_descendants + Gitlab::GroupHierarchy + .new(self.class.where(id: id)) + .base_and_descendants + end + def user_ids_for_project_authorizations [owner_id] end diff --git a/changelogs/unreleased/34533-speed-up-group-project-authorizations.yml b/changelogs/unreleased/34533-speed-up-group-project-authorizations.yml new file mode 100644 index 00000000000..ddaaf4a2507 --- /dev/null +++ b/changelogs/unreleased/34533-speed-up-group-project-authorizations.yml @@ -0,0 +1,5 @@ +--- +title: Fix timeouts when creating projects in groups with many members +merge_request: 13508 +author: +type: fixed diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 1a00c50690c..69286eff984 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -315,6 +315,20 @@ describe Namespace do end end + describe '#self_and_ancestors', :nested_groups do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let(:deep_nested_group) { create(:group, parent: nested_group) } + let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + + it 'returns the correct ancestors' do + expect(very_deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group) + expect(deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group) + expect(nested_group.self_and_ancestors).to contain_exactly(group, nested_group) + expect(group.self_and_ancestors).to contain_exactly(group) + end + end + describe '#descendants', :nested_groups do let!(:group) { create(:group, path: 'git_lab') } let!(:nested_group) { create(:group, parent: group) } @@ -331,6 +345,22 @@ describe Namespace do end end + describe '#self_and_descendants', :nested_groups do + let!(:group) { create(:group, path: 'git_lab') } + let!(:nested_group) { create(:group, parent: group) } + let!(:deep_nested_group) { create(:group, parent: nested_group) } + let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + let!(:another_group) { create(:group, path: 'gitllab') } + let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) } + + it 'returns the correct descendants' do + expect(very_deep_nested_group.self_and_descendants).to contain_exactly(very_deep_nested_group) + expect(deep_nested_group.self_and_descendants).to contain_exactly(deep_nested_group, very_deep_nested_group) + expect(nested_group.self_and_descendants).to contain_exactly(nested_group, deep_nested_group, very_deep_nested_group) + expect(group.self_and_descendants).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group) + end + end + describe '#users_with_descendants', :nested_groups do let(:user_a) { create(:user) } let(:user_b) { create(:user) } From c1f9403e45e636651010929b6113add34f8e6a8a Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 10 Aug 2017 15:01:38 +0200 Subject: [PATCH 081/141] Use Prev/Next pagination for exploring projects This changes the pagination of the "Explore" pages so they use a simpler pagination system that only shows "Prev" and "Next" buttons. This removes the need for getting the total number of rows to display, a process that can easily take up to 2 seconds when browsing through a large list of projects. Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/27390 --- Gemfile | 2 +- Gemfile.lock | 17 ++++++++++---- .../explore/projects_controller.rb | 11 +++++---- app/helpers/pagination_helper.rb | 21 +++++++++++++++++ .../kaminari/gitlab/_without_count.html.haml | 8 +++++++ app/views/shared/projects/_list.html.haml | 2 +- .../pagination-projects-explore.yml | 4 ++++ spec/helpers/pagination_helper_spec.rb | 23 +++++++++++++++++++ 8 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 app/helpers/pagination_helper.rb create mode 100644 app/views/kaminari/gitlab/_without_count.html.haml create mode 100644 changelogs/unreleased/pagination-projects-explore.yml create mode 100644 spec/helpers/pagination_helper_spec.rb diff --git a/Gemfile b/Gemfile index a768fa428bf..93363edf66e 100644 --- a/Gemfile +++ b/Gemfile @@ -84,7 +84,7 @@ gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' gem 'hashie-forbidden_attributes' # Pagination -gem 'kaminari', '~> 0.17.0' +gem 'kaminari', '~> 1.0' # HAML gem 'hamlit', '~> 2.6.1' diff --git a/Gemfile.lock b/Gemfile.lock index d443209349d..3e661d4e3ad 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -419,9 +419,18 @@ GEM json-schema (2.6.2) addressable (~> 2.3.8) jwt (1.5.6) - kaminari (0.17.0) - actionpack (>= 3.0.0) - activesupport (>= 3.0.0) + kaminari (1.0.1) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.0.1) + kaminari-activerecord (= 1.0.1) + kaminari-core (= 1.0.1) + kaminari-actionview (1.0.1) + actionview + kaminari-core (= 1.0.1) + kaminari-activerecord (1.0.1) + activerecord + kaminari-core (= 1.0.1) + kaminari-core (1.0.1) kgio (2.10.0) knapsack (1.11.0) rake @@ -1011,7 +1020,7 @@ DEPENDENCIES jquery-rails (~> 4.1.0) json-schema (~> 2.6.2) jwt (~> 1.5.6) - kaminari (~> 0.17.0) + kaminari (~> 1.0) knapsack (~> 1.11.0) kubeclient (~> 2.2.0) letter_opener_web (~> 1.3.0) diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 741879dee35..762c6ebf3a3 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -6,7 +6,7 @@ class Explore::ProjectsController < Explore::ApplicationController def index params[:sort] ||= 'latest_activity_desc' @sort = params[:sort] - @projects = load_projects.page(params[:page]) + @projects = load_projects respond_to do |format| format.html @@ -21,7 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController def trending params[:trending] = true @sort = params[:sort] - @projects = load_projects.page(params[:page]) + @projects = load_projects respond_to do |format| format.html @@ -34,7 +34,7 @@ class Explore::ProjectsController < Explore::ApplicationController end def starred - @projects = load_projects.reorder('star_count DESC').page(params[:page]) + @projects = load_projects.reorder('star_count DESC') respond_to do |format| format.html @@ -50,6 +50,9 @@ class Explore::ProjectsController < Explore::ApplicationController def load_projects ProjectsFinder.new(current_user: current_user, params: params) - .execute.includes(:route, namespace: :route) + .execute + .includes(:route, namespace: :route) + .page(params[:page]) + .without_count end end diff --git a/app/helpers/pagination_helper.rb b/app/helpers/pagination_helper.rb new file mode 100644 index 00000000000..83dd76a01dd --- /dev/null +++ b/app/helpers/pagination_helper.rb @@ -0,0 +1,21 @@ +module PaginationHelper + def paginate_collection(collection, remote: nil) + if collection.is_a?(Kaminari::PaginatableWithoutCount) + paginate_without_count(collection) + elsif collection.respond_to?(:total_pages) + paginate_with_count(collection, remote: remote) + end + end + + def paginate_without_count(collection) + render( + 'kaminari/gitlab/without_count', + previous_path: path_to_prev_page(collection), + next_path: path_to_next_page(collection) + ) + end + + def paginate_with_count(collection, remote: nil) + paginate(collection, remote: remote, theme: 'gitlab') + end +end diff --git a/app/views/kaminari/gitlab/_without_count.html.haml b/app/views/kaminari/gitlab/_without_count.html.haml new file mode 100644 index 00000000000..250029c4475 --- /dev/null +++ b/app/views/kaminari/gitlab/_without_count.html.haml @@ -0,0 +1,8 @@ +.gl-pagination + %ul.pagination.clearfix + - if previous_path + %li.prev + = link_to(t('views.pagination.previous'), previous_path, rel: 'prev') + - if next_path + %li.next + = link_to(t('views.pagination.next'), next_path, rel: 'next') diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index 914506bf0ce..0bedfea3502 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -23,6 +23,6 @@ = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon') %strong= pluralize(@private_forks_count, 'private fork') %span  you have no access to. - = paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages + = paginate_collection(projects, remote: remote) - else .nothing-here-block No projects found diff --git a/changelogs/unreleased/pagination-projects-explore.yml b/changelogs/unreleased/pagination-projects-explore.yml new file mode 100644 index 00000000000..dc9c4218793 --- /dev/null +++ b/changelogs/unreleased/pagination-projects-explore.yml @@ -0,0 +1,4 @@ +--- +title: Use Prev/Next pagination for exploring projects +merge_request: +author: diff --git a/spec/helpers/pagination_helper_spec.rb b/spec/helpers/pagination_helper_spec.rb new file mode 100644 index 00000000000..e235475fb47 --- /dev/null +++ b/spec/helpers/pagination_helper_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe PaginationHelper do + describe '#paginate_collection' do + let(:collection) { User.all.page(1) } + + it 'paginates a collection without using a COUNT' do + without_count = collection.without_count + + expect(helper).to receive(:paginate_without_count) + .with(without_count) + .and_call_original + + helper.paginate_collection(without_count) + end + + it 'paginates a collection using a COUNT' do + expect(helper).to receive(:paginate_with_count).and_call_original + + helper.paginate_collection(collection) + end + end +end From d1b4770d6c4f287def26f1990654a00f0990ed65 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 14 Aug 2017 20:06:55 +0800 Subject: [PATCH 082/141] Add comments about why we're stubbing them --- spec/requests/api/merge_requests_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 81ea1f0262b..7b7d84f9bbf 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -583,8 +583,8 @@ describe API::MergeRequests do before do fork_project.add_reporter(user2) - allow_any_instance_of(Repository).to receive(:fetch_ref) - allow_any_instance_of(Repository).to receive(:write_ref) + allow_any_instance_of(Repository).to receive(:fetch_ref) # for forks + allow_any_instance_of(Repository).to receive(:write_ref) # for non-forks end it "returns merge_request" do From 5c13c734baf2d71191f4f0487f5ff1eb4e5c7708 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 14 Aug 2017 12:51:24 +0000 Subject: [PATCH 083/141] Revert "Merge branch 'zj-fix-charlock_holmes' into 'master'" This reverts merge request !13523 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index ab01a556561..d443209349d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -113,7 +113,7 @@ GEM activesupport (>= 4.0.0) mime-types (>= 1.16) cause (0.1) - charlock_holmes (0.7.4) + charlock_holmes (0.7.3) chronic (0.10.2) chronic_duration (0.10.6) numerizer (~> 0.1.1) From f01e34df3ffc4e38c51b23c0c62b04d25c7b3696 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 14 Aug 2017 14:52:04 +0100 Subject: [PATCH 084/141] Stops propagation for dropdown content in pipeline graph. Applies the same behavior of the mini graph --- .../graph/dropdown_job_component.vue | 21 +++++++++++++++++++ .../36385-pipeline-graph-dropdown.yml | 5 +++++ 2 files changed, 26 insertions(+) create mode 100644 changelogs/unreleased/36385-pipeline-graph-dropdown.yml diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue index 2944689a5a7..7695b04db74 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -48,6 +48,27 @@ return `${this.job.name} - ${this.job.status.label}`; }, }, + + methods: { + /** + * When the user right clicks or cmd/ctrl + click in the job name + * the dropdown should not be closed and the link should open in another tab, + * so we stop propagation of the click event inside the dropdown. + * + * Since this component is rendered multiple times per page we need to guarantee we only + * target the click event of this component. + */ + stopDropdownClickPropagation() { + $(this.$el.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item')) + .on('click', (e) => { + e.stopPropagation(); + }); + }, + }, + + mounted() { + this.stopDropdownClickPropagation(); + }, }; \ No newline at end of file + From 06c330954e030e30e9e8284110907c53b206e447 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 10 Aug 2017 13:09:01 -0500 Subject: [PATCH 092/141] Use v-else instead of duplicating logic Fix http://192.168.1.135:3000/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/broadcast_message.js --- .../repo/components/repo_preview.vue | 20 ++++++++++++++++--- .../javascripts/repo/helpers/repo_helper.js | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue index 0caa3a4551a..2200754cbef 100644 --- a/app/assets/javascripts/repo/components/repo_preview.vue +++ b/app/assets/javascripts/repo/components/repo_preview.vue @@ -30,9 +30,23 @@ export default { diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js index fee98c12592..b12ea92c17a 100644 --- a/app/assets/javascripts/repo/helpers/repo_helper.js +++ b/app/assets/javascripts/repo/helpers/repo_helper.js @@ -190,7 +190,7 @@ const RepoHelper = { newFile.url = file.url || location.pathname; newFile.url = file.url; - if (newFile.render_error === 'too_large') { + if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') { newFile.tooLarge = true; } newFile.newContent = ''; From aef9f1eb9405e9bab92b15f5c99bf06eaf28a5d6 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 14 Aug 2017 15:22:09 +0200 Subject: [PATCH 093/141] Cache the number of forks of a project The number of forks of a project doesn't change very frequently and running a COUNT(*) every time this information is requested can be quite expensive. We also end up running such a COUNT(*) query at least twice on the homepage of a project. By caching this data and refreshing it when necessary we can reduce project homepage loading times by around 60 milliseconds (based on the timings of https://gitlab.com/gitlab-org/gitlab-ce). --- app/models/project.rb | 5 ++- app/services/projects/destroy_service.rb | 2 + app/services/projects/fork_service.rb | 6 +++ app/services/projects/forks_count_service.rb | 30 ++++++++++++++ app/services/projects/unlink_fork_service.rb | 6 +++ changelogs/unreleased/forks-count-cache.yml | 5 +++ lib/api/projects.rb | 2 + lib/api/v3/projects.rb | 2 + spec/models/project_spec.rb | 10 +++++ spec/requests/api/projects_spec.rb | 8 ++++ spec/requests/api/v3/projects_spec.rb | 8 ++++ spec/services/projects/fork_service_spec.rb | 8 ++++ .../projects/forks_count_service_spec.rb | 40 +++++++++++++++++++ .../projects/unlink_fork_service_spec.rb | 10 +++++ 14 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 app/services/projects/forks_count_service.rb create mode 100644 changelogs/unreleased/forks-count-cache.yml create mode 100644 spec/services/projects/forks_count_service_spec.rb diff --git a/app/models/project.rb b/app/models/project.rb index 7010664e1c8..92ca126bafc 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -196,7 +196,6 @@ class Project < ActiveRecord::Base accepts_nested_attributes_for :import_data delegate :name, to: :owner, allow_nil: true, prefix: true - delegate :count, to: :forks, prefix: true delegate :members, to: :team, prefix: true delegate :add_user, :add_users, to: :team delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team @@ -1398,6 +1397,10 @@ class Project < ActiveRecord::Base # @deprecated cannot remove yet because it has an index with its name in elasticsearch alias_method :path_with_namespace, :full_path + def forks_count + Projects::ForksCountService.new(self).count + end + private def cross_namespace_reference?(from) diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 11ad4838471..54eb75ab9bf 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -128,6 +128,8 @@ module Projects project.repository.before_delete Repository.new(wiki_path, project, disk_path: repo_path).before_delete + + Projects::ForksCountService.new(project).delete_cache end end end diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index a2b23ea6171..ad67e68a86a 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -21,11 +21,17 @@ module Projects builds_access_level = @project.project_feature.builds_access_level new_project.project_feature.update_attributes(builds_access_level: builds_access_level) + refresh_forks_count + new_project end private + def refresh_forks_count + Projects::ForksCountService.new(@project).refresh_cache + end + def allowed_visibility_level project_level = @project.visibility_level diff --git a/app/services/projects/forks_count_service.rb b/app/services/projects/forks_count_service.rb new file mode 100644 index 00000000000..e2e2b1da91d --- /dev/null +++ b/app/services/projects/forks_count_service.rb @@ -0,0 +1,30 @@ +module Projects + # Service class for getting and caching the number of forks of a project. + class ForksCountService + def initialize(project) + @project = project + end + + def count + Rails.cache.fetch(cache_key) { uncached_count } + end + + def refresh_cache + Rails.cache.write(cache_key, uncached_count) + end + + def delete_cache + Rails.cache.delete(cache_key) + end + + private + + def uncached_count + @project.forks.count + end + + def cache_key + ['projects', @project.id, 'forks_count'] + end + end +end diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb index f385e426827..f30b40423c8 100644 --- a/app/services/projects/unlink_fork_service.rb +++ b/app/services/projects/unlink_fork_service.rb @@ -13,7 +13,13 @@ module Projects ::MergeRequests::CloseService.new(@project, @current_user).execute(mr) end + refresh_forks_count(@project.forked_from_project) + @project.forked_project_link.destroy end + + def refresh_forks_count(project) + Projects::ForksCountService.new(project).refresh_cache + end end end diff --git a/changelogs/unreleased/forks-count-cache.yml b/changelogs/unreleased/forks-count-cache.yml new file mode 100644 index 00000000000..da8c53c2abd --- /dev/null +++ b/changelogs/unreleased/forks-count-cache.yml @@ -0,0 +1,5 @@ +--- +title: Cache the number of forks of a project +merge_request: 13535 +author: +type: other diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 89dda88d3f5..15c3832b032 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -351,6 +351,8 @@ module API if user_project.forked_from_project.nil? user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id) + + ::Projects::ForksCountService.new(forked_from_project).refresh_cache else render_api_error!("Project already forked", 409) end diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb index eb090453b48..449876c10d9 100644 --- a/lib/api/v3/projects.rb +++ b/lib/api/v3/projects.rb @@ -388,6 +388,8 @@ module API if user_project.forked_from_project.nil? user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id) + + ::Projects::ForksCountService.new(forked_from_project).refresh_cache else render_api_error!("Project already forked", 409) end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index d9ab44dc49f..eba71ba2f72 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2310,4 +2310,14 @@ describe Project do end end end + + describe '#forks_count' do + it 'returns the number of forks' do + project = build(:project) + + allow(project.forks).to receive(:count).and_return(1) + + expect(project.forks_count).to eq(1) + end + end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 6cb27d16fe5..a89a58ff713 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1065,6 +1065,14 @@ describe API::Projects do expect(project_fork_target.forked?).to be_truthy end + it 'refreshes the forks count cachce' do + expect(project_fork_source.forks_count).to be_zero + + post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) + + expect(project_fork_source.forks_count).to eq(1) + end + it 'fails if forked_from project which does not exist' do post api("/projects/#{project_fork_target.id}/fork/9999", admin) expect(response).to have_http_status(404) diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index fca5b5b5d82..a514166274a 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -1004,6 +1004,14 @@ describe API::V3::Projects do expect(project_fork_target.forked?).to be_truthy end + it 'refreshes the forks count cachce' do + expect(project_fork_source.forks_count).to be_zero + + post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) + + expect(project_fork_source.forks_count).to eq(1) + end + it 'fails if forked_from project which does not exist' do post v3_api("/projects/#{project_fork_target.id}/fork/9999", admin) expect(response).to have_http_status(404) diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index c90536ba346..21c4b30734c 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -50,6 +50,14 @@ describe Projects::ForkService do expect(@from_project.avatar.file).to be_exists end + + it 'flushes the forks count cache of the source project' do + expect(@from_project.forks_count).to be_zero + + fork_project(@from_project, @to_user) + + expect(@from_project.forks_count).to eq(1) + end end end diff --git a/spec/services/projects/forks_count_service_spec.rb b/spec/services/projects/forks_count_service_spec.rb new file mode 100644 index 00000000000..cf299c5d09b --- /dev/null +++ b/spec/services/projects/forks_count_service_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Projects::ForksCountService do + let(:project) { build(:project, id: 42) } + let(:service) { described_class.new(project) } + + describe '#count' do + it 'returns the number of forks' do + allow(service).to receive(:uncached_count).and_return(1) + + expect(service.count).to eq(1) + end + + it 'caches the forks count', :use_clean_rails_memory_store_caching do + expect(service).to receive(:uncached_count).once.and_return(1) + + 2.times { service.count } + end + end + + describe '#refresh_cache', :use_clean_rails_memory_store_caching do + it 'refreshes the cache' do + expect(service).to receive(:uncached_count).once.and_return(1) + + service.refresh_cache + + expect(service.count).to eq(1) + end + end + + describe '#delete_cache', :use_clean_rails_memory_store_caching do + it 'removes the cache' do + expect(service).to receive(:uncached_count).twice.and_return(1) + + service.count + service.delete_cache + service.count + end + end +end diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb index 2ae8d5f7c54..4f1ab697460 100644 --- a/spec/services/projects/unlink_fork_service_spec.rb +++ b/spec/services/projects/unlink_fork_service_spec.rb @@ -29,4 +29,14 @@ describe Projects::UnlinkForkService do subject.execute end + + it 'refreshes the forks count cache of the source project' do + source = fork_project.forked_from_project + + expect(source.forks_count).to eq(1) + + subject.execute + + expect(source.forks_count).to be_zero + end end From f1e1113bf4d107c0ecf3f989f6110b00a83cef2d Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 10 Aug 2017 13:50:59 -0500 Subject: [PATCH 094/141] Split out linkClicked and add tests Fix https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12198#note_37143174 --- .../repo/components/repo_sidebar.vue | 45 ++++++++--------- .../repo/components/repo_sidebar_spec.js | 48 +++++++++++++++++++ 2 files changed, 69 insertions(+), 24 deletions(-) diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue index d6d832efc49..ccc84c4ed7c 100644 --- a/app/assets/javascripts/repo/components/repo_sidebar.vue +++ b/app/assets/javascripts/repo/components/repo_sidebar.vue @@ -33,32 +33,29 @@ const RepoSidebar = { }); }, - linkClicked(clickedFile) { - let url = ''; + fileClicked(clickedFile) { let file = clickedFile; - if (typeof file === 'object') { - file.loading = true; - if (file.type === 'tree' && file.opened) { - file = Store.removeChildFilesOfTree(file); + + file.loading = true; + if (file.type === 'tree' && file.opened) { + file = Store.removeChildFilesOfTree(file); + file.loading = false; + } else { + Service.url = file.url; + // I need to refactor this to do the `then` here. + // Not a callback. For now this is good enough. + // it works. + Helper.getContent(file, () => { file.loading = false; - } else { - url = file.url; - Service.url = url; - // I need to refactor this to do the `then` here. - // Not a callback. For now this is good enough. - // it works. - Helper.getContent(file, () => { - file.loading = false; - Helper.scrollTabsRight(); - }); - } - } else if (typeof file === 'string') { - // go back - url = file; - Service.url = url; - Helper.getContent(null, () => Helper.scrollTabsRight()); + Helper.scrollTabsRight(); + }); } }, + + goToPreviousDirectoryClicked(prevURL) { + Service.url = prevURL; + Helper.getContent(null, () => Helper.scrollTabsRight()); + }, }, }; @@ -82,7 +79,7 @@ export default RepoSidebar; + @linkclicked="goToPreviousDirectoryClicked(prevURL)"/> diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js index 0d216c9c026..e8bc8a62240 100644 --- a/spec/javascripts/repo/components/repo_sidebar_spec.js +++ b/spec/javascripts/repo/components/repo_sidebar_spec.js @@ -1,4 +1,6 @@ import Vue from 'vue'; +import Helper from '~/repo/helpers/repo_helper'; +import RepoService from '~/repo/services/repo_service'; import RepoStore from '~/repo/stores/repo_store'; import repoSidebar from '~/repo/components/repo_sidebar.vue'; @@ -58,4 +60,50 @@ describe('RepoSidebar', () => { expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy(); }); + + describe('methods', () => { + describe('fileClicked', () => { + it('should fetch data for new file', () => { + spyOn(Helper, 'getContent'); + const file1 = { + id: 0, + }; + RepoStore.files = [file1]; + RepoStore.isRoot = true; + const vm = createComponent(); + + vm.fileClicked(file1); + + expect(Helper.getContent).toHaveBeenCalledWith(file1, jasmine.any(Function)); + }); + + it('should hide files in directory if already open', () => { + spyOn(RepoStore, 'removeChildFilesOfTree').and.callThrough(); + const file1 = { + id: 0, + type: 'tree', + url: '', + opened: true, + }; + RepoStore.files = [file1]; + RepoStore.isRoot = true; + const vm = createComponent(); + + vm.fileClicked(file1); + + expect(RepoStore.removeChildFilesOfTree).toHaveBeenCalledWith(file1); + }); + }); + + describe('goToPreviousDirectoryClicked', () => { + it('should hide files in directory if already open', () => { + const prevUrl = 'foo/bar'; + const vm = createComponent(); + + vm.goToPreviousDirectoryClicked(prevUrl); + + expect(RepoService.url).toEqual(prevUrl); + }); + }); + }); }); From ecb7c534f60f89a57468148f543a6895692b6081 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 10 Aug 2017 14:01:32 -0500 Subject: [PATCH 095/141] Use promise syntax with Helper.getContent Fix https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12198#note_37143217 --- .../repo/components/repo_sidebar.vue | 17 +++++++++-------- .../javascripts/repo/helpers/repo_helper.js | 3 +-- .../repo/components/repo_sidebar_spec.js | 5 +++-- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue index ccc84c4ed7c..0d4f8c6635e 100644 --- a/app/assets/javascripts/repo/components/repo_sidebar.vue +++ b/app/assets/javascripts/repo/components/repo_sidebar.vue @@ -42,19 +42,20 @@ const RepoSidebar = { file.loading = false; } else { Service.url = file.url; - // I need to refactor this to do the `then` here. - // Not a callback. For now this is good enough. - // it works. - Helper.getContent(file, () => { - file.loading = false; - Helper.scrollTabsRight(); - }); + Helper.getContent(file) + .then(() => { + file.loading = false; + Helper.scrollTabsRight(); + }) + .catch(Helper.loadingError); } }, goToPreviousDirectoryClicked(prevURL) { Service.url = prevURL; - Helper.getContent(null, () => Helper.scrollTabsRight()); + Helper.getContent(null) + .then(() => Helper.scrollTabsRight()) + .catch(Helper.loadingError); }, }, }; diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js index b12ea92c17a..308ede5fba0 100644 --- a/app/assets/javascripts/repo/helpers/repo_helper.js +++ b/app/assets/javascripts/repo/helpers/repo_helper.js @@ -135,14 +135,13 @@ const RepoHelper = { return isRoot; }, - getContent(treeOrFile, cb) { + getContent(treeOrFile) { let file = treeOrFile; // const loadingData = RepoHelper.setLoading(true); return Service.getContent() .then((response) => { const data = response.data; // RepoHelper.setLoading(false, loadingData); - if (cb) cb(); Store.isTree = RepoHelper.isTree(data); if (!Store.isTree) { if (!file) file = data; diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js index e8bc8a62240..edd27d3afb8 100644 --- a/spec/javascripts/repo/components/repo_sidebar_spec.js +++ b/spec/javascripts/repo/components/repo_sidebar_spec.js @@ -64,9 +64,10 @@ describe('RepoSidebar', () => { describe('methods', () => { describe('fileClicked', () => { it('should fetch data for new file', () => { - spyOn(Helper, 'getContent'); + spyOn(Helper, 'getContent').and.callThrough(); const file1 = { id: 0, + url: '', }; RepoStore.files = [file1]; RepoStore.isRoot = true; @@ -74,7 +75,7 @@ describe('RepoSidebar', () => { vm.fileClicked(file1); - expect(Helper.getContent).toHaveBeenCalledWith(file1, jasmine.any(Function)); + expect(Helper.getContent).toHaveBeenCalledWith(file1); }); it('should hide files in directory if already open', () => { From a081a60d89af1f05a8f6f243e073a96e35b2b349 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 10 Aug 2017 14:17:38 -0500 Subject: [PATCH 096/141] Make close/changed icon more accessible Fix https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12198#note_37143527 --- .../javascripts/repo/components/repo_tab.vue | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/repo/components/repo_tab.vue index 712d64c236f..b6a9948f487 100644 --- a/app/assets/javascripts/repo/components/repo_tab.vue +++ b/app/assets/javascripts/repo/components/repo_tab.vue @@ -10,6 +10,12 @@ const RepoTab = { }, computed: { + closeLabel() { + if (this.tab.changed) { + return `${this.tab.name} changed`; + } + return `Close ${this.tab.name}`; + }, changedClass() { const tabChangedObj = { 'fa-times': !this.tab.changed, @@ -34,8 +40,17 @@ export default RepoTab;