diff --git a/src/sentry/static/sentry/images/logos/logo-perforce.svg b/src/sentry/static/sentry/images/logos/logo-perforce.svg
new file mode 100644
index 00000000000000..eb8c0c234101f5
--- /dev/null
+++ b/src/sentry/static/sentry/images/logos/logo-perforce.svg
@@ -0,0 +1,5 @@
+
diff --git a/static/app/icons/iconPerforce.tsx b/static/app/icons/iconPerforce.tsx
new file mode 100644
index 00000000000000..73512b9bba1766
--- /dev/null
+++ b/static/app/icons/iconPerforce.tsx
@@ -0,0 +1,10 @@
+import type {SVGIconProps} from './svgIcon';
+import {SvgIcon} from './svgIcon';
+
+export function IconPerforce(props: SVGIconProps) {
+ return (
+
+
+
+ );
+}
diff --git a/static/app/icons/index.tsx b/static/app/icons/index.tsx
index db9dbe8842decf..5f39dbc9541ec8 100644
--- a/static/app/icons/index.tsx
+++ b/static/app/icons/index.tsx
@@ -85,6 +85,7 @@ export {IconNumber} from './iconNumber';
export {IconOpen} from './iconOpen';
export {IconPanel} from './iconPanel';
export {IconPause} from './iconPause';
+export {IconPerforce} from './iconPerforce';
export {IconPin} from './iconPin';
export {IconPlay} from './iconPlay';
export {IconPrevent} from './iconPrevent';
diff --git a/static/app/plugins/components/pluginIcon.tsx b/static/app/plugins/components/pluginIcon.tsx
index 8736193dc69520..b413d24a72d0e4 100644
--- a/static/app/plugins/components/pluginIcon.tsx
+++ b/static/app/plugins/components/pluginIcon.tsx
@@ -17,6 +17,7 @@ import jumpcloud from 'sentry-logos/logo-jumpcloud.svg';
import msteams from 'sentry-logos/logo-msteams.svg';
import opsgenie from 'sentry-logos/logo-opsgenie.svg';
import pagerduty from 'sentry-logos/logo-pagerduty.svg';
+import perforce from 'sentry-logos/logo-perforce.svg';
import pivotal from 'sentry-logos/logo-pivotaltracker.svg';
import pushover from 'sentry-logos/logo-pushover.svg';
import redmine from 'sentry-logos/logo-redmine.svg';
@@ -57,6 +58,7 @@ const PLUGIN_ICONS = {
msteams,
opsgenie,
pagerduty,
+ perforce,
pivotal,
pushover,
redmine,
diff --git a/static/app/types/integrations.tsx b/static/app/types/integrations.tsx
index 0f47431aa62efa..97582eaa517da6 100644
--- a/static/app/types/integrations.tsx
+++ b/static/app/types/integrations.tsx
@@ -577,7 +577,7 @@ export type CodeOwner = {
users_without_access: string[];
};
id: string;
- provider: 'github' | 'gitlab';
+ provider: 'github' | 'gitlab' | 'perforce';
raw: string;
codeMapping?: RepositoryProjectPathConfig;
ownershipSyntax?: string;
diff --git a/static/app/utils/integrationUtil.tsx b/static/app/utils/integrationUtil.tsx
index b71d10a168f486..1f7462cbfca238 100644
--- a/static/app/utils/integrationUtil.tsx
+++ b/static/app/utils/integrationUtil.tsx
@@ -9,6 +9,7 @@ import {
IconGithub,
IconGitlab,
IconJira,
+ IconPerforce,
IconSentry,
IconVsts,
} from 'sentry/icons';
@@ -206,6 +207,8 @@ export const getIntegrationIcon = (
case 'jira':
case 'jira_server':
return ;
+ case 'perforce':
+ return ;
case 'vsts':
return ;
case 'codecov':
@@ -230,6 +233,8 @@ export const getIntegrationDisplayName = (integrationType?: string) => {
case 'jira':
case 'jira_server':
return 'Jira';
+ case 'perforce':
+ return 'Perforce';
case 'vsts':
return 'Azure DevOps';
case 'codecov':
@@ -279,6 +284,8 @@ export function getCodeOwnerIcon(
return ;
case 'gitlab':
return ;
+ case 'perforce':
+ return ;
default:
return ;
}
diff --git a/static/app/views/settings/organizationIntegrations/repositoryProjectPathConfigForm.tsx b/static/app/views/settings/organizationIntegrations/repositoryProjectPathConfigForm.tsx
index 7f3f322e1ca036..ca42e505d1fe02 100644
--- a/static/app/views/settings/organizationIntegrations/repositoryProjectPathConfigForm.tsx
+++ b/static/app/views/settings/organizationIntegrations/repositoryProjectPathConfigForm.tsx
@@ -58,6 +58,10 @@ function RepositoryProjectPathConfigForm({
}
);
+ // Stream-based VCS (like Perforce) use streams/codelines instead of branches
+ // and don't require a default branch to be specified
+ const isStreamBased = integration.provider.key === 'perforce';
+
// Effect to handle the case when integration repos data becomes available
useEffect(() => {
if (integrationReposData?.repos && selectedRepo) {
@@ -93,13 +97,19 @@ function RepositoryProjectPathConfigForm({
{
name: 'defaultBranch',
type: 'string',
- required: true,
- label: t('Branch'),
- placeholder: t('Type your branch'),
+ required: !isStreamBased,
+ label: isStreamBased ? t('Stream') : t('Branch'),
+ placeholder: isStreamBased
+ ? t('Type your stream (optional, e.g., main)')
+ : t('Type your branch'),
showHelpInTooltip: true,
- help: t(
- 'If an event does not have a release tied to a commit, we will use this branch when linking to your source code.'
- ),
+ help: isStreamBased
+ ? t(
+ 'Optional: Specify a stream/codeline (e.g., "main"). If not specified, the depot root will be used. Streams are part of the depot path in Perforce.'
+ )
+ : t(
+ 'If an event does not have a release tied to a commit, we will use this branch when linking to your source code.'
+ ),
},
{
name: 'stackRoot',
@@ -135,7 +145,7 @@ function RepositoryProjectPathConfigForm({
}
const initialData = {
- defaultBranch: 'main',
+ defaultBranch: isStreamBased ? '' : 'main',
stackRoot: '',
sourceRoot: '',
repositoryId: existingConfig?.repoId,
diff --git a/static/images/integrations/perforce.svg b/static/images/integrations/perforce.svg
new file mode 100644
index 00000000000000..eb8c0c234101f5
--- /dev/null
+++ b/static/images/integrations/perforce.svg
@@ -0,0 +1,5 @@
+