Browse Source

feat(ecosystem): Use default branch from repo (#44270)

uses the integration search api to get the default branch for the
selected repo
Scott Cooper 2 years ago
parent
commit
9172035625

+ 12 - 0
static/app/types/integrations.tsx

@@ -81,6 +81,18 @@ export type Repository = {
   url: string;
 };
 
+/**
+ * Integration Repositories from OrganizationIntegrationReposEndpoint
+ */
+export type IntegrationRepository = {
+  /**
+   * ex - getsentry/sentry
+   */
+  identifier: string;
+  name: string;
+  defaultBranch?: string | null;
+};
+
 export type Commit = {
   dateCreated: string;
   id: string;

+ 30 - 0
static/app/views/settings/organizationIntegrations/integrationCodeMappings.spec.jsx

@@ -5,6 +5,7 @@ import {
   renderGlobalModal,
   screen,
   userEvent,
+  waitFor,
 } from 'sentry-test/reactTestingLibrary';
 
 import ModalStore from 'sentry/stores/modalStore';
@@ -64,6 +65,10 @@ describe('IntegrationCodeMappings', function () {
       url: `/organizations/${org.slug}/repos/`,
       body: repos,
     });
+    MockApiClient.addMockResponse({
+      url: `/organizations/${org.slug}/integrations/${integration.id}/repos/`,
+      body: {repos: []},
+    });
   });
 
   afterEach(() => {
@@ -170,4 +175,29 @@ describe('IntegrationCodeMappings', function () {
       })
     );
   });
+
+  it('switches default branch to the repo defaultBranch', async () => {
+    MockApiClient.addMockResponse({
+      url: `/organizations/${org.slug}/integrations/${integration.id}/repos/`,
+      body: {
+        repos: [
+          {
+            id: repos[0].id,
+            identifier: repos[1].name,
+            defaultBranch: 'main',
+          },
+        ],
+      },
+    });
+    render(<IntegrationCodeMappings organization={org} integration={integration} />);
+    renderGlobalModal();
+
+    userEvent.click(screen.getByRole('button', {name: 'Add Code Mapping'}));
+    expect(screen.getByRole('textbox', {name: 'Branch'})).toHaveValue('master');
+
+    await selectEvent.select(screen.getByText('Choose repo'), repos[1].name);
+    await waitFor(() => {
+      expect(screen.getByRole('textbox', {name: 'Branch'})).toHaveValue('main');
+    });
+  });
 });

+ 3 - 1
static/app/views/settings/organizationIntegrations/integrationCodeMappings.tsx

@@ -162,7 +162,9 @@ class IntegrationCodeMappings extends AsyncComponent<Props, State> {
 
     openModal(({Body, Header, closeModal}) => (
       <Fragment>
-        <Header closeButton>{t('Configure code path mapping')}</Header>
+        <Header closeButton>
+          <h4>{t('Configure code path mapping')}</h4>
+        </Header>
         <Body>
           <RepositoryProjectPathConfigForm
             organization={organization}

+ 7 - 2
static/app/views/settings/organizationIntegrations/integrationRepos.tsx

@@ -16,7 +16,12 @@ import {IconCommit} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import RepositoryStore from 'sentry/stores/repositoryStore';
 import space from 'sentry/styles/space';
-import {Integration, Organization, Repository} from 'sentry/types';
+import type {
+  Integration,
+  IntegrationRepository,
+  Organization,
+  Repository,
+} from 'sentry/types';
 import withOrganization from 'sentry/utils/withOrganization';
 
 type Props = AsyncComponent['props'] & {
@@ -28,7 +33,7 @@ type State = AsyncComponent['state'] & {
   adding: boolean;
   dropdownBusy: boolean;
   integrationRepos: {
-    repos: {identifier: string; name: string}[];
+    repos: IntegrationRepository[];
     searchable: boolean;
   };
   integrationReposErrorStatus: number | null;

+ 36 - 3
static/app/views/settings/organizationIntegrations/repositoryProjectPathConfigForm.tsx

@@ -1,11 +1,14 @@
+import {useRef} from 'react';
 import pick from 'lodash/pick';
 
 import {FieldFromConfig} from 'sentry/components/forms';
 import Form, {FormProps} from 'sentry/components/forms/form';
+import FormModel from 'sentry/components/forms/model';
 import {Field} from 'sentry/components/forms/types';
 import {t} from 'sentry/locale';
-import {
+import type {
   Integration,
+  IntegrationRepository,
   Organization,
   Project,
   Repository,
@@ -15,6 +18,7 @@ import {
   sentryNameToOption,
   trackIntegrationAnalytics,
 } from 'sentry/utils/integrationUtil';
+import useApi from 'sentry/utils/useApi';
 
 type Props = {
   integration: Integration;
@@ -35,8 +39,34 @@ function RepositoryProjectPathConfigForm({
   projects,
   repos,
 }: Props) {
-  const orgSlug = organization.slug;
+  const api = useApi();
+  const formRef = useRef(new FormModel());
   const repoChoices = repos.map(({name, id}) => ({value: id, label: name}));
+
+  /**
+   * Automatically switch to the default branch for the repo
+   */
+  function handleRepoChange(id: string) {
+    const repo = repos.find(r => r.id === id);
+    if (!repo) {
+      return;
+    }
+
+    // Use the integration repo search to get the default branch
+    api
+      .requestPromise(
+        `/organizations/${organization.slug}/integrations/${integration.id}/repos/`,
+        {query: {search: repo.name}}
+      )
+      .then((data: {repos: IntegrationRepository[]}) => {
+        const {defaultBranch} = data.repos.find(r => r.identifier === repo.name) ?? {};
+        const isCurrentRepo = formRef.current.getValue('repositoryId') === repo.id;
+        if (defaultBranch && isCurrentRepo) {
+          formRef.current.setValue('defaultBranch', defaultBranch);
+        }
+      });
+  }
+
   const formFields: Field[] = [
     {
       name: 'projectId',
@@ -51,11 +81,13 @@ function RepositoryProjectPathConfigForm({
       required: true,
       label: t('Repo'),
       placeholder: t('Choose repo'),
-      url: `/organizations/${orgSlug}/repos/`,
+      url: `/organizations/${organization.slug}/repos/`,
       defaultOptions: repoChoices,
       onResults: results => results.map(sentryNameToOption),
+      onChange: handleRepoChange,
     },
     {
+      id: 'defaultBranch',
       name: 'defaultBranch',
       type: 'string',
       required: true,
@@ -120,6 +152,7 @@ function RepositoryProjectPathConfigForm({
       initialData={initialData}
       apiEndpoint={endpoint}
       apiMethod={apiMethod}
+      model={formRef.current}
       onCancel={onCancel}
     >
       {formFields.map(field => (