We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Unclear instructions for GitLab integration setup
> Step 1: Create a Sentry App in GitLab

> To configure GitLab with Sentry, you will need to create a Sentry app in your GitLab instance.

> You'll also need to be a maintainer or owner in GitLab. Projects owned by users are not supported.

> Navigate to the **User Settings** section of your GitLab instance.

> In the sidebar, select 'Applications'.

Did the author mean `https://gitlaburl/admin`? If so, this is called `Admin Area`:

![image](https://user-images.githubusercontent.com/611746/75067118-f71c6800-54b1-11ea-8c30-4d24449abe9b.png)

Also, providing a direct URL won't hurt. Why not something like this:

> Navigate to the Admin Area of your GitLab instance, then select "Applications" in the sidebar. (Or navigate directly to /admin/applications/new)

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/sentry/integrations/gitlab/integration.py`
Content:
```
1 from __future__ import absolute_import
2 
3 import six
4 from six.moves.urllib.parse import urlparse
5 from django.utils.translation import ugettext_lazy as _
6 from django import forms
7 
8 from sentry.web.helpers import render_to_response
9 from sentry.identity.pipeline import IdentityProviderPipeline
10 from sentry.identity.gitlab import get_user_info, get_oauth_data
11 from sentry.identity.gitlab.provider import GitlabIdentityProvider
12 from sentry.integrations import (
13     FeatureDescription,
14     IntegrationInstallation,
15     IntegrationFeatures,
16     IntegrationProvider,
17     IntegrationMetadata,
18 )
19 from sentry.integrations.exceptions import ApiError, IntegrationError
20 from sentry.integrations.repositories import RepositoryMixin
21 from sentry.pipeline import NestedPipelineView, PipelineView
22 from sentry.utils.http import absolute_uri
23 from sentry.utils.hashlib import sha1_text
24 
25 from .client import GitLabApiClient, GitLabSetupClient
26 from .issues import GitlabIssueBasic
27 from .repository import GitlabRepositoryProvider
28 
29 DESCRIPTION = """
30 Connect your Sentry organization to an organization in your GitLab instance or gitlab.com, enabling the following features:
31 """
32 
33 FEATURES = [
34     FeatureDescription(
35         """
36         Track commits and releases (learn more
37         [here](https://docs.sentry.io/learn/releases/))
38         """,
39         IntegrationFeatures.COMMITS,
40     ),
41     FeatureDescription(
42         """
43         Resolve Sentry issues via GitLab commits and merge requests by
44         including `Fixes PROJ-ID` in the message
45         """,
46         IntegrationFeatures.COMMITS,
47     ),
48     FeatureDescription(
49         """
50         Create GitLab issues from Sentry
51         """,
52         IntegrationFeatures.ISSUE_BASIC,
53     ),
54     FeatureDescription(
55         """
56         Link Sentry issues to existing GitLab issues
57         """,
58         IntegrationFeatures.ISSUE_BASIC,
59     ),
60 ]
61 
62 metadata = IntegrationMetadata(
63     description=DESCRIPTION.strip(),
64     features=FEATURES,
65     author="The Sentry Team",
66     noun=_("Installation"),
67     issue_url="https://github.com/getsentry/sentry/issues/",
68     source_url="https://github.com/getsentry/sentry/tree/master/src/sentry/integrations/gitlab",
69     aspects={},
70 )
71 
72 
73 class GitlabIntegration(IntegrationInstallation, GitlabIssueBasic, RepositoryMixin):
74     repo_search = True
75 
76     def __init__(self, *args, **kwargs):
77         super(GitlabIntegration, self).__init__(*args, **kwargs)
78         self.default_identity = None
79 
80     def get_group_id(self):
81         return self.model.metadata["group_id"]
82 
83     def get_client(self):
84         if self.default_identity is None:
85             self.default_identity = self.get_default_identity()
86 
87         return GitLabApiClient(self)
88 
89     def get_repositories(self, query=None):
90         # Note: gitlab projects are the same things as repos everywhere else
91         group = self.get_group_id()
92         resp = self.get_client().search_group_projects(group, query)
93         return [{"identifier": repo["id"], "name": repo["name_with_namespace"]} for repo in resp]
94 
95     def search_projects(self, query):
96         client = self.get_client()
97         group_id = self.get_group_id()
98         return client.search_group_projects(group_id, query)
99 
100     def search_issues(self, project_id, query, iids):
101         client = self.get_client()
102         return client.search_project_issues(project_id, query, iids)
103 
104     def error_message_from_json(self, data):
105         """
106         Extract error messages from gitlab API errors.
107         Generic errors come in the `error` key while validation errors
108         are generally in `message`.
109 
110         See https://docs.gitlab.com/ee/api/#data-validation-and-error-reporting
111         """
112         if "message" in data:
113             return data["message"]
114         if "error" in data:
115             return data["error"]
116 
117 
118 class InstallationForm(forms.Form):
119     url = forms.CharField(
120         label=_("GitLab URL"),
121         help_text=_(
122             "The base URL for your GitLab instance, including the host and protocol. "
123             "Do not include group path."
124             "<br>"
125             "If using gitlab.com, enter https://gitlab.com/"
126         ),
127         widget=forms.TextInput(attrs={"placeholder": "https://gitlab.example.com"}),
128     )
129     group = forms.CharField(
130         label=_("GitLab Group Path"),
131         help_text=_(
132             "This can be found in the URL of your group's GitLab page. "
133             "<br>"
134             "For example, if your group can be found at "
135             "https://gitlab.com/my-group/my-subgroup, enter `my-group/my-subgroup`."
136         ),
137         widget=forms.TextInput(attrs={"placeholder": _("my-group/my-subgroup")}),
138     )
139     include_subgroups = forms.BooleanField(
140         label=_("Include Subgroups"),
141         help_text=_("Include projects in subgroups of the GitLab group."),
142         widget=forms.CheckboxInput(),
143         required=False,
144         initial=False,
145     )
146     verify_ssl = forms.BooleanField(
147         label=_("Verify SSL"),
148         help_text=_(
149             "By default, we verify SSL certificates "
150             "when delivering payloads to your GitLab instance, "
151             "and request GitLab to verify SSL when it delivers "
152             "webhooks to Sentry."
153         ),
154         widget=forms.CheckboxInput(),
155         required=False,
156         initial=True,
157     )
158     client_id = forms.CharField(
159         label=_("GitLab Application ID"),
160         widget=forms.TextInput(
161             attrs={
162                 "placeholder": _("5832fc6e14300a0d962240a8144466eef4ee93ef0d218477e55f11cf12fc3737")
163             }
164         ),
165     )
166     client_secret = forms.CharField(
167         label=_("GitLab Application Secret"),
168         widget=forms.TextInput(attrs={"placeholder": _("XXXXXXXXXXXXXXXXXXXXXXXXXXX")}),
169     )
170 
171     def clean_url(self):
172         """Strip off trailing / as they cause invalid URLs downstream"""
173         return self.cleaned_data["url"].rstrip("/")
174 
175 
176 class InstallationConfigView(PipelineView):
177     def dispatch(self, request, pipeline):
178         if request.method == "POST":
179             form = InstallationForm(request.POST)
180             if form.is_valid():
181                 form_data = form.cleaned_data
182 
183                 pipeline.bind_state("installation_data", form_data)
184 
185                 pipeline.bind_state(
186                     "oauth_config_information",
187                     {
188                         "access_token_url": u"{}/oauth/token".format(form_data.get("url")),
189                         "authorize_url": u"{}/oauth/authorize".format(form_data.get("url")),
190                         "client_id": form_data.get("client_id"),
191                         "client_secret": form_data.get("client_secret"),
192                         "verify_ssl": form_data.get("verify_ssl"),
193                     },
194                 )
195                 pipeline.get_logger().info(
196                     "gitlab.setup.installation-config-view.success",
197                     extra={
198                         "base_url": form_data.get("url"),
199                         "client_id": form_data.get("client_id"),
200                         "verify_ssl": form_data.get("verify_ssl"),
201                     },
202                 )
203                 return pipeline.next_step()
204         else:
205             form = InstallationForm()
206 
207         return render_to_response(
208             template="sentry/integrations/gitlab-config.html",
209             context={"form": form},
210             request=request,
211         )
212 
213 
214 class InstallationGuideView(PipelineView):
215     def dispatch(self, request, pipeline):
216         if "completed_installation_guide" in request.GET:
217             return pipeline.next_step()
218         return render_to_response(
219             template="sentry/integrations/gitlab-config.html",
220             context={
221                 "next_url": "%s%s"
222                 % (absolute_uri("extensions/gitlab/setup/"), "?completed_installation_guide"),
223                 "setup_values": [
224                     {"label": "Name", "value": "Sentry"},
225                     {"label": "Redirect URI", "value": absolute_uri("/extensions/gitlab/setup/")},
226                     {"label": "Scopes", "value": "api"},
227                 ],
228             },
229             request=request,
230         )
231 
232 
233 class GitlabIntegrationProvider(IntegrationProvider):
234     key = "gitlab"
235     name = "GitLab"
236     metadata = metadata
237     integration_cls = GitlabIntegration
238 
239     needs_default_identity = True
240 
241     features = frozenset([IntegrationFeatures.ISSUE_BASIC, IntegrationFeatures.COMMITS])
242 
243     setup_dialog_config = {"width": 1030, "height": 1000}
244 
245     def _make_identity_pipeline_view(self):
246         """
247         Make the nested identity provider view. It is important that this view is
248         not constructed until we reach this step and the
249         ``oauth_config_information`` is available in the pipeline state. This
250         method should be late bound into the pipeline vies.
251         """
252         identity_pipeline_config = dict(
253             oauth_scopes=sorted(GitlabIdentityProvider.oauth_scopes),
254             redirect_url=absolute_uri("/extensions/gitlab/setup/"),
255             **self.pipeline.fetch_state("oauth_config_information")
256         )
257 
258         return NestedPipelineView(
259             bind_key="identity",
260             provider_key="gitlab",
261             pipeline_cls=IdentityProviderPipeline,
262             config=identity_pipeline_config,
263         )
264 
265     def get_group_info(self, access_token, installation_data):
266         client = GitLabSetupClient(
267             installation_data["url"], access_token, installation_data["verify_ssl"]
268         )
269         try:
270             resp = client.get_group(installation_data["group"])
271             return resp.json
272         except ApiError as e:
273             self.get_logger().info(
274                 "gitlab.installation.get-group-info-failure",
275                 extra={
276                     "base_url": installation_data["url"],
277                     "verify_ssl": installation_data["verify_ssl"],
278                     "group": installation_data["group"],
279                     "include_subgroups": installation_data["include_subgroups"],
280                     "error_message": six.text_type(e),
281                     "error_status": e.code,
282                 },
283             )
284             raise IntegrationError("The requested GitLab group could not be found.")
285 
286     def get_pipeline_views(self):
287         return [
288             InstallationGuideView(),
289             InstallationConfigView(),
290             lambda: self._make_identity_pipeline_view(),
291         ]
292 
293     def build_integration(self, state):
294         data = state["identity"]["data"]
295         oauth_data = get_oauth_data(data)
296         user = get_user_info(data["access_token"], state["installation_data"])
297         group = self.get_group_info(data["access_token"], state["installation_data"])
298         include_subgroups = state["installation_data"]["include_subgroups"]
299         scopes = sorted(GitlabIdentityProvider.oauth_scopes)
300         base_url = state["installation_data"]["url"]
301 
302         hostname = urlparse(base_url).netloc
303         verify_ssl = state["installation_data"]["verify_ssl"]
304 
305         # Generate a hash to prevent stray hooks from being accepted
306         # use a consistent hash so that reinstalls/shared integrations don't
307         # rotate secrets.
308         secret = sha1_text("".join([hostname, state["installation_data"]["client_id"]]))
309 
310         integration = {
311             "name": group["full_name"],
312             # Splice the gitlab host and project together to
313             # act as unique link between a gitlab instance, group + sentry.
314             # This value is embedded then in the webook token that we
315             # give to gitlab to allow us to find the integration a hook came
316             # from.
317             "external_id": u"{}:{}".format(hostname, group["id"]),
318             "metadata": {
319                 "icon": group["avatar_url"],
320                 "instance": hostname,
321                 "domain_name": u"{}/{}".format(hostname, group["full_path"]),
322                 "scopes": scopes,
323                 "verify_ssl": verify_ssl,
324                 "base_url": base_url,
325                 "webhook_secret": secret.hexdigest(),
326                 "group_id": group["id"],
327                 "include_subgroups": include_subgroups,
328             },
329             "user_identity": {
330                 "type": "gitlab",
331                 "external_id": u"{}:{}".format(hostname, user["id"]),
332                 "scopes": scopes,
333                 "data": oauth_data,
334             },
335         }
336         return integration
337 
338     def setup(self):
339         from sentry.plugins.base import bindings
340 
341         bindings.add(
342             "integration-repository.provider", GitlabRepositoryProvider, id="integrations:gitlab"
343         )
344
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/sentry/integrations/gitlab/integration.py b/src/sentry/integrations/gitlab/integration.py
--- a/src/sentry/integrations/gitlab/integration.py
+++ b/src/sentry/integrations/gitlab/integration.py
@@ -175,6 +175,10 @@
 
 class InstallationConfigView(PipelineView):
     def dispatch(self, request, pipeline):
+        if "goback" in request.GET:
+            pipeline.state.step_index = 0
+            return pipeline.current_step()
+
         if request.method == "POST":
             form = InstallationForm(request.POST)
             if form.is_valid():

{"golden_diff": "diff --git a/src/sentry/integrations/gitlab/integration.py b/src/sentry/integrations/gitlab/integration.py\n--- a/src/sentry/integrations/gitlab/integration.py\n+++ b/src/sentry/integrations/gitlab/integration.py\n@@ -175,6 +175,10 @@\n \n class InstallationConfigView(PipelineView):\n     def dispatch(self, request, pipeline):\n+        if \"goback\" in request.GET:\n+            pipeline.state.step_index = 0\n+            return pipeline.current_step()\n+\n         if request.method == \"POST\":\n             form = InstallationForm(request.POST)\n             if form.is_valid():\n", "issue": "Unclear instructions for GitLab integration setup\n> Step 1: Create a Sentry App in GitLab\r\n\r\n> To configure GitLab with Sentry, you will need to create a Sentry app in your GitLab instance.\r\n\r\n> You'll also need to be a maintainer or owner in GitLab. Projects owned by users are not supported.\r\n\r\n> Navigate to the **User Settings** section of your GitLab instance.\r\n\r\n> In the sidebar, select 'Applications'.\r\n\r\nDid the author mean `https://gitlaburl/admin`? If so, this is called `Admin Area`:\r\n\r\n![image](https://user-images.githubusercontent.com/611746/75067118-f71c6800-54b1-11ea-8c30-4d24449abe9b.png)\r\n\r\nAlso, providing a direct URL won't hurt. Why not something like this:\r\n\r\n> Navigate to the Admin Area of your GitLab instance, then select \"Applications\" in the sidebar. (Or navigate directly to /admin/applications/new)\n", "before_files": [{"content": "from __future__ import absolute_import\n\nimport six\nfrom six.moves.urllib.parse import urlparse\nfrom django.utils.translation import ugettext_lazy as _\nfrom django import forms\n\nfrom sentry.web.helpers import render_to_response\nfrom sentry.identity.pipeline import IdentityProviderPipeline\nfrom sentry.identity.gitlab import get_user_info, get_oauth_data\nfrom sentry.identity.gitlab.provider import GitlabIdentityProvider\nfrom sentry.integrations import (\n    FeatureDescription,\n    IntegrationInstallation,\n    IntegrationFeatures,\n    IntegrationProvider,\n    IntegrationMetadata,\n)\nfrom sentry.integrations.exceptions import ApiError, IntegrationError\nfrom sentry.integrations.repositories import RepositoryMixin\nfrom sentry.pipeline import NestedPipelineView, PipelineView\nfrom sentry.utils.http import absolute_uri\nfrom sentry.utils.hashlib import sha1_text\n\nfrom .client import GitLabApiClient, GitLabSetupClient\nfrom .issues import GitlabIssueBasic\nfrom .repository import GitlabRepositoryProvider\n\nDESCRIPTION = \"\"\"\nConnect your Sentry organization to an organization in your GitLab instance or gitlab.com, enabling the following features:\n\"\"\"\n\nFEATURES = [\n    FeatureDescription(\n        \"\"\"\n        Track commits and releases (learn more\n        [here](https://docs.sentry.io/learn/releases/))\n        \"\"\",\n        IntegrationFeatures.COMMITS,\n    ),\n    FeatureDescription(\n        \"\"\"\n        Resolve Sentry issues via GitLab commits and merge requests by\n        including `Fixes PROJ-ID` in the message\n        \"\"\",\n        IntegrationFeatures.COMMITS,\n    ),\n    FeatureDescription(\n        \"\"\"\n        Create GitLab issues from Sentry\n        \"\"\",\n        IntegrationFeatures.ISSUE_BASIC,\n    ),\n    FeatureDescription(\n        \"\"\"\n        Link Sentry issues to existing GitLab issues\n        \"\"\",\n        IntegrationFeatures.ISSUE_BASIC,\n    ),\n]\n\nmetadata = IntegrationMetadata(\n    description=DESCRIPTION.strip(),\n    features=FEATURES,\n    author=\"The Sentry Team\",\n    noun=_(\"Installation\"),\n    issue_url=\"https://github.com/getsentry/sentry/issues/\",\n    source_url=\"https://github.com/getsentry/sentry/tree/master/src/sentry/integrations/gitlab\",\n    aspects={},\n)\n\n\nclass GitlabIntegration(IntegrationInstallation, GitlabIssueBasic, RepositoryMixin):\n    repo_search = True\n\n    def __init__(self, *args, **kwargs):\n        super(GitlabIntegration, self).__init__(*args, **kwargs)\n        self.default_identity = None\n\n    def get_group_id(self):\n        return self.model.metadata[\"group_id\"]\n\n    def get_client(self):\n        if self.default_identity is None:\n            self.default_identity = self.get_default_identity()\n\n        return GitLabApiClient(self)\n\n    def get_repositories(self, query=None):\n        # Note: gitlab projects are the same things as repos everywhere else\n        group = self.get_group_id()\n        resp = self.get_client().search_group_projects(group, query)\n        return [{\"identifier\": repo[\"id\"], \"name\": repo[\"name_with_namespace\"]} for repo in resp]\n\n    def search_projects(self, query):\n        client = self.get_client()\n        group_id = self.get_group_id()\n        return client.search_group_projects(group_id, query)\n\n    def search_issues(self, project_id, query, iids):\n        client = self.get_client()\n        return client.search_project_issues(project_id, query, iids)\n\n    def error_message_from_json(self, data):\n        \"\"\"\n        Extract error messages from gitlab API errors.\n        Generic errors come in the `error` key while validation errors\n        are generally in `message`.\n\n        See https://docs.gitlab.com/ee/api/#data-validation-and-error-reporting\n        \"\"\"\n        if \"message\" in data:\n            return data[\"message\"]\n        if \"error\" in data:\n            return data[\"error\"]\n\n\nclass InstallationForm(forms.Form):\n    url = forms.CharField(\n        label=_(\"GitLab URL\"),\n        help_text=_(\n            \"The base URL for your GitLab instance, including the host and protocol. \"\n            \"Do not include group path.\"\n            \"<br>\"\n            \"If using gitlab.com, enter https://gitlab.com/\"\n        ),\n        widget=forms.TextInput(attrs={\"placeholder\": \"https://gitlab.example.com\"}),\n    )\n    group = forms.CharField(\n        label=_(\"GitLab Group Path\"),\n        help_text=_(\n            \"This can be found in the URL of your group's GitLab page. \"\n            \"<br>\"\n            \"For example, if your group can be found at \"\n            \"https://gitlab.com/my-group/my-subgroup, enter `my-group/my-subgroup`.\"\n        ),\n        widget=forms.TextInput(attrs={\"placeholder\": _(\"my-group/my-subgroup\")}),\n    )\n    include_subgroups = forms.BooleanField(\n        label=_(\"Include Subgroups\"),\n        help_text=_(\"Include projects in subgroups of the GitLab group.\"),\n        widget=forms.CheckboxInput(),\n        required=False,\n        initial=False,\n    )\n    verify_ssl = forms.BooleanField(\n        label=_(\"Verify SSL\"),\n        help_text=_(\n            \"By default, we verify SSL certificates \"\n            \"when delivering payloads to your GitLab instance, \"\n            \"and request GitLab to verify SSL when it delivers \"\n            \"webhooks to Sentry.\"\n        ),\n        widget=forms.CheckboxInput(),\n        required=False,\n        initial=True,\n    )\n    client_id = forms.CharField(\n        label=_(\"GitLab Application ID\"),\n        widget=forms.TextInput(\n            attrs={\n                \"placeholder\": _(\"5832fc6e14300a0d962240a8144466eef4ee93ef0d218477e55f11cf12fc3737\")\n            }\n        ),\n    )\n    client_secret = forms.CharField(\n        label=_(\"GitLab Application Secret\"),\n        widget=forms.TextInput(attrs={\"placeholder\": _(\"XXXXXXXXXXXXXXXXXXXXXXXXXXX\")}),\n    )\n\n    def clean_url(self):\n        \"\"\"Strip off trailing / as they cause invalid URLs downstream\"\"\"\n        return self.cleaned_data[\"url\"].rstrip(\"/\")\n\n\nclass InstallationConfigView(PipelineView):\n    def dispatch(self, request, pipeline):\n        if request.method == \"POST\":\n            form = InstallationForm(request.POST)\n            if form.is_valid():\n                form_data = form.cleaned_data\n\n                pipeline.bind_state(\"installation_data\", form_data)\n\n                pipeline.bind_state(\n                    \"oauth_config_information\",\n                    {\n                        \"access_token_url\": u\"{}/oauth/token\".format(form_data.get(\"url\")),\n                        \"authorize_url\": u\"{}/oauth/authorize\".format(form_data.get(\"url\")),\n                        \"client_id\": form_data.get(\"client_id\"),\n                        \"client_secret\": form_data.get(\"client_secret\"),\n                        \"verify_ssl\": form_data.get(\"verify_ssl\"),\n                    },\n                )\n                pipeline.get_logger().info(\n                    \"gitlab.setup.installation-config-view.success\",\n                    extra={\n                        \"base_url\": form_data.get(\"url\"),\n                        \"client_id\": form_data.get(\"client_id\"),\n                        \"verify_ssl\": form_data.get(\"verify_ssl\"),\n                    },\n                )\n                return pipeline.next_step()\n        else:\n            form = InstallationForm()\n\n        return render_to_response(\n            template=\"sentry/integrations/gitlab-config.html\",\n            context={\"form\": form},\n            request=request,\n        )\n\n\nclass InstallationGuideView(PipelineView):\n    def dispatch(self, request, pipeline):\n        if \"completed_installation_guide\" in request.GET:\n            return pipeline.next_step()\n        return render_to_response(\n            template=\"sentry/integrations/gitlab-config.html\",\n            context={\n                \"next_url\": \"%s%s\"\n                % (absolute_uri(\"extensions/gitlab/setup/\"), \"?completed_installation_guide\"),\n                \"setup_values\": [\n                    {\"label\": \"Name\", \"value\": \"Sentry\"},\n                    {\"label\": \"Redirect URI\", \"value\": absolute_uri(\"/extensions/gitlab/setup/\")},\n                    {\"label\": \"Scopes\", \"value\": \"api\"},\n                ],\n            },\n            request=request,\n        )\n\n\nclass GitlabIntegrationProvider(IntegrationProvider):\n    key = \"gitlab\"\n    name = \"GitLab\"\n    metadata = metadata\n    integration_cls = GitlabIntegration\n\n    needs_default_identity = True\n\n    features = frozenset([IntegrationFeatures.ISSUE_BASIC, IntegrationFeatures.COMMITS])\n\n    setup_dialog_config = {\"width\": 1030, \"height\": 1000}\n\n    def _make_identity_pipeline_view(self):\n        \"\"\"\n        Make the nested identity provider view. It is important that this view is\n        not constructed until we reach this step and the\n        ``oauth_config_information`` is available in the pipeline state. This\n        method should be late bound into the pipeline vies.\n        \"\"\"\n        identity_pipeline_config = dict(\n            oauth_scopes=sorted(GitlabIdentityProvider.oauth_scopes),\n            redirect_url=absolute_uri(\"/extensions/gitlab/setup/\"),\n            **self.pipeline.fetch_state(\"oauth_config_information\")\n        )\n\n        return NestedPipelineView(\n            bind_key=\"identity\",\n            provider_key=\"gitlab\",\n            pipeline_cls=IdentityProviderPipeline,\n            config=identity_pipeline_config,\n        )\n\n    def get_group_info(self, access_token, installation_data):\n        client = GitLabSetupClient(\n            installation_data[\"url\"], access_token, installation_data[\"verify_ssl\"]\n        )\n        try:\n            resp = client.get_group(installation_data[\"group\"])\n            return resp.json\n        except ApiError as e:\n            self.get_logger().info(\n                \"gitlab.installation.get-group-info-failure\",\n                extra={\n                    \"base_url\": installation_data[\"url\"],\n                    \"verify_ssl\": installation_data[\"verify_ssl\"],\n                    \"group\": installation_data[\"group\"],\n                    \"include_subgroups\": installation_data[\"include_subgroups\"],\n                    \"error_message\": six.text_type(e),\n                    \"error_status\": e.code,\n                },\n            )\n            raise IntegrationError(\"The requested GitLab group could not be found.\")\n\n    def get_pipeline_views(self):\n        return [\n            InstallationGuideView(),\n            InstallationConfigView(),\n            lambda: self._make_identity_pipeline_view(),\n        ]\n\n    def build_integration(self, state):\n        data = state[\"identity\"][\"data\"]\n        oauth_data = get_oauth_data(data)\n        user = get_user_info(data[\"access_token\"], state[\"installation_data\"])\n        group = self.get_group_info(data[\"access_token\"], state[\"installation_data\"])\n        include_subgroups = state[\"installation_data\"][\"include_subgroups\"]\n        scopes = sorted(GitlabIdentityProvider.oauth_scopes)\n        base_url = state[\"installation_data\"][\"url\"]\n\n        hostname = urlparse(base_url).netloc\n        verify_ssl = state[\"installation_data\"][\"verify_ssl\"]\n\n        # Generate a hash to prevent stray hooks from being accepted\n        # use a consistent hash so that reinstalls/shared integrations don't\n        # rotate secrets.\n        secret = sha1_text(\"\".join([hostname, state[\"installation_data\"][\"client_id\"]]))\n\n        integration = {\n            \"name\": group[\"full_name\"],\n            # Splice the gitlab host and project together to\n            # act as unique link between a gitlab instance, group + sentry.\n            # This value is embedded then in the webook token that we\n            # give to gitlab to allow us to find the integration a hook came\n            # from.\n            \"external_id\": u\"{}:{}\".format(hostname, group[\"id\"]),\n            \"metadata\": {\n                \"icon\": group[\"avatar_url\"],\n                \"instance\": hostname,\n                \"domain_name\": u\"{}/{}\".format(hostname, group[\"full_path\"]),\n                \"scopes\": scopes,\n                \"verify_ssl\": verify_ssl,\n                \"base_url\": base_url,\n                \"webhook_secret\": secret.hexdigest(),\n                \"group_id\": group[\"id\"],\n                \"include_subgroups\": include_subgroups,\n            },\n            \"user_identity\": {\n                \"type\": \"gitlab\",\n                \"external_id\": u\"{}:{}\".format(hostname, user[\"id\"]),\n                \"scopes\": scopes,\n                \"data\": oauth_data,\n            },\n        }\n        return integration\n\n    def setup(self):\n        from sentry.plugins.base import bindings\n\n        bindings.add(\n            \"integration-repository.provider\", GitlabRepositoryProvider, id=\"integrations:gitlab\"\n        )\n", "path": "src/sentry/integrations/gitlab/integration.py"}], "after_files": [{"content": "from __future__ import absolute_import\n\nimport six\nfrom six.moves.urllib.parse import urlparse\nfrom django.utils.translation import ugettext_lazy as _\nfrom django import forms\n\nfrom sentry.web.helpers import render_to_response\nfrom sentry.identity.pipeline import IdentityProviderPipeline\nfrom sentry.identity.gitlab import get_user_info, get_oauth_data\nfrom sentry.identity.gitlab.provider import GitlabIdentityProvider\nfrom sentry.integrations import (\n    FeatureDescription,\n    IntegrationInstallation,\n    IntegrationFeatures,\n    IntegrationProvider,\n    IntegrationMetadata,\n)\nfrom sentry.integrations.exceptions import ApiError, IntegrationError\nfrom sentry.integrations.repositories import RepositoryMixin\nfrom sentry.pipeline import NestedPipelineView, PipelineView\nfrom sentry.utils.http import absolute_uri\nfrom sentry.utils.hashlib import sha1_text\n\nfrom .client import GitLabApiClient, GitLabSetupClient\nfrom .issues import GitlabIssueBasic\nfrom .repository import GitlabRepositoryProvider\n\nDESCRIPTION = \"\"\"\nConnect your Sentry organization to an organization in your GitLab instance or gitlab.com, enabling the following features:\n\"\"\"\n\nFEATURES = [\n    FeatureDescription(\n        \"\"\"\n        Track commits and releases (learn more\n        [here](https://docs.sentry.io/learn/releases/))\n        \"\"\",\n        IntegrationFeatures.COMMITS,\n    ),\n    FeatureDescription(\n        \"\"\"\n        Resolve Sentry issues via GitLab commits and merge requests by\n        including `Fixes PROJ-ID` in the message\n        \"\"\",\n        IntegrationFeatures.COMMITS,\n    ),\n    FeatureDescription(\n        \"\"\"\n        Create GitLab issues from Sentry\n        \"\"\",\n        IntegrationFeatures.ISSUE_BASIC,\n    ),\n    FeatureDescription(\n        \"\"\"\n        Link Sentry issues to existing GitLab issues\n        \"\"\",\n        IntegrationFeatures.ISSUE_BASIC,\n    ),\n]\n\nmetadata = IntegrationMetadata(\n    description=DESCRIPTION.strip(),\n    features=FEATURES,\n    author=\"The Sentry Team\",\n    noun=_(\"Installation\"),\n    issue_url=\"https://github.com/getsentry/sentry/issues/\",\n    source_url=\"https://github.com/getsentry/sentry/tree/master/src/sentry/integrations/gitlab\",\n    aspects={},\n)\n\n\nclass GitlabIntegration(IntegrationInstallation, GitlabIssueBasic, RepositoryMixin):\n    repo_search = True\n\n    def __init__(self, *args, **kwargs):\n        super(GitlabIntegration, self).__init__(*args, **kwargs)\n        self.default_identity = None\n\n    def get_group_id(self):\n        return self.model.metadata[\"group_id\"]\n\n    def get_client(self):\n        if self.default_identity is None:\n            self.default_identity = self.get_default_identity()\n\n        return GitLabApiClient(self)\n\n    def get_repositories(self, query=None):\n        # Note: gitlab projects are the same things as repos everywhere else\n        group = self.get_group_id()\n        resp = self.get_client().search_group_projects(group, query)\n        return [{\"identifier\": repo[\"id\"], \"name\": repo[\"name_with_namespace\"]} for repo in resp]\n\n    def search_projects(self, query):\n        client = self.get_client()\n        group_id = self.get_group_id()\n        return client.search_group_projects(group_id, query)\n\n    def search_issues(self, project_id, query, iids):\n        client = self.get_client()\n        return client.search_project_issues(project_id, query, iids)\n\n    def error_message_from_json(self, data):\n        \"\"\"\n        Extract error messages from gitlab API errors.\n        Generic errors come in the `error` key while validation errors\n        are generally in `message`.\n\n        See https://docs.gitlab.com/ee/api/#data-validation-and-error-reporting\n        \"\"\"\n        if \"message\" in data:\n            return data[\"message\"]\n        if \"error\" in data:\n            return data[\"error\"]\n\n\nclass InstallationForm(forms.Form):\n    url = forms.CharField(\n        label=_(\"GitLab URL\"),\n        help_text=_(\n            \"The base URL for your GitLab instance, including the host and protocol. \"\n            \"Do not include group path.\"\n            \"<br>\"\n            \"If using gitlab.com, enter https://gitlab.com/\"\n        ),\n        widget=forms.TextInput(attrs={\"placeholder\": \"https://gitlab.example.com\"}),\n    )\n    group = forms.CharField(\n        label=_(\"GitLab Group Path\"),\n        help_text=_(\n            \"This can be found in the URL of your group's GitLab page. \"\n            \"<br>\"\n            \"For example, if your group can be found at \"\n            \"https://gitlab.com/my-group/my-subgroup, enter `my-group/my-subgroup`.\"\n        ),\n        widget=forms.TextInput(attrs={\"placeholder\": _(\"my-group/my-subgroup\")}),\n    )\n    include_subgroups = forms.BooleanField(\n        label=_(\"Include Subgroups\"),\n        help_text=_(\"Include projects in subgroups of the GitLab group.\"),\n        widget=forms.CheckboxInput(),\n        required=False,\n        initial=False,\n    )\n    verify_ssl = forms.BooleanField(\n        label=_(\"Verify SSL\"),\n        help_text=_(\n            \"By default, we verify SSL certificates \"\n            \"when delivering payloads to your GitLab instance, \"\n            \"and request GitLab to verify SSL when it delivers \"\n            \"webhooks to Sentry.\"\n        ),\n        widget=forms.CheckboxInput(),\n        required=False,\n        initial=True,\n    )\n    client_id = forms.CharField(\n        label=_(\"GitLab Application ID\"),\n        widget=forms.TextInput(\n            attrs={\n                \"placeholder\": _(\"5832fc6e14300a0d962240a8144466eef4ee93ef0d218477e55f11cf12fc3737\")\n            }\n        ),\n    )\n    client_secret = forms.CharField(\n        label=_(\"GitLab Application Secret\"),\n        widget=forms.TextInput(attrs={\"placeholder\": _(\"XXXXXXXXXXXXXXXXXXXXXXXXXXX\")}),\n    )\n\n    def clean_url(self):\n        \"\"\"Strip off trailing / as they cause invalid URLs downstream\"\"\"\n        return self.cleaned_data[\"url\"].rstrip(\"/\")\n\n\nclass InstallationConfigView(PipelineView):\n    def dispatch(self, request, pipeline):\n        if \"goback\" in request.GET:\n            pipeline.state.step_index = 0\n            return pipeline.current_step()\n\n        if request.method == \"POST\":\n            form = InstallationForm(request.POST)\n            if form.is_valid():\n                form_data = form.cleaned_data\n\n                pipeline.bind_state(\"installation_data\", form_data)\n\n                pipeline.bind_state(\n                    \"oauth_config_information\",\n                    {\n                        \"access_token_url\": u\"{}/oauth/token\".format(form_data.get(\"url\")),\n                        \"authorize_url\": u\"{}/oauth/authorize\".format(form_data.get(\"url\")),\n                        \"client_id\": form_data.get(\"client_id\"),\n                        \"client_secret\": form_data.get(\"client_secret\"),\n                        \"verify_ssl\": form_data.get(\"verify_ssl\"),\n                    },\n                )\n                pipeline.get_logger().info(\n                    \"gitlab.setup.installation-config-view.success\",\n                    extra={\n                        \"base_url\": form_data.get(\"url\"),\n                        \"client_id\": form_data.get(\"client_id\"),\n                        \"verify_ssl\": form_data.get(\"verify_ssl\"),\n                    },\n                )\n                return pipeline.next_step()\n        else:\n            form = InstallationForm()\n\n        return render_to_response(\n            template=\"sentry/integrations/gitlab-config.html\",\n            context={\"form\": form},\n            request=request,\n        )\n\n\nclass InstallationGuideView(PipelineView):\n    def dispatch(self, request, pipeline):\n        if \"completed_installation_guide\" in request.GET:\n            return pipeline.next_step()\n        return render_to_response(\n            template=\"sentry/integrations/gitlab-config.html\",\n            context={\n                \"next_url\": \"%s%s\"\n                % (absolute_uri(\"extensions/gitlab/setup/\"), \"?completed_installation_guide\"),\n                \"setup_values\": [\n                    {\"label\": \"Name\", \"value\": \"Sentry\"},\n                    {\"label\": \"Redirect URI\", \"value\": absolute_uri(\"/extensions/gitlab/setup/\")},\n                    {\"label\": \"Scopes\", \"value\": \"api\"},\n                ],\n            },\n            request=request,\n        )\n\n\nclass GitlabIntegrationProvider(IntegrationProvider):\n    key = \"gitlab\"\n    name = \"GitLab\"\n    metadata = metadata\n    integration_cls = GitlabIntegration\n\n    needs_default_identity = True\n\n    features = frozenset([IntegrationFeatures.ISSUE_BASIC, IntegrationFeatures.COMMITS])\n\n    setup_dialog_config = {\"width\": 1030, \"height\": 1000}\n\n    def _make_identity_pipeline_view(self):\n        \"\"\"\n        Make the nested identity provider view. It is important that this view is\n        not constructed until we reach this step and the\n        ``oauth_config_information`` is available in the pipeline state. This\n        method should be late bound into the pipeline vies.\n        \"\"\"\n        identity_pipeline_config = dict(\n            oauth_scopes=sorted(GitlabIdentityProvider.oauth_scopes),\n            redirect_url=absolute_uri(\"/extensions/gitlab/setup/\"),\n            **self.pipeline.fetch_state(\"oauth_config_information\")\n        )\n\n        return NestedPipelineView(\n            bind_key=\"identity\",\n            provider_key=\"gitlab\",\n            pipeline_cls=IdentityProviderPipeline,\n            config=identity_pipeline_config,\n        )\n\n    def get_group_info(self, access_token, installation_data):\n        client = GitLabSetupClient(\n            installation_data[\"url\"], access_token, installation_data[\"verify_ssl\"]\n        )\n        try:\n            resp = client.get_group(installation_data[\"group\"])\n            return resp.json\n        except ApiError as e:\n            self.get_logger().info(\n                \"gitlab.installation.get-group-info-failure\",\n                extra={\n                    \"base_url\": installation_data[\"url\"],\n                    \"verify_ssl\": installation_data[\"verify_ssl\"],\n                    \"group\": installation_data[\"group\"],\n                    \"include_subgroups\": installation_data[\"include_subgroups\"],\n                    \"error_message\": six.text_type(e),\n                    \"error_status\": e.code,\n                },\n            )\n            raise IntegrationError(\"The requested GitLab group could not be found.\")\n\n    def get_pipeline_views(self):\n        return [\n            InstallationGuideView(),\n            InstallationConfigView(),\n            lambda: self._make_identity_pipeline_view(),\n        ]\n\n    def build_integration(self, state):\n        data = state[\"identity\"][\"data\"]\n        oauth_data = get_oauth_data(data)\n        user = get_user_info(data[\"access_token\"], state[\"installation_data\"])\n        group = self.get_group_info(data[\"access_token\"], state[\"installation_data\"])\n        include_subgroups = state[\"installation_data\"][\"include_subgroups\"]\n        scopes = sorted(GitlabIdentityProvider.oauth_scopes)\n        base_url = state[\"installation_data\"][\"url\"]\n\n        hostname = urlparse(base_url).netloc\n        verify_ssl = state[\"installation_data\"][\"verify_ssl\"]\n\n        # Generate a hash to prevent stray hooks from being accepted\n        # use a consistent hash so that reinstalls/shared integrations don't\n        # rotate secrets.\n        secret = sha1_text(\"\".join([hostname, state[\"installation_data\"][\"client_id\"]]))\n\n        integration = {\n            \"name\": group[\"full_name\"],\n            # Splice the gitlab host and project together to\n            # act as unique link between a gitlab instance, group + sentry.\n            # This value is embedded then in the webook token that we\n            # give to gitlab to allow us to find the integration a hook came\n            # from.\n            \"external_id\": u\"{}:{}\".format(hostname, group[\"id\"]),\n            \"metadata\": {\n                \"icon\": group[\"avatar_url\"],\n                \"instance\": hostname,\n                \"domain_name\": u\"{}/{}\".format(hostname, group[\"full_path\"]),\n                \"scopes\": scopes,\n                \"verify_ssl\": verify_ssl,\n                \"base_url\": base_url,\n                \"webhook_secret\": secret.hexdigest(),\n                \"group_id\": group[\"id\"],\n                \"include_subgroups\": include_subgroups,\n            },\n            \"user_identity\": {\n                \"type\": \"gitlab\",\n                \"external_id\": u\"{}:{}\".format(hostname, user[\"id\"]),\n                \"scopes\": scopes,\n                \"data\": oauth_data,\n            },\n        }\n        return integration\n\n    def setup(self):\n        from sentry.plugins.base import bindings\n\n        bindings.add(\n            \"integration-repository.provider\", GitlabRepositoryProvider, id=\"integrations:gitlab\"\n        )\n", "path": "src/sentry/integrations/gitlab/integration.py"}]}