Merge pull request #28847 from appsmithorg/release_v1.9.45.1
Release v1.9.46
This commit is contained in:
commit
c69226e4d9
|
|
@ -2,37 +2,9 @@ import { featureFlagIntercept } from "../../../../../support/Objects/FeatureFlag
|
|||
import * as _ from "../../../../../support/Objects/ObjectsCore";
|
||||
|
||||
let guid: any;
|
||||
let repoName1: any;
|
||||
let repoName2: any;
|
||||
let repoName: any;
|
||||
|
||||
describe("Git Branch Protection", function () {
|
||||
it("Issue 28056 - 1 : Check if protection is not enabled when feature flag is disabled", function () {
|
||||
_.agHelper.GenerateUUID();
|
||||
cy.get("@guid").then((uid) => {
|
||||
guid = uid;
|
||||
const wsName = "GitBranchProtect-1" + uid;
|
||||
const appName = "GitBranchProtect-1" + uid;
|
||||
_.homePage.CreateNewWorkspace(wsName, true);
|
||||
_.homePage.CreateAppInWorkspace(wsName, appName);
|
||||
featureFlagIntercept({
|
||||
release_git_connect_v2_enabled: true,
|
||||
release_git_branch_protection_enabled: false,
|
||||
});
|
||||
cy.wait(1000);
|
||||
_.gitSync.CreateNConnectToGitV2();
|
||||
cy.get("@gitRepoName").then((repName) => {
|
||||
repoName1 = repName;
|
||||
_.agHelper.AssertElementExist(_.entityExplorer._entityExplorerWrapper);
|
||||
_.agHelper.AssertElementExist(_.propPane._propertyPaneSidebar);
|
||||
_.agHelper.AssertElementEnabledDisabled(
|
||||
_.gitSync._bottomBarCommit,
|
||||
0,
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("Issue 28056 - 2 : Check if protection is enabled when feature flag is enabled", function () {
|
||||
_.agHelper.GenerateUUID();
|
||||
cy.get("@guid").then((uid) => {
|
||||
|
|
@ -43,7 +15,6 @@ describe("Git Branch Protection", function () {
|
|||
_.homePage.CreateAppInWorkspace(wsName, appName);
|
||||
featureFlagIntercept({
|
||||
release_git_connect_v2_enabled: true,
|
||||
release_git_branch_protection_enabled: true,
|
||||
});
|
||||
cy.wait(1000);
|
||||
|
||||
|
|
@ -54,7 +25,7 @@ describe("Git Branch Protection", function () {
|
|||
|
||||
_.gitSync.CreateNConnectToGitV2();
|
||||
cy.get("@gitRepoName").then((repName) => {
|
||||
repoName2 = repName;
|
||||
repoName = repName;
|
||||
cy.wait("@gitProtectApi").then((res1) => {
|
||||
expect(res1.response).to.have.property("statusCode", 200);
|
||||
_.agHelper.AssertElementVisibility(
|
||||
|
|
@ -76,7 +47,6 @@ describe("Git Branch Protection", function () {
|
|||
});
|
||||
|
||||
after(() => {
|
||||
_.gitSync.DeleteTestGithubRepo(repoName1);
|
||||
_.gitSync.DeleteTestGithubRepo(repoName2);
|
||||
_.gitSync.DeleteTestGithubRepo(repoName);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ let ws1Name: string;
|
|||
let ws2Name: string;
|
||||
let app1Name: string;
|
||||
let repoName: any;
|
||||
let branchName: any;
|
||||
|
||||
describe("Git Connect V2", function () {
|
||||
before(() => {
|
||||
|
|
@ -36,17 +37,21 @@ describe("Git Connect V2", function () {
|
|||
release_git_connect_v2_enabled: true,
|
||||
});
|
||||
|
||||
_.entityExplorer.DragDropWidgetNVerify(_.draggableWidgets.TEXT, 300, 300);
|
||||
_.propPane.RenameWidget("Text1", "MyText");
|
||||
_.propPane.UpdatePropertyFieldValue("Text", "Hello World");
|
||||
_.gitSync.CommitAndPush();
|
||||
_.gitSync.CreateGitBranch("test", true);
|
||||
cy.get("@gitbranchName").then((bName) => {
|
||||
branchName = bName;
|
||||
_.entityExplorer.DragDropWidgetNVerify(_.draggableWidgets.TEXT, 300, 300);
|
||||
_.propPane.RenameWidget("Text1", "MyText");
|
||||
_.propPane.UpdatePropertyFieldValue("Text", "Hello World");
|
||||
_.gitSync.CommitAndPush();
|
||||
|
||||
_.gitSync.ImportAppFromGitV2(ws2Name, repoName);
|
||||
|
||||
_.entityExplorer.ExpandCollapseEntity("Widgets");
|
||||
_.entityExplorer.AssertEntityPresenceInExplorer("MyText");
|
||||
_.entityExplorer.SelectEntityByName("MyText");
|
||||
_.propPane.ValidatePropertyFieldValue("Text", "Hello World");
|
||||
_.gitSync.ImportAppFromGitV2(ws2Name, repoName);
|
||||
_.gitSync.SwitchGitBranch(branchName);
|
||||
_.entityExplorer.ExpandCollapseEntity("Widgets");
|
||||
_.entityExplorer.AssertEntityPresenceInExplorer("MyText");
|
||||
_.entityExplorer.SelectEntityByName("MyText");
|
||||
_.propPane.ValidatePropertyFieldValue("Text", "Hello World");
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ describe("excludeForAirgap", "Tests JS Library with Git", () => {
|
|||
installer.uninstallLibrary("uuidjs");
|
||||
installer.assertUnInstall("uuidjs");
|
||||
// discard js library uninstallation
|
||||
cy.gitDiscardChanges();
|
||||
gitSync.DiscardChanges();
|
||||
// verify js library is present
|
||||
entityExplorer.ExpandCollapseEntity("Libraries");
|
||||
installer.AssertLibraryinExplorer("uuidjs");
|
||||
|
|
|
|||
|
|
@ -474,9 +474,9 @@ export class GitSync {
|
|||
this.agHelper.AssertContains(
|
||||
Cypress.env("MESSAGES").DISCARDING_AND_PULLING_CHANGES(),
|
||||
);
|
||||
this.agHelper.AssertContains("Discarded changes successfully");
|
||||
this.assertHelper.AssertNetworkStatus("@discardChanges");
|
||||
this.assertHelper.AssertNetworkStatus("@gitStatus");
|
||||
this.agHelper.AssertContains("Discarded changes successfully");
|
||||
this.agHelper.AssertElementExist(this._bottomBarCommit, 0, 30000);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -196,8 +196,11 @@ export const fetchGitRemoteStatusSuccess = (payload: GitRemoteStatusData) => ({
|
|||
payload,
|
||||
});
|
||||
|
||||
export const discardChanges = () => ({
|
||||
export const discardChanges = (
|
||||
payload: { successToastMessage?: string } | undefined | null = {},
|
||||
) => ({
|
||||
type: ReduxActionTypes.GIT_DISCARD_CHANGES,
|
||||
payload,
|
||||
});
|
||||
|
||||
export const discardChangesSuccess = (payload: any) => ({
|
||||
|
|
|
|||
|
|
@ -508,7 +508,6 @@ export const PAGE_SERVER_UNAVAILABLE_ERROR_MESSAGES = (
|
|||
export const POST = () => "Post";
|
||||
export const CANCEL = () => "Cancel";
|
||||
export const REMOVE = () => "Remove";
|
||||
export const CREATE = () => "Create";
|
||||
|
||||
// Showcase Carousel
|
||||
export const NEXT = () => "NEXT";
|
||||
|
|
@ -843,6 +842,7 @@ export const COMMITTING_AND_PUSHING_CHANGES = () =>
|
|||
export const DISCARDING_AND_PULLING_CHANGES = () =>
|
||||
"Discarding and pulling changes...";
|
||||
export const DISCARD_SUCCESS = () => "Discarded changes successfully.";
|
||||
export const DISCARD_AND_PULL_SUCCESS = () => "Pulled from remote successfully";
|
||||
|
||||
export const IS_MERGING = () => "Merging changes...";
|
||||
|
||||
|
|
@ -1033,7 +1033,7 @@ export const ADD_DEPLOY_KEY_STEP_TITLE = () =>
|
|||
export const HOW_TO_ADD_DEPLOY_KEY = () =>
|
||||
"How to paste SSH Key in repo and give write access?";
|
||||
export const CONSENT_ADDED_DEPLOY_KEY = () =>
|
||||
"I've added deploy key and gave it write access";
|
||||
"I've added the deploy key and gave it write access";
|
||||
export const PREVIOUS_STEP = () => "Previous step";
|
||||
export const GIT_CONNECT_SUCCESS_TITLE = () =>
|
||||
"Successfully connected to your Git remote repository";
|
||||
|
|
@ -1082,13 +1082,16 @@ export const BRANCH_PROTECTION_CHANGE_RULE = () =>
|
|||
"You can remove protection on your default branch in Git settings.";
|
||||
export const BRANCH_TOOLTIP_TITLE = () => "🚫 This is a protected branch";
|
||||
export const BRANCH_TOOLTIP_MESSAGE = () =>
|
||||
"You can remove protection on your default branch in Git settings.";
|
||||
"Please create a new branch or checkout an existing one to edit the app.";
|
||||
export const GO_TO_SETTINGS = () => "Go to settings";
|
||||
export const NOW_PROTECT_BRANCH = () =>
|
||||
"You can now protect your default branch.";
|
||||
export const APPSMITH_ENTERPRISE = () => "Appsmith Enterprise";
|
||||
export const PROTECT_BRANCH_SUCCESS = () => "Changed protected branches";
|
||||
export const UPDATE_DEFAULT_BRANCH_SUCCESS = () => "Updated default branch";
|
||||
export const UPDATE_DEFAULT_BRANCH_SUCCESS = (branchName: string) =>
|
||||
`Updated default branch ${!!branchName ? `to ${branchName}` : ""}`;
|
||||
export const CONTACT_ADMIN_FOR_GIT = () =>
|
||||
"Please contact your workspace admin to connect your app to a git repo";
|
||||
// Git Branch Protection end
|
||||
|
||||
export const NAV_DESCRIPTION = () =>
|
||||
|
|
|
|||
|
|
@ -28,8 +28,6 @@ export const FEATURE_FLAG = {
|
|||
ab_show_templates_instead_of_blank_canvas_enabled:
|
||||
"ab_show_templates_instead_of_blank_canvas_enabled",
|
||||
release_app_sidebar_enabled: "release_app_sidebar_enabled",
|
||||
release_git_branch_protection_enabled:
|
||||
"release_git_branch_protection_enabled",
|
||||
license_git_branch_protection_enabled:
|
||||
"license_git_branch_protection_enabled",
|
||||
license_widget_rtl_support_enabled: "license_widget_rtl_support_enabled",
|
||||
|
|
@ -61,7 +59,6 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = {
|
|||
release_anvil_enabled: false,
|
||||
ab_show_templates_instead_of_blank_canvas_enabled: false,
|
||||
release_app_sidebar_enabled: false,
|
||||
release_git_branch_protection_enabled: false,
|
||||
license_git_branch_protection_enabled: false,
|
||||
license_widget_rtl_support_enabled: false,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ import {
|
|||
createMessage,
|
||||
DELETING_APPLICATION,
|
||||
DELETING_MULTIPLE_APPLICATION,
|
||||
DISCARD_SUCCESS,
|
||||
ERROR_IMPORTING_APPLICATION_TO_WORKSPACE,
|
||||
} from "@appsmith/constants/messages";
|
||||
import { APP_MODE } from "entities/App";
|
||||
|
|
@ -300,12 +299,6 @@ export function* fetchAppAndPagesSaga(
|
|||
},
|
||||
});
|
||||
|
||||
if (localStorage.getItem("GIT_DISCARD_CHANGES") === "success") {
|
||||
toast.show(createMessage(DISCARD_SUCCESS), {
|
||||
kind: "success",
|
||||
});
|
||||
localStorage.setItem("GIT_DISCARD_CHANGES", "");
|
||||
}
|
||||
yield put({
|
||||
type: ReduxActionTypes.SET_APP_VERSION_ON_WORKER,
|
||||
payload: response.data.application?.evaluationVersion,
|
||||
|
|
|
|||
|
|
@ -185,6 +185,9 @@ export type EventName =
|
|||
| "GS_GENERATE_KEY_BUTTON_CLICK"
|
||||
| "GS_CONNECT_BUTTON_ON_GIT_SYNC_MODAL_CLICK"
|
||||
| "GS_START_USING_GIT"
|
||||
| "GS_DEFAULT_BRANCH_UPDATE"
|
||||
| "GS_PROTECTED_BRANCHES_UPDATE"
|
||||
| "GS_OPEN_GIT_SETTINGS"
|
||||
| "GIT_DISCARD_WARNING"
|
||||
| "GIT_DISCARD_CANCEL"
|
||||
| "GIT_DISCARD"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from "@appsmith/constants/messages";
|
||||
import { Button } from "design-system";
|
||||
import { KBEditorMenuItem } from "@appsmith/pages/Editor/KnowledgeBase/KBEditorMenuItem";
|
||||
import { useIsGitAdmin } from "pages/Editor/gitSync/hooks/useIsGitAdmin";
|
||||
|
||||
interface Props {
|
||||
trigger: ReactNode;
|
||||
|
|
@ -21,6 +22,7 @@ interface Props {
|
|||
export const DeployLinkButton = (props: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const isGitConnected = useSelector(getIsGitConnected);
|
||||
const isGitAdmin = useIsGitAdmin();
|
||||
|
||||
const goToGitConnectionPopup = () => {
|
||||
AnalyticsUtil.logEvent("GS_CONNECT_GIT_CLICK", {
|
||||
|
|
@ -46,7 +48,7 @@ export const DeployLinkButton = (props: Props) => {
|
|||
/>
|
||||
</MenuTrigger>
|
||||
<MenuContent>
|
||||
{!isGitConnected && (
|
||||
{!isGitConnected && isGitAdmin && (
|
||||
<MenuItem
|
||||
className="t--connect-to-git-btn"
|
||||
onClick={goToGitConnectionPopup}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ export const PROVISIONING_SETUP_DOC =
|
|||
"http://docs.appsmith.com/advanced-concepts/user-provisioning-group-sync";
|
||||
export const DISCORD_URL = "https://discord.gg/rBTTVJp";
|
||||
export const ENTERPRISE_PRICING_PAGE = "https://www.appsmith.com/enterprise";
|
||||
export const DOCS_BRANCH_PROTECTION_URL =
|
||||
"https://docs.appsmith.com/advanced-concepts/version-control-with-git/working-with-branches#branch-protection";
|
||||
export const DOCS_DEFAULT_BRANCH_URL =
|
||||
"https://docs.appsmith.com/advanced-concepts/version-control-with-git/working-with-branches#default-branch";
|
||||
|
||||
export const PRICING_PAGE_URL = (
|
||||
URL: string,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import BranchButton from "./BranchButton";
|
||||
|
|
@ -11,7 +11,9 @@ import {
|
|||
CONNECT_GIT,
|
||||
CONNECT_GIT_BETA,
|
||||
CONNECTING_TO_REPO_DISABLED,
|
||||
CONTACT_ADMIN_FOR_GIT,
|
||||
createMessage,
|
||||
DISCARD_AND_PULL_SUCCESS,
|
||||
DURING_ONBOARDING_TOUR,
|
||||
GIT_SETTINGS,
|
||||
MERGE,
|
||||
|
|
@ -44,6 +46,7 @@ import { Button, Icon, Tooltip } from "design-system";
|
|||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
|
||||
import { useIsGitAdmin } from "../hooks/useIsGitAdmin";
|
||||
|
||||
interface QuickActionButtonProps {
|
||||
className?: string;
|
||||
|
|
@ -127,14 +130,10 @@ function QuickActionButton({
|
|||
);
|
||||
}
|
||||
|
||||
const getPullBtnStatus = (
|
||||
gitStatus: any,
|
||||
pullFailed: boolean,
|
||||
isProtected: boolean,
|
||||
) => {
|
||||
const getPullBtnStatus = (gitStatus: any, pullFailed: boolean) => {
|
||||
const { behindCount, isClean } = gitStatus || {};
|
||||
let message = createMessage(NO_COMMITS_TO_PULL);
|
||||
let disabled = isProtected ? false : behindCount === 0;
|
||||
let disabled = behindCount === 0;
|
||||
if (!isClean) {
|
||||
disabled = true;
|
||||
message = createMessage(CANNOT_PULL_WITH_LOCAL_UNCOMMITTED_CHANGES);
|
||||
|
|
@ -187,7 +186,7 @@ const getQuickActionButtons = ({
|
|||
},
|
||||
{
|
||||
className: "t--bottom-bar-pull",
|
||||
count: isProtectedMode ? undefined : gitStatus?.behindCount,
|
||||
count: gitStatus?.behindCount,
|
||||
icon: "down-arrow-2",
|
||||
onClick: () => !pullDisabled && pull(),
|
||||
tooltipText: pullTooltipMessage,
|
||||
|
|
@ -236,21 +235,35 @@ const OuterContainer = styled.div`
|
|||
height: 100%;
|
||||
`;
|
||||
|
||||
const CenterDiv = styled.div`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
function ConnectGitPlaceholder() {
|
||||
const dispatch = useDispatch();
|
||||
const isInGuidedTour = useSelector(inGuidedTour);
|
||||
const isTooltipEnabled = isInGuidedTour;
|
||||
const tooltipContent = !isInGuidedTour ? (
|
||||
<>
|
||||
<div>{createMessage(NOT_LIVE_FOR_YOU_YET)}</div>
|
||||
<div>{createMessage(COMING_SOON)}</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div>{createMessage(CONNECTING_TO_REPO_DISABLED)}</div>
|
||||
<div>{createMessage(DURING_ONBOARDING_TOUR)}</div>
|
||||
</>
|
||||
);
|
||||
const isGitAdmin = useIsGitAdmin();
|
||||
const isTooltipEnabled = isInGuidedTour || !isGitAdmin;
|
||||
const tooltipContent = useMemo(() => {
|
||||
if (!isGitAdmin) {
|
||||
return <CenterDiv>{createMessage(CONTACT_ADMIN_FOR_GIT)}</CenterDiv>;
|
||||
}
|
||||
if (isInGuidedTour) {
|
||||
return (
|
||||
<>
|
||||
<div>{createMessage(CONNECTING_TO_REPO_DISABLED)}</div>
|
||||
<div>{createMessage(DURING_ONBOARDING_TOUR)}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div>{createMessage(NOT_LIVE_FOR_YOU_YET)}</div>
|
||||
<div>{createMessage(COMING_SOON)}</div>
|
||||
</>
|
||||
);
|
||||
}, [isInGuidedTour, isGitAdmin]);
|
||||
|
||||
const isGitConnectionEnabled = !isInGuidedTour;
|
||||
|
||||
return (
|
||||
|
|
@ -265,6 +278,7 @@ function ConnectGitPlaceholder() {
|
|||
{isGitConnectionEnabled ? (
|
||||
<Button
|
||||
className="t--connect-git-bottom-bar"
|
||||
isDisabled={!isGitAdmin}
|
||||
kind="secondary"
|
||||
onClick={() => {
|
||||
AnalyticsUtil.logEvent("GS_CONNECT_GIT_CLICK", {
|
||||
|
|
@ -301,7 +315,7 @@ export default function QuickGitActions() {
|
|||
const isProtectedMode = useSelector(protectedModeSelector);
|
||||
|
||||
const { disabled: pullDisabled, message: pullTooltipMessage } =
|
||||
getPullBtnStatus(gitStatus, !!pullFailed, isProtectedMode);
|
||||
getPullBtnStatus(gitStatus, !!pullFailed);
|
||||
|
||||
const isPullInProgress = useSelector(getPullInProgress);
|
||||
const isFetchingGitStatus = useSelector(getIsFetchingGitStatus);
|
||||
|
|
@ -343,7 +357,11 @@ export default function QuickGitActions() {
|
|||
source: "BOTTOM_BAR_GIT_PULL_BUTTON",
|
||||
});
|
||||
if (isProtectedMode) {
|
||||
dispatch(discardChanges());
|
||||
dispatch(
|
||||
discardChanges({
|
||||
successToastMessage: createMessage(DISCARD_AND_PULL_SUCCESS),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(gitPullInit({ triggeredFromBottomBar: true }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {
|
|||
BRANCH_PROTECTION_RULE_1,
|
||||
BRANCH_PROTECTION_RULE_2,
|
||||
BRANCH_PROTECTION_RULE_3,
|
||||
GIT_CONNECT_SUCCESS_MESSAGE,
|
||||
GIT_CONNECT_SUCCESS_TITLE,
|
||||
OPEN_GIT_SETTINGS,
|
||||
START_USING_GIT,
|
||||
|
|
@ -21,10 +20,6 @@ import { useDispatch, useSelector } from "react-redux";
|
|||
import styled from "styled-components";
|
||||
import { getCurrentAppGitMetaData } from "@appsmith/selectors/applicationSelectors";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import {
|
||||
getDefaultGitBranchName,
|
||||
getIsGitProtectedFeatureEnabled,
|
||||
} from "selectors/gitSyncSelectors";
|
||||
|
||||
const Container = styled.div``;
|
||||
|
||||
|
|
@ -77,10 +72,6 @@ const features = [
|
|||
|
||||
function ConnectionSuccess() {
|
||||
const gitMetadata = useSelector(getCurrentAppGitMetaData);
|
||||
const defaultBranchName = useSelector(getDefaultGitBranchName);
|
||||
const isGitProtectedFeatureEnabled = useSelector(
|
||||
getIsGitProtectedFeatureEnabled,
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -105,24 +96,20 @@ function ConnectionSuccess() {
|
|||
tab: GitSyncModalTab.SETTINGS,
|
||||
}),
|
||||
);
|
||||
AnalyticsUtil.logEvent("GS_START_USING_GIT", {
|
||||
AnalyticsUtil.logEvent("GS_OPEN_GIT_SETTINGS", {
|
||||
repoUrl: gitMetadata?.remoteUrl,
|
||||
});
|
||||
};
|
||||
|
||||
const preBranchProtectionContent = () => {
|
||||
return (
|
||||
<Text renderAs="p">{createMessage(GIT_CONNECT_SUCCESS_MESSAGE)}</Text>
|
||||
);
|
||||
};
|
||||
|
||||
const postBranchProtectionContent = () => {
|
||||
const branchProtectionContent = () => {
|
||||
return (
|
||||
<>
|
||||
<DefaultBranchMessage renderAs="p">
|
||||
Right now,{" "}
|
||||
<BranchTag isClosable={false}>{defaultBranchName}</BranchTag> is set
|
||||
as the default branch and it is protected.
|
||||
<BranchTag isClosable={false}>
|
||||
{gitMetadata?.defaultBranchName}
|
||||
</BranchTag>{" "}
|
||||
is set as the default branch and it is protected.
|
||||
</DefaultBranchMessage>
|
||||
<ProtectionRulesTitle renderAs="p">
|
||||
{createMessage(BRANCH_PROTECTION_RULES_AS_FOLLOWS)}
|
||||
|
|
@ -144,19 +131,7 @@ function ConnectionSuccess() {
|
|||
);
|
||||
};
|
||||
|
||||
const preBranchProtectionActions = () => {
|
||||
return (
|
||||
<Button
|
||||
data-testid="t--start-using-git-button"
|
||||
onClick={handleStartGit}
|
||||
size="md"
|
||||
>
|
||||
{createMessage(START_USING_GIT)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const postBranchProtectionActions = () => {
|
||||
const branchProtectionActions = () => {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
|
|
@ -184,16 +159,10 @@ function ConnectionSuccess() {
|
|||
{createMessage(GIT_CONNECT_SUCCESS_TITLE)}
|
||||
</TitleText>
|
||||
</TitleContainer>
|
||||
{isGitProtectedFeatureEnabled
|
||||
? postBranchProtectionContent()
|
||||
: preBranchProtectionContent()}
|
||||
{branchProtectionContent()}
|
||||
</Container>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{isGitProtectedFeatureEnabled
|
||||
? postBranchProtectionActions()
|
||||
: preBranchProtectionActions()}
|
||||
</ModalFooter>
|
||||
<ModalFooter>{branchProtectionActions()}</ModalFooter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import styled from "styled-components";
|
|||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
|
||||
import { useAppsmithEnterpriseLink } from "./hooks";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
|
||||
const Container = styled.div`
|
||||
padding-top: 16px;
|
||||
|
|
@ -70,6 +71,10 @@ function GitDefaultBranch() {
|
|||
|
||||
const handleUpdate = () => {
|
||||
if (selectedValue) {
|
||||
AnalyticsUtil.logEvent("GS_DEFAULT_BRANCH_UPDATE", {
|
||||
old_branch: currentDefaultBranch,
|
||||
new_branch: selectedValue,
|
||||
});
|
||||
dispatch(updateGitDefaultBranch({ branchName: selectedValue }));
|
||||
}
|
||||
};
|
||||
|
|
@ -103,6 +108,8 @@ function GitDefaultBranch() {
|
|||
<BodyContainer>
|
||||
<StyledSelect
|
||||
data-testid="t--git-default-branch-select"
|
||||
dropdownMatchSelectWidth
|
||||
getPopupContainer={(triggerNode) => triggerNode.parentNode}
|
||||
isDisabled={!isGitProtectedFeatureLicensed}
|
||||
onChange={(v) => setSelectedValue(v)}
|
||||
value={selectedValue}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,12 @@ function GitDisconnect() {
|
|||
</Text>
|
||||
<Text renderAs="p">{createMessage(DISCONNECT_GIT_MESSAGE)}</Text>
|
||||
</BodyInnerContainer>
|
||||
<Button kind="error" onClick={handleDisconnect} size="md">
|
||||
<Button
|
||||
data-testid="t--git-disconnect-btn"
|
||||
kind="error"
|
||||
onClick={handleDisconnect}
|
||||
size="md"
|
||||
>
|
||||
{createMessage(DISCONNECT_GIT)}
|
||||
</Button>
|
||||
</BodyContainer>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {
|
|||
APPSMITH_ENTERPRISE,
|
||||
BRANCH_PROTECTION,
|
||||
BRANCH_PROTECTION_DESC,
|
||||
LEARN_MORE,
|
||||
UPDATE,
|
||||
createMessage,
|
||||
} from "@appsmith/constants/messages";
|
||||
|
|
@ -20,6 +21,9 @@ import styled from "styled-components";
|
|||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
|
||||
import { useAppsmithEnterpriseLink } from "./hooks";
|
||||
import { REMOTE_BRANCH_PREFIX } from "../../constants";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { DOCS_BRANCH_PROTECTION_URL } from "constants/ThirdPartyConstants";
|
||||
|
||||
const Container = styled.div`
|
||||
padding-top: 16px;
|
||||
|
|
@ -56,16 +60,36 @@ function GitProtectedBranches() {
|
|||
const dispatch = useDispatch();
|
||||
|
||||
const unfilteredBranches = useSelector(getGitBranches);
|
||||
const branches = unfilteredBranches.filter(
|
||||
(b) => !b.branchName.includes("origin/"),
|
||||
);
|
||||
const defaultBranch = useSelector(getDefaultGitBranchName);
|
||||
|
||||
const branchNames = useMemo(() => {
|
||||
const returnVal: string[] = [];
|
||||
for (const unfilteredBranch of unfilteredBranches) {
|
||||
if (unfilteredBranch.branchName === defaultBranch) {
|
||||
returnVal.unshift(unfilteredBranch.branchName);
|
||||
} else if (unfilteredBranch.branchName.includes(REMOTE_BRANCH_PREFIX)) {
|
||||
const localBranchName = unfilteredBranch.branchName.replace(
|
||||
REMOTE_BRANCH_PREFIX,
|
||||
"",
|
||||
);
|
||||
if (!returnVal.includes(localBranchName)) {
|
||||
returnVal.push(
|
||||
unfilteredBranch.branchName.replace(REMOTE_BRANCH_PREFIX, ""),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
returnVal.push(unfilteredBranch.branchName);
|
||||
}
|
||||
}
|
||||
return returnVal;
|
||||
}, [unfilteredBranches, defaultBranch]);
|
||||
|
||||
const isGitProtectedFeatureLicensed = useFeatureFlag(
|
||||
FEATURE_FLAG.license_git_branch_protection_enabled,
|
||||
);
|
||||
const defaultBranch = useSelector(getDefaultGitBranchName);
|
||||
const protectedBranches = useSelector(getProtectedBranchesSelector);
|
||||
const isUpdateLoading = useSelector(getIsUpdateProtectedBranchesLoading);
|
||||
const [selectedValues, setSelectedValues] = useState<string[]>();
|
||||
const [selectedValues, setSelectedValues] = useState<string[]>([]);
|
||||
|
||||
const enterprisePricingLink = useAppsmithEnterpriseLink(
|
||||
"git_branch_protection",
|
||||
|
|
@ -82,6 +106,7 @@ function GitProtectedBranches() {
|
|||
const updateIsDisabled = !areProtectedBranchesDifferent;
|
||||
|
||||
const handleUpdate = () => {
|
||||
sendAnalyticsEvent();
|
||||
dispatch(
|
||||
updateGitProtectedBranchesInit({
|
||||
protectedBranches: selectedValues ?? [],
|
||||
|
|
@ -89,6 +114,25 @@ function GitProtectedBranches() {
|
|||
);
|
||||
};
|
||||
|
||||
const sendAnalyticsEvent = () => {
|
||||
const eventData = {
|
||||
branches_added: [] as string[],
|
||||
branches_removed: [] as string[],
|
||||
protected_branches: selectedValues,
|
||||
};
|
||||
for (const val of selectedValues) {
|
||||
if (!protectedBranches.includes(val)) {
|
||||
eventData.branches_added.push(val);
|
||||
}
|
||||
}
|
||||
for (const val of protectedBranches) {
|
||||
if (!selectedValues.includes(val)) {
|
||||
eventData.branches_removed.push(val);
|
||||
}
|
||||
}
|
||||
AnalyticsUtil.logEvent("GS_PROTECTED_BRANCHES_UPDATE", eventData);
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<HeadContainer>
|
||||
|
|
@ -96,7 +140,10 @@ function GitProtectedBranches() {
|
|||
{createMessage(BRANCH_PROTECTION)}
|
||||
</SectionTitle>
|
||||
<SectionDesc kind="body-m" renderAs="p">
|
||||
{createMessage(BRANCH_PROTECTION_DESC)}
|
||||
{createMessage(BRANCH_PROTECTION_DESC)}{" "}
|
||||
<StyledLink target="_blank" to={DOCS_BRANCH_PROTECTION_URL}>
|
||||
{createMessage(LEARN_MORE)}
|
||||
</StyledLink>
|
||||
</SectionDesc>
|
||||
{!isGitProtectedFeatureLicensed && (
|
||||
<SectionDesc kind="body-m" renderAs="p">
|
||||
|
|
@ -114,20 +161,22 @@ function GitProtectedBranches() {
|
|||
<BodyContainer>
|
||||
<StyledSelect
|
||||
data-testid="t--git-protected-branches-select"
|
||||
dropdownMatchSelectWidth
|
||||
getPopupContainer={(triggerNode) => triggerNode.parentNode}
|
||||
isMultiSelect
|
||||
maxTagTextLength={8}
|
||||
onChange={(v) => setSelectedValues(v)}
|
||||
value={selectedValues}
|
||||
>
|
||||
{branches.map((b) => (
|
||||
{branchNames.map((branchName) => (
|
||||
<Option
|
||||
disabled={
|
||||
!isGitProtectedFeatureLicensed && b.branchName !== defaultBranch
|
||||
!isGitProtectedFeatureLicensed && branchName !== defaultBranch
|
||||
}
|
||||
key={b.branchName}
|
||||
value={b.branchName}
|
||||
key={branchName}
|
||||
value={branchName}
|
||||
>
|
||||
{b.branchName}
|
||||
{branchName}
|
||||
</Option>
|
||||
))}
|
||||
</StyledSelect>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
/* eslint-disable jest/no-focused-tests */
|
||||
import React from "react";
|
||||
import { render, screen } from "test/testUtils";
|
||||
import GitSettings from ".";
|
||||
import type { AppState } from "@appsmith/reducers";
|
||||
|
||||
jest.mock("../../hooks/useIsGitAdmin", () => ({
|
||||
useIsGitAdmin: () => false,
|
||||
}));
|
||||
|
||||
describe("GitSettings test for git admin disabled", () => {
|
||||
it("Branch protection, default branch and disconnect disabled when not ", () => {
|
||||
const initialState: Partial<AppState> = {};
|
||||
render(<GitSettings />, { initialState });
|
||||
expect(screen.queryByTestId("t--git-protected-branches-select")).toBe(null);
|
||||
expect(screen.queryByTestId("t--git-default-branch-select")).toBe(null);
|
||||
expect(screen.queryByTestId("t--git-disconnect-btn")).toBe(null);
|
||||
});
|
||||
});
|
||||
|
|
@ -5,8 +5,7 @@ import styled from "styled-components";
|
|||
import { Divider, ModalBody } from "design-system";
|
||||
import GitDefaultBranch from "./GitDefaultBranch";
|
||||
import GitProtectedBranches from "./GitProtectedBranches";
|
||||
import { useSelector } from "react-redux";
|
||||
import { getIsGitProtectedFeatureEnabled } from "selectors/gitSyncSelectors";
|
||||
import { useIsGitAdmin } from "../../hooks/useIsGitAdmin";
|
||||
|
||||
const Container = styled.div`
|
||||
overflow: auto;
|
||||
|
|
@ -20,21 +19,20 @@ const StyledDivider = styled(Divider)`
|
|||
`;
|
||||
|
||||
function GitSettings() {
|
||||
const isGitProtectedFeatureEnabled = useSelector(
|
||||
getIsGitProtectedFeatureEnabled,
|
||||
);
|
||||
const isGitAdmin = useIsGitAdmin();
|
||||
|
||||
return (
|
||||
<ModalBody>
|
||||
<Container>
|
||||
<GitUserSettings />
|
||||
{isGitProtectedFeatureEnabled ? (
|
||||
{isGitAdmin ? (
|
||||
<>
|
||||
<StyledDivider />
|
||||
<GitDefaultBranch />
|
||||
<GitProtectedBranches />
|
||||
</>
|
||||
) : null}
|
||||
<GitDisconnect />
|
||||
{isGitAdmin && <GitDisconnect />}
|
||||
</Container>
|
||||
</ModalBody>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { useDispatch, useSelector } from "react-redux";
|
|||
import {
|
||||
createNewBranchInit,
|
||||
fetchBranchesInit,
|
||||
// setIsGitSyncModalOpen,
|
||||
fetchGitProtectedBranchesInit,
|
||||
switchGitBranchInit,
|
||||
} from "actions/gitSyncActions";
|
||||
import {
|
||||
|
|
@ -15,6 +15,7 @@ import {
|
|||
getFetchingBranches,
|
||||
getGitBranches,
|
||||
getGitBranchNames,
|
||||
getIsGetProtectedBranchesLoading,
|
||||
getProtectedBranchesSelector,
|
||||
} from "selectors/gitSyncSelectors";
|
||||
|
||||
|
|
@ -54,7 +55,6 @@ import { RemoteBranchList } from "./RemoteBranchList";
|
|||
import { LocalBranchList } from "./LocalBranchList";
|
||||
import type { Theme } from "constants/DefaultTheme";
|
||||
import { Space } from "./StyledComponents";
|
||||
// import { GitSyncModalTab } from "entities/GitSync";
|
||||
|
||||
const ListContainer = styled.div`
|
||||
flex: 1;
|
||||
|
|
@ -69,11 +69,6 @@ const BranchDropdownContainer = styled.div`
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// & .title {
|
||||
// ${getTypographyByKey("h3")};
|
||||
// color: var(--ads-v2-color-fg-emphasis-plus);
|
||||
// }
|
||||
|
||||
padding: ${(props) => props.theme.spaces[5]}px;
|
||||
min-height: 0;
|
||||
`;
|
||||
|
|
@ -246,6 +241,7 @@ export default function BranchList(props: {
|
|||
source: "BRANCH_LIST_POPUP_FROM_BOTTOM_BAR",
|
||||
});
|
||||
dispatch(fetchBranchesInit({ pruneBranches: true }));
|
||||
dispatch(fetchGitProtectedBranchesInit());
|
||||
};
|
||||
|
||||
const branches = useSelector(getGitBranches);
|
||||
|
|
@ -254,6 +250,9 @@ export default function BranchList(props: {
|
|||
const fetchingBranches = useSelector(getFetchingBranches);
|
||||
const defaultBranch = useSelector(getDefaultGitBranchName);
|
||||
const protectedBranches = useSelector(getProtectedBranchesSelector);
|
||||
const isGetProtectedBranchesLoading = useSelector(
|
||||
getIsGetProtectedBranchesLoading,
|
||||
);
|
||||
const [searchText, changeSearchTextInState] = useState("");
|
||||
const changeSearchText = (text: string) => {
|
||||
changeSearchTextInState(removeSpecialChars(text));
|
||||
|
|
@ -340,6 +339,9 @@ export default function BranchList(props: {
|
|||
switchBranch,
|
||||
protectedBranches,
|
||||
);
|
||||
|
||||
const loading = fetchingBranches || isGetProtectedBranchesLoading;
|
||||
|
||||
return (
|
||||
<BranchListHotkeys
|
||||
handleDownKey={handleDownKey}
|
||||
|
|
@ -358,12 +360,12 @@ export default function BranchList(props: {
|
|||
/>
|
||||
<Space size={3} />
|
||||
<div style={{ width: 300 }}>
|
||||
{fetchingBranches && (
|
||||
{loading && (
|
||||
<div style={{ width: "100%", height: textInputHeight }}>
|
||||
<Skeleton />
|
||||
</div>
|
||||
)}
|
||||
{!fetchingBranches && (
|
||||
{!loading && (
|
||||
<SearchInput
|
||||
autoFocus
|
||||
className="branch-search t--branch-search-input"
|
||||
|
|
@ -376,8 +378,8 @@ export default function BranchList(props: {
|
|||
</div>
|
||||
<Space size={3} />
|
||||
|
||||
{fetchingBranches && <BranchesLoading />}
|
||||
{!fetchingBranches && (
|
||||
{loading && <BranchesLoading />}
|
||||
{!loading && (
|
||||
<ListContainer>
|
||||
{/* keeping it commented for future use */}
|
||||
{/* <Callout
|
||||
|
|
|
|||
|
|
@ -22,3 +22,5 @@ export enum CREDENTIAL_MODE {
|
|||
MANUALLY = "MANUALLY",
|
||||
IMPORT_JSON = "IMPORT_JSON",
|
||||
}
|
||||
|
||||
export const REMOTE_BRANCH_PREFIX = "origin/";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
import { useSelector } from "react-redux";
|
||||
import { getCurrentAppWorkspace } from "@appsmith/selectors/workspaceSelectors";
|
||||
import { hasCreateNewAppPermission } from "@appsmith/utils/permissionHelpers";
|
||||
|
||||
export const useIsGitAdmin = () => {
|
||||
const workspace = useSelector(getCurrentAppWorkspace);
|
||||
return hasCreateNewAppPermission(workspace.userPermissions);
|
||||
};
|
||||
|
|
@ -10,6 +10,7 @@ import {
|
|||
import {
|
||||
actionChannel,
|
||||
call,
|
||||
delay,
|
||||
fork,
|
||||
put,
|
||||
select,
|
||||
|
|
@ -84,6 +85,7 @@ import {
|
|||
import {
|
||||
createMessage,
|
||||
DELETE_BRANCH_SUCCESS,
|
||||
DISCARD_SUCCESS,
|
||||
ERROR_GIT_AUTH_FAIL,
|
||||
ERROR_GIT_INVALID_REMOTE,
|
||||
GIT_USER_UPDATED_SUCCESSFULLY,
|
||||
|
|
@ -96,7 +98,7 @@ import { addBranchParam, GIT_BRANCH_QUERY_KEY } from "constants/routes";
|
|||
import {
|
||||
getCurrentGitBranch,
|
||||
getDisconnectingGitApplication,
|
||||
getIsGitProtectedFeatureEnabled,
|
||||
getIsGitConnectV2Enabled,
|
||||
getIsGitStatusLiteEnabled,
|
||||
} from "selectors/gitSyncSelectors";
|
||||
import { initEditor } from "actions/initActions";
|
||||
|
|
@ -219,6 +221,9 @@ function* connectToGitSaga(action: ConnectToGitReduxAction) {
|
|||
const applicationId: string = yield select(getCurrentApplicationId);
|
||||
const currentPageId: string = yield select(getCurrentPageId);
|
||||
response = yield GitSyncAPI.connect(action.payload, applicationId);
|
||||
const isGitConnectV2Enabled: boolean = yield select(
|
||||
getIsGitConnectV2Enabled,
|
||||
);
|
||||
|
||||
const isValidResponse: boolean = yield validateResponse(
|
||||
response,
|
||||
|
|
@ -231,10 +236,7 @@ function* connectToGitSaga(action: ConnectToGitReduxAction) {
|
|||
yield put(connectToGitSuccess(response?.data));
|
||||
const defaultBranch = response?.data?.gitApplicationMetadata?.branchName;
|
||||
|
||||
const isGitProtectedFeatureEnabled: boolean = yield select(
|
||||
getIsGitProtectedFeatureEnabled,
|
||||
);
|
||||
if (isGitProtectedFeatureEnabled) {
|
||||
if (isGitConnectV2Enabled) {
|
||||
yield put(
|
||||
updateGitProtectedBranchesInit({
|
||||
protectedBranches: defaultBranch ? [defaultBranch] : [],
|
||||
|
|
@ -1024,11 +1026,13 @@ export function* deleteBranch({ payload }: ReduxAction<any>) {
|
|||
yield put(fetchBranchesInit({ pruneBranches: true }));
|
||||
}
|
||||
} catch (error) {
|
||||
yield put(deleteBranchError(error));
|
||||
yield put(deleteBranchError({ error, show: true }));
|
||||
}
|
||||
}
|
||||
|
||||
function* discardChanges() {
|
||||
function* discardChanges({
|
||||
payload,
|
||||
}: ReduxAction<{ successToastMessage: string } | null | undefined>) {
|
||||
let response: ApiResponse<GitDiscardResponse>;
|
||||
try {
|
||||
const appId: string = yield select(getCurrentApplicationId);
|
||||
|
|
@ -1040,10 +1044,15 @@ function* discardChanges() {
|
|||
);
|
||||
if (isValidResponse) {
|
||||
yield put(discardChangesSuccess(response.data));
|
||||
// const applicationId: string = response.data.id;
|
||||
const successToastMessage =
|
||||
payload?.successToastMessage ?? createMessage(DISCARD_SUCCESS);
|
||||
toast.show(successToastMessage, {
|
||||
kind: "success",
|
||||
});
|
||||
// adding delay to show toast animation before reloading
|
||||
yield delay(500);
|
||||
const pageId: string =
|
||||
response.data?.pages?.find((page: any) => page.isDefault)?.id || "";
|
||||
localStorage.setItem("GIT_DISCARD_CHANGES", "success");
|
||||
const branch = response.data.gitApplicationMetadata.branchName;
|
||||
window.open(builderURL({ pageId, branch }), "_self");
|
||||
} else {
|
||||
|
|
@ -1053,21 +1062,13 @@ function* discardChanges() {
|
|||
show: true,
|
||||
}),
|
||||
);
|
||||
localStorage.setItem("GIT_DISCARD_CHANGES", "failure");
|
||||
}
|
||||
} catch (error) {
|
||||
yield put(discardChangesFailure({ error, show: true }));
|
||||
localStorage.setItem("GIT_DISCARD_CHANGES", "failure");
|
||||
}
|
||||
}
|
||||
|
||||
function* fetchGitProtectedBranchesSaga() {
|
||||
const isGitProtectedFeatureEnabled: boolean = yield select(
|
||||
getIsGitProtectedFeatureEnabled,
|
||||
);
|
||||
if (!isGitProtectedFeatureEnabled) {
|
||||
return;
|
||||
}
|
||||
let response: ApiResponse<string[]>;
|
||||
try {
|
||||
const appId: string = yield select(getCurrentApplicationId);
|
||||
|
|
@ -1104,12 +1105,6 @@ function* fetchGitProtectedBranchesSaga() {
|
|||
function* updateGitProtectedBranchesSaga({
|
||||
payload,
|
||||
}: ReduxAction<{ protectedBranches: string[] }>) {
|
||||
const isGitProtectedFeatureEnabled: boolean = yield select(
|
||||
getIsGitProtectedFeatureEnabled,
|
||||
);
|
||||
if (!isGitProtectedFeatureEnabled) {
|
||||
return;
|
||||
}
|
||||
const { protectedBranches } = payload;
|
||||
const applicationId: string = yield select(getCurrentApplicationId);
|
||||
let response: ApiResponse<string[]>;
|
||||
|
|
|
|||
|
|
@ -240,7 +240,6 @@ export const getIsUpdateProtectedBranchesLoading = (state: AppState) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const getIsGitProtectedFeatureEnabled = createSelector(
|
||||
selectFeatureFlags,
|
||||
(flags) => !!flags?.release_git_branch_protection_enabled,
|
||||
);
|
||||
export const getIsGetProtectedBranchesLoading = (state: AppState) => {
|
||||
return state.ui.gitSync.protectedBranchesLoading;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
package com.appsmith.server.helpers.ce;
|
||||
|
||||
import com.appsmith.server.domains.GitApplicationMetadata;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface GitPrivateRepoHelperCE {
|
||||
|
||||
Mono<Boolean> isRepoLimitReached(String workspaceId, Boolean isClearCache);
|
||||
|
||||
Mono<Boolean> isBranchProtected(GitApplicationMetadata metaData, String branchName);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
package com.appsmith.server.helpers.ce;
|
||||
|
||||
import com.appsmith.server.domains.GitApplicationMetadata;
|
||||
import com.appsmith.server.helpers.GitCloudServicesUtils;
|
||||
import com.appsmith.server.services.ApplicationService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class GitPrivateRepoHelperCEImpl implements GitPrivateRepoHelperCE {
|
||||
|
|
@ -40,4 +43,18 @@ public class GitPrivateRepoHelperCEImpl implements GitPrivateRepoHelperCE {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Boolean> isBranchProtected(GitApplicationMetadata metaData, String branchName) {
|
||||
boolean result = false;
|
||||
if (metaData != null) {
|
||||
String defaultBranch = metaData.getDefaultBranchName();
|
||||
List<String> branchProtectionRules = metaData.getBranchProtectionRules();
|
||||
|
||||
result = branchProtectionRules != null
|
||||
&& branchName.equals(defaultBranch)
|
||||
&& branchProtectionRules.contains(branchName);
|
||||
}
|
||||
return Mono.just(result);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -359,12 +359,6 @@ public class GitServiceCEImpl implements GitServiceCE {
|
|||
return this.commitApplication(commitDTO, defaultApplicationId, branchName, doAmend, true);
|
||||
}
|
||||
|
||||
private boolean isBranchProtected(GitApplicationMetadata metaData, String branchName) {
|
||||
return metaData != null
|
||||
&& metaData.getBranchProtectionRules() != null
|
||||
&& metaData.getBranchProtectionRules().contains(branchName);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will make a commit to local repo and is used internally in flows like create, merge branch
|
||||
* Since the lock is already acquired by the other flows, we do not need to acquire file lock again
|
||||
|
|
@ -439,14 +433,16 @@ public class GitServiceCEImpl implements GitServiceCE {
|
|||
|
||||
boolean isSystemGenerated = isSystemGeneratedTemp;
|
||||
Mono<String> commitMono = this.getApplicationById(defaultApplicationId)
|
||||
.map(application -> {
|
||||
if (isBranchProtected(application.getGitApplicationMetadata(), branchName)) {
|
||||
.zipWhen(application ->
|
||||
gitPrivateRepoHelper.isBranchProtected(application.getGitApplicationMetadata(), branchName))
|
||||
.map(objects -> {
|
||||
if (objects.getT2()) {
|
||||
throw new AppsmithException(
|
||||
AppsmithError.GIT_ACTION_FAILED,
|
||||
"commit",
|
||||
"Cannot commit to protected branch " + branchName);
|
||||
}
|
||||
return application;
|
||||
return objects.getT1();
|
||||
})
|
||||
.flatMap(application -> {
|
||||
GitApplicationMetadata gitData = application.getGitApplicationMetadata();
|
||||
|
|
@ -2727,14 +2723,16 @@ public class GitServiceCEImpl implements GitServiceCE {
|
|||
@Override
|
||||
public Mono<Application> deleteBranch(String defaultApplicationId, String branchName) {
|
||||
Mono<Application> deleteBranchMono = getApplicationById(defaultApplicationId)
|
||||
.map(application -> {
|
||||
if (isBranchProtected(application.getGitApplicationMetadata(), branchName)) {
|
||||
.zipWhen(application ->
|
||||
gitPrivateRepoHelper.isBranchProtected(application.getGitApplicationMetadata(), branchName))
|
||||
.map(objects -> {
|
||||
if (objects.getT2()) {
|
||||
throw new AppsmithException(
|
||||
AppsmithError.GIT_ACTION_FAILED,
|
||||
"delete",
|
||||
"Cannot delete protected branch " + branchName);
|
||||
}
|
||||
return application;
|
||||
return objects.getT1();
|
||||
})
|
||||
.flatMap(application -> addFileLock(defaultApplicationId).map(status -> application))
|
||||
.flatMap(application -> {
|
||||
|
|
@ -3336,11 +3334,20 @@ public class GitServiceCEImpl implements GitServiceCE {
|
|||
|
||||
@Override
|
||||
public Mono<List<String>> getProtectedBranches(String defaultApplicationId) {
|
||||
return getApplicationById(defaultApplicationId)
|
||||
.flatMap(application -> {
|
||||
GitApplicationMetadata gitApplicationMetadata = application.getGitApplicationMetadata();
|
||||
return Mono.justOrEmpty(gitApplicationMetadata.getBranchProtectionRules());
|
||||
})
|
||||
.defaultIfEmpty(List.of());
|
||||
return getApplicationById(defaultApplicationId).map(application -> {
|
||||
GitApplicationMetadata gitApplicationMetadata = application.getGitApplicationMetadata();
|
||||
/*
|
||||
user may have multiple branches as protected, but we only return the default branch
|
||||
as protected branch if it's present in the list of protected branches
|
||||
*/
|
||||
List<String> protectedBranches = gitApplicationMetadata.getBranchProtectionRules();
|
||||
String defaultBranchName = gitApplicationMetadata.getDefaultBranchName();
|
||||
|
||||
if (!CollectionUtils.isNullOrEmpty(protectedBranches) && protectedBranches.contains(defaultBranchName)) {
|
||||
return List.of(defaultBranchName);
|
||||
} else {
|
||||
return List.of();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.appsmith.server.helpers.ce;
|
||||
|
||||
import com.appsmith.server.domains.GitApplicationMetadata;
|
||||
import com.appsmith.server.helpers.GitCloudServicesUtils;
|
||||
import com.appsmith.server.helpers.GitPrivateRepoHelper;
|
||||
import com.appsmith.server.services.ApplicationService;
|
||||
|
|
@ -12,12 +13,18 @@ import org.mockito.Mockito;
|
|||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.boot.test.mock.mockito.SpyBean;
|
||||
import org.springframework.security.test.context.support.WithUserDetails;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
|
|
@ -25,15 +32,16 @@ import static org.mockito.ArgumentMatchers.anyString;
|
|||
@ExtendWith(SpringExtension.class)
|
||||
@SpringBootTest
|
||||
@Slf4j
|
||||
@DirtiesContext
|
||||
public class GitPrivateRepoCEImplTest {
|
||||
|
||||
@Autowired
|
||||
GitPrivateRepoHelper gitPrivateRepoHelper;
|
||||
|
||||
@MockBean
|
||||
@SpyBean
|
||||
GitCloudServicesUtils gitCloudServicesUtils;
|
||||
|
||||
@MockBean
|
||||
@SpyBean
|
||||
ApplicationService applicationService;
|
||||
|
||||
/**
|
||||
|
|
@ -88,4 +96,34 @@ public class GitPrivateRepoCEImplTest {
|
|||
.assertNext(aBoolean -> assertEquals(true, aBoolean))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
boolean isBranchProtected(GitApplicationMetadata metaData, String branchName) {
|
||||
return Boolean.TRUE.equals(
|
||||
gitPrivateRepoHelper.isBranchProtected(metaData, branchName).block());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithUserDetails(value = "api_user")
|
||||
public void isBranchProtected() {
|
||||
GitApplicationMetadata metaData = new GitApplicationMetadata();
|
||||
|
||||
assertFalse(isBranchProtected(null, "master"));
|
||||
assertFalse(isBranchProtected(metaData, "master"));
|
||||
|
||||
metaData.setDefaultBranchName("master2");
|
||||
assertFalse(isBranchProtected(metaData, "master"));
|
||||
|
||||
metaData.setDefaultBranchName("master");
|
||||
assertFalse(isBranchProtected(metaData, "master"));
|
||||
|
||||
metaData.setBranchProtectionRules(List.of("dev"));
|
||||
assertFalse(isBranchProtected(metaData, "master"));
|
||||
|
||||
metaData.setBranchProtectionRules(List.of("dev"));
|
||||
assertFalse(isBranchProtected(metaData, "dev"));
|
||||
|
||||
metaData.setBranchProtectionRules(List.of("dev", "master"));
|
||||
assertFalse(isBranchProtected(metaData, "dev"));
|
||||
assertTrue(isBranchProtected(metaData, "master"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4227,7 +4227,7 @@ public class GitServiceCETest {
|
|||
|
||||
StepVerifier.create(branchListMono)
|
||||
.assertNext(branches -> {
|
||||
assertThat(branches).containsExactlyInAnyOrder("main", "develop");
|
||||
assertThat(branches).isNullOrEmpty();
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
|
@ -4401,4 +4401,49 @@ public class GitServiceCETest {
|
|||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithUserDetails("api_user")
|
||||
public void getProtectedBranches_WhenUserHasMultipleBranchProtected_ReturnsEmptyOrDefaultBranchOnly() {
|
||||
Application testApplication = new Application();
|
||||
testApplication.setName("App" + UUID.randomUUID());
|
||||
testApplication.setWorkspaceId(workspaceId);
|
||||
|
||||
Mono<Application> applicationMono = applicationPageService
|
||||
.createApplication(testApplication)
|
||||
.flatMap(application -> {
|
||||
GitApplicationMetadata gitApplicationMetadata = new GitApplicationMetadata();
|
||||
gitApplicationMetadata.setDefaultApplicationId(application.getId());
|
||||
gitApplicationMetadata.setDefaultBranchName("master");
|
||||
// include default branch in the list of the protected branches
|
||||
gitApplicationMetadata.setBranchProtectionRules(List.of("master", "develop"));
|
||||
application.setGitApplicationMetadata(gitApplicationMetadata);
|
||||
return applicationRepository.save(application);
|
||||
})
|
||||
.cache();
|
||||
|
||||
Mono<List<String>> branchListMonoWithDefaultBranch =
|
||||
applicationMono.flatMap(application -> gitService.getProtectedBranches(application.getId()));
|
||||
|
||||
StepVerifier.create(branchListMonoWithDefaultBranch)
|
||||
.assertNext(branchList -> {
|
||||
assertThat(branchList).containsExactly("master");
|
||||
})
|
||||
.verifyComplete();
|
||||
|
||||
Mono<List<String>> branchListMonoWithoutDefaultBranch = applicationMono
|
||||
.flatMap(application -> {
|
||||
GitApplicationMetadata gitApplicationMetadata = application.getGitApplicationMetadata();
|
||||
// remove the default branch from the protected branches
|
||||
gitApplicationMetadata.setBranchProtectionRules(List.of("develop", "feature"));
|
||||
return applicationRepository.save(application);
|
||||
})
|
||||
.flatMap(application -> gitService.getProtectedBranches(application.getId()));
|
||||
|
||||
StepVerifier.create(branchListMonoWithoutDefaultBranch)
|
||||
.assertNext(branchList -> {
|
||||
assertThat(branchList).isNullOrEmpty();
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user