diff --git a/apps/studio/components/interfaces/Branching/MergeRequest.tsx b/apps/studio/components/interfaces/Branching/MergeRequest.tsx
new file mode 100644
index 0000000000..2381bf8cd5
--- /dev/null
+++ b/apps/studio/components/interfaces/Branching/MergeRequest.tsx
@@ -0,0 +1,248 @@
+import { LOCAL_STORAGE_KEYS, useParams } from 'common'
+import dayjs from 'dayjs'
+import { GitBranchIcon, GitMerge, MoreVertical, Shield } from 'lucide-react'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useMemo } from 'react'
+import { toast } from 'sonner'
+import {
+ Button,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from 'ui'
+import { TimestampInfo } from 'ui-patterns'
+
+import { useIsPgDeltaDiffEnabled } from '../App/FeaturePreview/FeaturePreviewContext'
+import { ReviewWithAI } from '../BranchManagement/ReviewWithAI'
+import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
+import { FeaturePreviewBadge } from '@/components/ui/FeaturePreviewBadge'
+import { useBranchUpdateMutation } from '@/data/branches/branch-update-mutation'
+import { useBranchesQuery } from '@/data/branches/branches-query'
+import { useProjectGitHubConnectionQuery } from '@/data/integrations/github-connections-query'
+import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
+import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
+import { useBranchMergeDiff } from '@/hooks/branches/useBranchMergeDiff'
+import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
+import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
+
+export const MergeTitle = () => {
+ const { ref } = useParams()
+ const { data: project } = useSelectedProjectQuery()
+ const pgDeltaDiffEnabled = useIsPgDeltaDiffEnabled()
+
+ const parentProjectRef = project?.parent_project_ref
+
+ const { data: branches } = useBranchesQuery(
+ { projectRef: parentProjectRef },
+ {
+ refetchOnMount: 'always',
+ refetchOnWindowFocus: true,
+ staleTime: 0,
+ }
+ )
+ const currentBranch = branches?.find((branch) => branch.project_ref === ref)
+ const mainBranch = branches?.find((branch) => branch.is_default)
+
+ return (
+
+
+ Merge
+
+
+
+ {currentBranch?.name}
+
+
+ into
+
+
+
+
+ {mainBranch?.name || 'main'}
+
+
+
+
+ {pgDeltaDiffEnabled && (
+
+ )}
+
+ )
+}
+
+export const MergeSubtitle = () => {
+ const { ref } = useParams()
+ const { data: project } = useSelectedProjectQuery()
+ const parentProjectRef = project?.parent_project_ref
+
+ const { data: branches } = useBranchesQuery(
+ { projectRef: parentProjectRef },
+ {
+ refetchOnMount: 'always',
+ refetchOnWindowFocus: true,
+ staleTime: 0,
+ }
+ )
+ const currentBranch = branches?.find((branch) => branch.project_ref === ref)
+
+ const subtitle = useMemo(() => {
+ if (!currentBranch?.created_at) return 'Branch information unavailable'
+
+ if (!currentBranch?.review_requested_at) {
+ return 'Not ready for review'
+ }
+
+ const reviewRequestedTime = dayjs(currentBranch.review_requested_at).fromNow()
+ return (
+ <>
+ Request opened{' '}
+
+ >
+ )
+ }, [currentBranch?.created_at, currentBranch?.review_requested_at])
+
+ return {subtitle}
+}
+
+export const MergeActions = ({
+ isWorkflowRunning,
+ isSubmitting,
+ onSelectMerge,
+}: {
+ isWorkflowRunning: boolean
+ isSubmitting: boolean
+ onSelectMerge: () => void
+}) => {
+ const router = useRouter()
+ const { ref } = useParams()
+ const { data: project } = useSelectedProjectQuery()
+ const { data: selectedOrg } = useSelectedOrganizationQuery()
+
+ const { mutate: sendEvent } = useSendEventMutation()
+ const { mutate: updateBranch, isPending: isUpdating } = useBranchUpdateMutation({
+ onError: (error) => {
+ toast.error(`Failed to update branch: ${error.message}`)
+ },
+ })
+
+ const parentProjectRef = project?.parent_project_ref
+ const { data: parentProject } = useProjectDetailQuery({ ref: parentProjectRef })
+ const { data: ghConnection } = useProjectGitHubConnectionQuery({ ref: parentProjectRef })
+
+ const { data: branches } = useBranchesQuery(
+ { projectRef: parentProjectRef },
+ {
+ refetchOnMount: 'always',
+ refetchOnWindowFocus: true,
+ staleTime: 0,
+ }
+ )
+ const currentBranch = branches?.find((branch) => branch.project_ref === ref)
+ const mainBranch = branches?.find((branch) => branch.is_default)
+
+ const {
+ diffContent,
+ isBranchOutOfDateOverall,
+ isLoading: isCombinedDiffLoading,
+ hasChanges: combinedHasChanges,
+ } = useBranchMergeDiff({
+ currentBranchRef: ref,
+ parentProjectRef,
+ currentBranchConnectionString: project?.connectionString || undefined,
+ parentBranchConnectionString: parentProject?.connectionString || undefined,
+ currentBranchCreatedAt: currentBranch?.created_at,
+ })
+
+ const isMergeDisabled =
+ !combinedHasChanges ||
+ isCombinedDiffLoading ||
+ isBranchOutOfDateOverall ||
+ isWorkflowRunning ||
+ (!!ghConnection && Boolean(mainBranch?.git_branch))
+
+ return (
+
+
+ {isMergeDisabled ? (
+ }
+ >
+ Merge branch
+
+ ) : (
+ }
+ >
+ Merge branch
+
+ )}
+
+
+
+ } />
+
+
+ {
+ if (!ref || !parentProjectRef) return
+ updateBranch(
+ {
+ branchRef: ref,
+ projectRef: parentProjectRef,
+ requestReview: false,
+ },
+ {
+ onSuccess: () => {
+ toast.success('Successfully closed merge request')
+ router.push(`/project/${project?.ref}/branches?tab=prs`)
+ sendEvent({
+ action: 'branch_close_merge_request_button_clicked',
+ groups: {
+ project: parentProjectRef ?? 'Unknown',
+ organization: selectedOrg?.slug ?? 'Unknown',
+ },
+ })
+ },
+ }
+ )
+ }}
+ >
+ Close this merge request
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Settings/Integrations/IntegrationsSettings.tsx b/apps/studio/components/interfaces/Settings/Integrations/IntegrationsSettings.tsx
index 39c6b7b939..fb58bd8ffd 100644
--- a/apps/studio/components/interfaces/Settings/Integrations/IntegrationsSettings.tsx
+++ b/apps/studio/components/interfaces/Settings/Integrations/IntegrationsSettings.tsx
@@ -47,6 +47,7 @@ export const IntegrationSettings = () => {
)}
+
{showVercelIntegration && (
diff --git a/apps/studio/data/integrations/github-connections-query.ts b/apps/studio/data/integrations/github-connections-query.ts
index 336171ef15..cb22cf1916 100644
--- a/apps/studio/data/integrations/github-connections-query.ts
+++ b/apps/studio/data/integrations/github-connections-query.ts
@@ -1,7 +1,9 @@
import { useQuery } from '@tanstack/react-query'
+import { useMemo } from 'react'
import { integrationKeys } from './keys'
import { get, handleError } from '@/data/fetchers'
+import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import type { ResponseError, UseCustomQueryOptions } from '@/types'
export type GitHubConnectionsVariables = {
@@ -47,3 +49,17 @@ export const useGitHubConnectionsQuery = (
...options,
})
}
+
+export const useProjectGitHubConnectionQuery = ({ ref }: { ref?: string }) => {
+ const { data: organization } = useSelectedOrganizationQuery()
+ const { data: connections, ...props } = useGitHubConnectionsQuery(
+ { organizationId: organization?.id },
+ { enabled: !!ref && !!organization?.id }
+ )
+
+ const existingConnection = useMemo(
+ () => connections?.find((c) => c.project.ref === ref),
+ [connections, ref]
+ )
+ return { data: existingConnection, ...props }
+}
diff --git a/apps/studio/pages/project/[ref]/merge.tsx b/apps/studio/pages/project/[ref]/merge.tsx
index 4a823d87f7..98bc528cae 100644
--- a/apps/studio/pages/project/[ref]/merge.tsx
+++ b/apps/studio/pages/project/[ref]/merge.tsx
@@ -1,41 +1,35 @@
-import { LOCAL_STORAGE_KEYS, useParams } from 'common'
-import dayjs from 'dayjs'
-import { AlertTriangle, GitBranchIcon, GitMerge, MoreVertical, Shield, X } from 'lucide-react'
+import { useParams } from 'common'
+import { AlertTriangle, GitBranchIcon, X } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
-import {
- Badge,
- Button,
- cn,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
- NavMenu,
- NavMenuItem,
-} from 'ui'
+import { Button, cn, NavMenu, NavMenuItem } from 'ui'
+import { Admonition } from 'ui-patterns'
import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal'
import { useIsPgDeltaDiffEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
+import {
+ MergeActions,
+ MergeSubtitle,
+ MergeTitle,
+} from '@/components/interfaces/Branching/MergeRequest'
import { DatabaseDiffPanel } from '@/components/interfaces/BranchManagement/DatabaseDiffPanel'
import { EdgeFunctionsDiffPanel } from '@/components/interfaces/BranchManagement/EdgeFunctionsDiffPanel'
import { OutOfDateNotice } from '@/components/interfaces/BranchManagement/OutOfDateNotice'
-import { ReviewWithAI } from '@/components/interfaces/BranchManagement/ReviewWithAI'
import { WorkflowLogsCard } from '@/components/interfaces/BranchManagement/WorkflowLogsCard'
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
import { PageLayout } from '@/components/layouts/PageLayout/PageLayout'
import { ProjectLayoutWithAuth } from '@/components/layouts/ProjectLayout'
import { ScaffoldContainer } from '@/components/layouts/Scaffold'
import ProductEmptyState from '@/components/to-be-cleaned/ProductEmptyState'
-import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
-import { FeaturePreviewBadge } from '@/components/ui/FeaturePreviewBadge'
+import { InlineLink } from '@/components/ui/InlineLink'
import { useBranchDeleteMutation } from '@/data/branches/branch-delete-mutation'
import { useBranchMergeMutation } from '@/data/branches/branch-merge-mutation'
import { useBranchPushMutation } from '@/data/branches/branch-push-mutation'
import { useBranchUpdateMutation } from '@/data/branches/branch-update-mutation'
import { useBranchesQuery } from '@/data/branches/branches-query'
+import { useProjectGitHubConnectionQuery } from '@/data/integrations/github-connections-query'
import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
import { useBranchMergeDiff } from '@/hooks/branches/useBranchMergeDiff'
@@ -59,6 +53,7 @@ const MergePage: NextPageWithLayout = () => {
const parentProjectRef = project?.parent_project_ref
const { data: parentProject } = useProjectDetailQuery({ ref: parentProjectRef })
+ const { data: ghConnection } = useProjectGitHubConnectionQuery({ ref: parentProjectRef })
const { data: branches } = useBranchesQuery(
{ projectRef: parentProjectRef },
@@ -87,8 +82,6 @@ const MergePage: NextPageWithLayout = () => {
isBranchOutOfDateOverall,
missingMigrationsCount,
modifiedFunctionsCount,
- isLoading: isCombinedDiffLoading,
- hasChanges: combinedHasChanges,
} = useBranchMergeDiff({
currentBranchRef: ref,
parentProjectRef,
@@ -97,7 +90,7 @@ const MergePage: NextPageWithLayout = () => {
currentBranchCreatedAt: currentBranch?.created_at,
})
- const { mutate: updateBranch, isPending: isUpdating } = useBranchUpdateMutation({
+ const { mutate: updateBranch } = useBranchUpdateMutation({
onError: (error) => {
toast.error(`Failed to update branch: ${error.message}`)
},
@@ -359,143 +352,42 @@ const MergePage: NextPageWithLayout = () => {
)
}
- const isMergeDisabled =
- !combinedHasChanges ||
- isCombinedDiffLoading ||
- isBranchOutOfDateOverall ||
- isWorkflowRunning ||
- Boolean(mainBranch?.git_branch)
-
- const primaryActions = (
-
-
- {isMergeDisabled ? (
- setShowConfirmDialog(true)}
- icon={ }
- >
- Merge branch
-
- ) : (
- setShowConfirmDialog(true)}
- icon={ }
- >
- Merge branch
-
- )}
-
-
- } />
-
-
- {
- if (!ref || !parentProjectRef) return
- updateBranch(
- {
- branchRef: ref,
- projectRef: parentProjectRef,
- requestReview: false,
- },
- {
- onSuccess: () => {
- toast.success('Successfully closed merge request')
- router.push(`/project/${project?.ref}/branches?tab=prs`)
- sendEvent({
- action: 'branch_close_merge_request_button_clicked',
- groups: {
- project: parentProjectRef ?? 'Unknown',
- organization: selectedOrg?.slug ?? 'Unknown',
- },
- })
- },
- }
- )
- }}
- >
- Close this merge request
-
-
-
-
- )
-
- const pageTitle = () => (
-
- Merge
-
-
-
-
- {currentBranch.name}
-
-
-
- into
-
-
-
-
- {mainBranch?.name || 'main'}
-
-
-
- {pgDeltaDiffEnabled && (
-
- )}
-
- )
-
- const pageSubtitle = () => {
- if (!currentBranch?.created_at) return 'Branch information unavailable'
-
- if (!currentBranch?.review_requested_at) {
- return 'Not ready for review'
- }
-
- const reviewRequestedTime = dayjs(currentBranch.review_requested_at).fromNow()
- return `Request opened ${reviewRequestedTime}`
- }
+ const hasGHProductionDeployEnabled = !!ghConnection && Boolean(mainBranch?.git_branch)
return (
}
+ subtitle={ }
breadcrumbs={breadcrumbs}
- primaryActions={primaryActions}
+ primaryActions={
+ setShowConfirmDialog(true)}
+ />
+ }
size="full"
className="h-full border-b-0 pb-0"
>
- {isBranchOutOfDateOverall && !currentWorkflowRunId ? (
+ {hasGHProductionDeployEnabled ? (
+
+
+ Branches should be managed via GitHub to prevent drifts in migrations from your
+ repository's state. You may either move your schema changes to a GitHub pull
+ request, or disable "Deploy to production" in the{' '}
+
+ GitHub integration settings
+
+ .
+
+
+ ) : isBranchOutOfDateOverall && !currentWorkflowRunId ? (
{
+
{currentTab === 'database' ? (