add bitbucket support for import space and repo (#721)

pull/3423/head
Dan Wilson 2023-10-26 11:12:10 +00:00 committed by Harness
parent b9b80197a4
commit 3701d4a24f
8 changed files with 258 additions and 105 deletions

View File

@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/harness/gitness/app/api/usererror"
@ -239,7 +240,7 @@ func LoadRepositoriesFromProviderSpace(
for _, scmRepo := range scmRepos {
// in some cases the namespace filter isn't working (e.g. Gitlab)
if scmRepo.Namespace != spaceSlug {
if !strings.EqualFold(scmRepo.Namespace, spaceSlug) {
continue
}

View File

@ -23,7 +23,14 @@ import { Icon } from '@harnessio/icons'
import { FontVariation } from '@harnessio/design-system'
import { useStrings } from 'framework/strings'
import { REGEX_VALID_REPO_NAME } from 'utils/Utils'
import { ImportFormData, RepoVisibility, parseUrl } from 'utils/GitUtils'
import {
ImportFormData,
RepoVisibility,
GitProviders,
getProviders,
getOrgLabel,
getOrgPlaceholder
} from 'utils/GitUtils'
import Private from '../../../icons/private.svg'
import css from '../NewRepoModalButton.module.scss'
@ -39,30 +46,41 @@ const ImportForm = (props: ImportFormProps) => {
const [auth, setAuth] = useState(false)
// eslint-disable-next-line no-control-regex
const MATCH_REPOURL_REGEX = /^(https?:\/\/(?:www\.)?(github|gitlab)\.com\/([^/]+\/[^/]+))/
// const MATCH_REPOURL_REGEX = /^(https?:\/\/(?:www\.)?(github|gitlab)\.com\/([^/]+\/[^/]+))/
const formInitialValues: ImportFormData = {
repoUrl: '',
gitProvider: GitProviders.GITHUB,
hostUrl: '',
org: '',
repo: '',
username: '',
password: '',
name: '',
description: '',
isPublic: RepoVisibility.PRIVATE
}
const validationSchemaStepOne = yup.object().shape({
repoUrl: yup
gitProvider: yup.string().required(),
hostUrl: yup
.string()
.matches(MATCH_REPOURL_REGEX, getString('importSpace.invalidUrl'))
.required(getString('importRepo.required')),
// .matches(MATCH_REPOURL_REGEX, getString('importSpace.invalidUrl'))
.when('gitProvider', {
is: gitProvider => ![GitProviders.GITHUB, GitProviders.GITLAB, GitProviders.BITBUCKET].includes(gitProvider),
then: yup.string().required(getString('importRepo.required')),
otherwise: yup.string().notRequired() // Optional based on your needs
}),
name: yup
.string()
.trim()
.required(getString('validation.nameIsRequired'))
.matches(REGEX_VALID_REPO_NAME, getString('validation.repoNamePatternIsNotValid'))
})
return (
<Formik onSubmit={handleSubmit} initialValues={formInitialValues} formName="importRepoForm">
{formik => {
const { values } = formik
const handleValidationClick = async () => {
try {
await validationSchemaStepOne.validate(formik.values, { abortEarly: false })
@ -80,33 +98,77 @@ const ImportForm = (props: ImportFormProps) => {
}
return (
<FormikForm>
<FormInput.Text
className={css.hideContainer}
name="repoUrl"
label={getString('importRepo.url')}
placeholder={getString('importRepo.urlPlaceholder')}
tooltipProps={{
dataTooltipId: 'repositoryURLTextField'
}}
onChange={event => {
const target = event.target as HTMLInputElement
formik.setFieldValue('repoUrl', target.value)
if (target.value) {
const provider = parseUrl(target.value)
if (provider?.fullRepo) {
formik.setFieldValue('name', provider.repoName ? provider.repoName : provider?.fullRepo)
formik.validateField('repoUrl')
}
}
}}
<FormInput.Select
name={'gitProvider'}
label={getString('importSpace.gitProvider')}
items={getProviders()}
// className={css.selectBox}
/>
{formik.errors.repoUrl ? (
{formik.errors.gitProvider ? (
<Text
margin={{ top: 'small', bottom: 'small' }}
color={Color.RED_500}
icon="circle-cross"
iconProps={{ color: Color.RED_500 }}>
{formik.errors.repoUrl}
{formik.errors.gitProvider}
</Text>
) : null}
{![GitProviders.GITHUB, GitProviders.GITLAB, GitProviders.BITBUCKET].includes(values.gitProvider) && (
<FormInput.Text
className={css.hideContainer}
name="hostUrl"
label={getString('importRepo.url')}
placeholder={getString('importRepo.urlPlaceholder')}
tooltipProps={{
dataTooltipId: 'repositoryURLTextField'
}}
/>
)}
{formik.errors.hostUrl ? (
<Text
margin={{ top: 'small', bottom: 'small' }}
color={Color.RED_500}
icon="circle-cross"
iconProps={{ color: Color.RED_500 }}>
{formik.errors.hostUrl}
</Text>
) : null}
<FormInput.Text
className={css.hideContainer}
name="org"
label={getString(getOrgLabel(values.gitProvider))}
placeholder={getString(getOrgPlaceholder(values.gitProvider))}
/>
{formik.errors.org ? (
<Text
margin={{ top: 'small', bottom: 'small' }}
color={Color.RED_500}
icon="circle-cross"
iconProps={{ color: Color.RED_500 }}>
{formik.errors.org}
</Text>
) : null}
<FormInput.Text
className={css.hideContainer}
name="repo"
label={getString('importRepo.repo')}
placeholder={getString('importRepo.repoPlaceholder')}
onChange={event => {
const target = event.target as HTMLInputElement
formik.setFieldValue('repo', target.value)
if (target.value) {
formik.setFieldValue('name', target.value)
formik.validateField('repo')
}
}}
/>
{formik.errors.repo ? (
<Text
margin={{ top: 'small', bottom: 'small' }}
color={Color.RED_500}
icon="circle-cross"
iconProps={{ color: Color.RED_500 }}>
{formik.errors.repo}
</Text>
) : null}
<FormInput.CheckBox

View File

@ -57,12 +57,13 @@ import {
SUGGESTED_BRANCH_NAMES
} from 'utils/Utils'
import {
GitProviders,
ImportFormData,
RepoCreationType,
RepoFormData,
RepoVisibility,
isGitBranchNameValid,
parseUrl
getProviderTypeMapping
} from 'utils/GitUtils'
import type { TypesRepository, OpenapiCreateRepositoryRequest } from 'services/code'
import { useAppContext } from 'AppContext'
@ -160,14 +161,27 @@ export const NewRepoModalButton: React.FC<NewRepoModalButtonProps> = ({
showError(getErrorMessage(exception), 0, getString('failedToCreateRepo'))
}
}
const handleImportSubmit = (formData: ImportFormData) => {
const provider = parseUrl(formData.repoUrl)
const type = getProviderTypeMapping(formData.gitProvider)
const provider = {
type,
username: formData.username,
password: formData.password,
host: ''
}
if (![GitProviders.GITHUB, GitProviders.GITLAB, GitProviders.BITBUCKET].includes(formData.gitProvider)) {
provider.host = formData.hostUrl
}
const importPayload = {
description: formData.description || '',
parent_ref: space,
uid: formData.name,
provider: { type: provider?.provider.toLowerCase(), username: formData.username, password: formData.password },
provider_repo: provider?.fullRepo.replace(/\.git$/, '')
provider,
provider_repo: `${formData.org}/${formData.name}`.replace(/\.git$/, '')
}
importRepo(importPayload)
.then(response => {

View File

@ -21,7 +21,7 @@ import { Color } from '@harnessio/design-system'
import { Button, Container, Label, Layout, FlexExpander, Formik, FormikForm, FormInput, Text } from '@harnessio/uicore'
import { Icon } from '@harnessio/icons'
import { useStrings } from 'framework/strings'
import { Organization, type ImportSpaceFormData } from 'utils/GitUtils'
import { type ImportSpaceFormData, GitProviders, getProviders, getOrgLabel, getOrgPlaceholder } from 'utils/GitUtils'
import css from '../NewSpaceModalButton.module.scss'
interface ImportFormProps {
@ -31,6 +31,22 @@ interface ImportFormProps {
hideModal: any
}
const getHostPlaceHolder = (gitProvider: string) => {
switch (gitProvider) {
case GitProviders.GITHUB:
case GitProviders.GITHUB_ENTERPRISE:
return 'enterGithubPlaceholder'
case GitProviders.GITLAB:
case GitProviders.GITLAB_SELF_HOSTED:
return 'enterGitlabPlaceholder'
case GitProviders.BITBUCKET:
case GitProviders.BITBUCKET_SERVER:
return 'enterBitbucketPlaceholder'
default:
return 'enterAddress'
}
}
const ImportSpaceForm = (props: ImportFormProps) => {
const { handleSubmit, loading, hideModal } = props
const { getString } = useStrings()
@ -39,7 +55,7 @@ const ImportSpaceForm = (props: ImportFormProps) => {
const [buttonLoading, setButtonLoading] = useState(false)
const formInitialValues: ImportSpaceFormData = {
gitProvider: '',
gitProvider: GitProviders.GITHUB,
username: '',
password: '',
name: '',
@ -48,10 +64,6 @@ const ImportSpaceForm = (props: ImportFormProps) => {
host: ''
}
const providers = [
{ value: 'GitHub', label: 'GitHub' },
{ value: 'GitLab', label: 'GitLab' }
]
const validationSchemaStepOne = yup.object().shape({
gitProvider: yup.string().trim().required(getString('importSpace.providerRequired'))
})
@ -93,15 +105,7 @@ const ImportSpaceForm = (props: ImportFormProps) => {
await handleSubmit(formik.values)
setButtonLoading(false)
}
const getHostPlaceHolder = (host: string) => {
if (host.toLowerCase() === Organization.GITHUB.toLowerCase()) {
return getString('enterGithubPlaceholder')
} else if (host.toLowerCase() === Organization.GITLAB.toLowerCase()) {
return getString('enterGitlabPlaceholder')
} else {
return getString('enterAddress')
}
}
return (
<Container className={css.hideContainer} width={'97%'}>
<FormikForm>
@ -118,10 +122,9 @@ const ImportSpaceForm = (props: ImportFormProps) => {
<hr className={css.dividerContainer} />
<Container className={css.textContainer} width={'70%'}>
<FormInput.Select
value={{ value: values.gitProvider, label: values.gitProvider } || providers[0]}
name={'gitProvider'}
label={getString('importSpace.gitProvider')}
items={providers}
items={getProviders()}
className={css.selectBox}
/>
{formik.errors.gitProvider ? (
@ -133,15 +136,19 @@ const ImportSpaceForm = (props: ImportFormProps) => {
{formik.errors.gitProvider}
</Text>
) : null}
<FormInput.Text
name="host"
label={'Address (optional)'}
placeholder={getHostPlaceHolder(formik.values.gitProvider)}
tooltipProps={{
dataTooltipId: 'spaceUserTextField'
}}
className={css.hostContainer}
/>
{![GitProviders.GITHUB, GitProviders.GITLAB, GitProviders.BITBUCKET].includes(
values.gitProvider
) && (
<FormInput.Text
name="host"
label={getString('importRepo.url')}
placeholder={getString(getHostPlaceHolder(values.gitProvider))}
tooltipProps={{
dataTooltipId: 'spaceUserTextField'
}}
className={css.hostContainer}
/>
)}
{formik.errors.host ? (
<Text
margin={{ top: 'small', bottom: 'small' }}
@ -210,12 +217,8 @@ const ImportSpaceForm = (props: ImportFormProps) => {
<Container className={css.textContainer} width={'70%'}>
<FormInput.Text
name="organization"
label={
formik.values.gitProvider.toLowerCase() === Organization.GITHUB.toLowerCase()
? getString('importSpace.githubOrg')
: getString('importSpace.gitlabGroup')
}
placeholder={getString('importSpace.orgNamePlaceholder')}
label={getString(getOrgLabel(values.gitProvider))}
placeholder={getString(getOrgPlaceholder(values.gitProvider))}
tooltipProps={{
dataTooltipId: 'importSpaceOrgName'
}}

View File

@ -42,7 +42,7 @@ import { useStrings } from 'framework/strings'
import { getErrorMessage, permissionProps, REGEX_VALID_REPO_NAME } from 'utils/Utils'
import type { TypesSpace, OpenapiCreateSpaceRequest } from 'services/code'
import { useAppContext } from 'AppContext'
import { ImportSpaceFormData, SpaceCreationType } from 'utils/GitUtils'
import { ImportSpaceFormData, SpaceCreationType, GitProviders, getProviderTypeMapping } from 'utils/GitUtils'
import ImportSpaceForm from './ImportSpaceForm/ImportSpaceForm'
import css from './NewSpaceModalButton.module.scss'
@ -112,7 +112,7 @@ export const NewSpaceModalButton: React.FC<NewSpaceModalButtonProps> = ({
const loading = submitLoading || submitImportLoading
const handleSubmit = (formData: SpaceFormData) => {
const handleSubmit = async (formData: SpaceFormData) => {
try {
const payload: OpenapiCreateSpaceRequestExtended = {
description: get(formData, 'description', '').trim(),
@ -120,42 +120,40 @@ export const NewSpaceModalButton: React.FC<NewSpaceModalButtonProps> = ({
uid: get(formData, 'name', '').trim(),
parent_id: standalone ? Number(space) : 0 // TODO: Backend needs to fix parentID: accept string or number
}
createSpace(payload)
.then(() => {
hideModal()
handleNavigation?.(formData.name.trim())
onRefetch()
})
.catch(_error => {
showError(getErrorMessage(_error), 0, getString('failedToCreateSpace'))
})
await createSpace(payload)
hideModal()
handleNavigation?.(formData.name.trim())
onRefetch()
} catch (exception) {
showError(getErrorMessage(exception), 0, getString('failedToCreateSpace'))
}
}
const handleImportSubmit = async (formData: ImportSpaceFormData) => {
const type = getProviderTypeMapping(formData.gitProvider)
const provider = {
type,
username: formData.username,
password: formData.password,
host: ''
}
if (![GitProviders.GITHUB, GitProviders.GITLAB, GitProviders.BITBUCKET].includes(formData.gitProvider)) {
provider.host = formData.host
}
try {
const importPayload = {
description: (formData.description || '').trim(),
uid: formData.name.trim(),
provider: {
type: formData.gitProvider.toLowerCase(),
username: formData.username,
password: formData.password,
host: formData.host
},
provider,
provider_space: formData.organization
}
await importSpace(importPayload)
.then(response => {
hideModal()
onSubmit(response)
onRefetch()
})
.catch(_error => {
showError(getErrorMessage(_error), 0, getString('failedToImportSpace'))
})
const response = await importSpace(importPayload)
hideModal()
onSubmit(response)
onRefetch()
} catch (exception) {
showError(getErrorMessage(exception), 0, getString('failedToImportSpace'))
}

View File

@ -170,6 +170,7 @@ export interface StringsMap {
enableWebhookTitle: string
enabled: string
enterAddress: string
enterBitbucketPlaceholder: string
enterBranchPlaceholder: string
enterDescription: string
enterGithubPlaceholder: string
@ -258,9 +259,17 @@ export interface StringsMap {
importGitRepo: string
importProgress: string
'importRepo.failedToImportRepo': string
'importRepo.group': string
'importRepo.groupPlaceholder': string
'importRepo.org': string
'importRepo.orgPlaceholder': string
'importRepo.passToken': string
'importRepo.passwordPlaceholder': string
'importRepo.passwordReq': string
'importRepo.project': string
'importRepo.projectPlaceholder': string
'importRepo.repo': string
'importRepo.repoPlaceholder': string
'importRepo.reqAuth': string
'importRepo.required': string
'importRepo.spaceNameReq': string
@ -270,6 +279,8 @@ export interface StringsMap {
'importRepo.userPlaceholder': string
'importRepo.usernameReq': string
'importRepo.validation': string
'importRepo.workspace': string
'importRepo.workspacePlaceholder': string
'importSpace.authorization': string
'importSpace.content': string
'importSpace.createASpace': string

View File

@ -750,8 +750,18 @@ createNewRepo: Create New repository
importGitRepo: Import Repository
importRepo:
title: Import Repository
url: Repository URL
urlPlaceholder: Enter the Repository URL
url: Host URL
org: Organization
workspace: Workspace
project: Project
group: Group
repo: Repository
urlPlaceholder: Enter the Host URL
repoPlaceholder: Enter the Repository name
orgPlaceholder: Enter the Organization name
workspacePlaceholder: Enter the Workspace name
projectPlaceholder: Enter the Project name
groupPlaceholder: Enter the Group name
reqAuth: Requires Authorization
userPlaceholder: Enter Username
passwordPlaceholder: Enter Password
@ -766,7 +776,7 @@ importSpace:
title: Import Project
createASpace: Create a project
authorization: Authorization
content: Import a GitLab Group or a GitHub Org to a new Project in Gitness. Entities at the top level will be imported to the project.
content: Import a GitLab Group, GitHub Org or Bitbucket Project to a new Project in Gitness. Entities at the top level will be imported to the project.
next: Next step
details: Details of target to import
organizationName: Organization Name
@ -828,6 +838,7 @@ generateHelptext: Let Gitness build your Pipeline for you.
enterAddress: Enter Address
enterGitlabPlaceholder: https://gitlab.com/
enterGithubPlaceholder: https://api.github.com/
enterBitbucketPlaceholder: https://bitbucket.org/
changeRepoVis: Change repository visibility
changeRepoVisContent: Are you sure you want to make this repository {repoVis}? {repoText}
repoVisibility: Repository Visibility

View File

@ -48,7 +48,10 @@ export interface RepoFormData {
isPublic: RepoVisibility
}
export interface ImportFormData {
repoUrl: string
gitProvider: GitProviders
hostUrl: string
org: string
repo: string
username: string
password: string
name: string
@ -68,7 +71,7 @@ export interface ExportFormDataExtended extends ExportFormData {
}
export interface ImportSpaceFormData {
gitProvider: string
gitProvider: GitProviders
username: string
password: string
name: string
@ -129,6 +132,16 @@ export enum PullRequestState {
CLOSED = 'closed'
}
export enum GitProviders {
GITHUB = 'GitHub',
GITHUB_ENTERPRISE = 'GitHub Enterprise',
GITLAB = 'GitLab',
GITLAB_SELF_HOSTED = 'GitLab Self-Hosted',
BITBUCKET = 'Bitbucket',
BITBUCKET_SERVER = 'Bitbucket Server'
// GITEA = 'Gitea' - not added on back end yet
}
export const PullRequestFilterOption = {
...PullRequestState,
// REJECTED: 'rejected',
@ -171,11 +184,6 @@ export const CodeIcon = {
ChecksSuccess: 'success-tick' as IconName
}
export enum Organization {
GITHUB = 'Github',
GITLAB = 'Gitlab'
}
export const normalizeGitRef = (gitRef: string | undefined) => {
if (isRefATag(gitRef)) {
return gitRef
@ -270,14 +278,13 @@ export const decodeGitContent = (content = '') => {
}
export const parseUrl = (url: string) => {
const pattern = /^(https?:\/\/(?:www\.)?(github|gitlab)\.com\/([^/]+\/[^/]+))/
const pattern = /^(https?:\/\/[^/]+)\/([^/]+\/[^/]+)/
const match = url.match(pattern)
if (match) {
const provider = match[2]
const fullRepo = match[3]
const repoName = match[3].split('/')[1].replace('.git', '')
return { provider, fullRepo, repoName }
const fullRepo = match[2]
const repoName = match[2].split('/')[1].replace('.git', '')
return { fullRepo, repoName }
} else {
return null
}
@ -285,3 +292,49 @@ export const parseUrl = (url: string) => {
// Check if gitRef is a git commit hash (https://github.com/diegohaz/is-git-rev, MIT © Diego Haz)
export const isGitRev = (gitRef = ''): boolean => /^[0-9a-f]{7,40}$/i.test(gitRef)
export const getProviderTypeMapping = (provider: GitProviders): string => {
switch (provider) {
case GitProviders.BITBUCKET_SERVER:
return 'stash'
case GitProviders.GITHUB_ENTERPRISE:
return 'github'
case GitProviders.GITLAB_SELF_HOSTED:
return 'gitlab'
default:
return provider.toLowerCase()
}
}
export const getOrgLabel = (gitProvider: string) => {
switch (gitProvider) {
case GitProviders.BITBUCKET:
return 'importRepo.workspace'
case GitProviders.BITBUCKET_SERVER:
return 'importRepo.project'
case GitProviders.GITLAB:
case GitProviders.GITLAB_SELF_HOSTED:
return 'importRepo.group'
default:
return 'importRepo.org'
}
}
export const getOrgPlaceholder = (gitProvider: string) => {
switch (gitProvider) {
case GitProviders.BITBUCKET:
return 'importRepo.workspacePlaceholder'
case GitProviders.BITBUCKET_SERVER:
return 'importRepo.projectPlaceholder'
case GitProviders.GITLAB:
case GitProviders.GITLAB_SELF_HOSTED:
return 'importRepo.groupPlaceholder'
default:
return 'importRepo.orgPlaceholder'
}
}
export const getProviders = () =>
Object.values(GitProviders).map(value => {
return { value, label: value }
})