Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { usePrDetails } from "../../git-interaction/usePrDetails";
import { makeFileKey } from "../../git-interaction/utils/fileKey";
import { usePanelLayoutStore } from "../../panels/panelLayoutStore";
import { useCwd } from "../../sidebar/useCwd";
import { useDiscardAllChanges } from "../../task-detail/hooks/useDiscardAllChanges";
import { useDiscardFile } from "../../task-detail/hooks/useDiscardFile";
import { useStageToggle } from "../../task-detail/hooks/useStageToggle";
import { REVIEW_FILE_CACHE_TIME_MS, REVIEW_MAX_FILE_LINES } from "../constants";
Expand Down Expand Up @@ -267,6 +268,7 @@ function LocalReviewContent({
usePrefetchUntrackedFileContents(repoPath, untrackedFiles, true);

const discardFile = useDiscardFile(repoPath);
const discardAllChanges = useDiscardAllChanges(repoPath);
const stageToggle = useStageToggle(repoPath);
const filesByKey = useMemo(() => {
const map = new Map<string, ChangedFile>();
Expand Down Expand Up @@ -392,6 +394,7 @@ function LocalReviewContent({
onCollapseAll={collapseAll}
onUncollapseFile={uncollapseFile}
onRefresh={refetch}
onDiscardAll={totalFileCount > 0 ? discardAllChanges : undefined}
effectiveSource={effectiveSource}
branchSourceAvailable={branchSourceAvailable}
prSourceAvailable={prSourceAvailable}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export function ReviewShell({
onExpandAll,
onCollapseAll,
onRefresh,
onDiscardAll,
effectiveSource,
branchSourceAvailable,
prSourceAvailable,
Expand Down Expand Up @@ -229,6 +230,7 @@ export function ReviewShell({
onExpandAll={onExpandAll}
onCollapseAll={onCollapseAll}
onRefresh={onRefresh}
onDiscardAll={onDiscardAll}
effectiveSource={effectiveSource}
branchSourceAvailable={branchSourceAvailable}
prSourceAvailable={prSourceAvailable}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { ArrowsClockwise, Columns, Rows, X } from "@phosphor-icons/react";
import {
ArrowCounterClockwise,
ArrowsClockwise,
Columns,
Rows,
X,
} from "@phosphor-icons/react";
import type { ResolvedDiffSource } from "@posthog/core/code-review/resolveDiffSource";
import { Button } from "@posthog/quill";
import { useDiffViewerStore } from "@posthog/ui/features/code-editor/diffViewerStore";
Expand All @@ -22,6 +28,7 @@ interface ReviewToolbarProps {
onExpandAll: () => void;
onCollapseAll: () => void;
onRefresh?: () => void;
onDiscardAll?: () => void;
effectiveSource?: ResolvedDiffSource;
branchSourceAvailable?: boolean;
prSourceAvailable?: boolean;
Expand All @@ -35,6 +42,7 @@ export const ReviewToolbar = memo(function ReviewToolbar({
onExpandAll,
onCollapseAll,
onRefresh,
onDiscardAll,
effectiveSource,
branchSourceAvailable,
prSourceAvailable,
Expand Down Expand Up @@ -91,6 +99,18 @@ export const ReviewToolbar = memo(function ReviewToolbar({
</Tooltip>
)}

{onDiscardAll && (
<Tooltip content="Revert all local changes">
<Button
size="icon-sm"
onClick={onDiscardAll}
className="rounded-xs"
>
<ArrowCounterClockwise size={14} />
</Button>
</Tooltip>
)}

<Tooltip content={viewMode === "split" ? "Split view" : "Columns view"}>
<Button
size="icon-sm"
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/features/code-review/reviewShellParts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export interface ReviewShellProps {
onExpandAll: () => void;
onCollapseAll: () => void;
onRefresh?: () => void;
onDiscardAll?: () => void;
effectiveSource?: ResolvedDiffSource;
branchSourceAvailable?: boolean;
prSourceAvailable?: boolean;
Expand Down
37 changes: 37 additions & 0 deletions packages/ui/src/features/task-detail/hooks/useDiscardAllChanges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useWorkspaceTRPC } from "@posthog/workspace-client/trpc";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useCallback } from "react";
import { showMessageBox } from "../../../utils/dialog";
import { updateGitCacheFromSnapshot } from "../../git-interaction/utils/updateGitCache";

export function useDiscardAllChanges(repoPath: string | undefined) {
const queryClient = useQueryClient();
const trpc = useWorkspaceTRPC();
const discardAllChanges = useMutation(
trpc.git.discardAllChanges.mutationOptions(),
);

return useCallback(async () => {
if (!repoPath) return;

const dialogResult = await showMessageBox({
type: "warning",
title: "Revert all local changes",
message:
"This will discard all uncommitted changes, including new files. A backup will be kept in a git stash.",
buttons: ["Cancel", "Revert All"],
defaultId: 1,
cancelId: 0,
});

if (dialogResult.response !== 1) return;

const result = await discardAllChanges.mutateAsync({
directoryPath: repoPath,
});
Comment on lines +29 to +31

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Failed Revert Has No Feedback

If discardAllChanges.mutateAsync rejects after the user confirms, this async click handler lets the error escape without showing any failure message. The local diff can stay unchanged while the user gets no clear indication that the revert did not happen.

Rule Used: Always wrap asynchronous calls that may fail, such... (source)

Learned From
PostHog/posthog#32098


if (result.state) {
updateGitCacheFromSnapshot(queryClient, repoPath, result.state);
}
}, [repoPath, queryClient, discardAllChanges]);
}
4 changes: 4 additions & 0 deletions packages/workspace-server/src/services/git/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ export const discardFileChangesOutput = z.object({

export type DiscardFileChangesOutput = z.infer<typeof discardFileChangesOutput>;

export const discardAllChangesInput = z.object({
directoryPath: z.string(),
});

export const getGitSyncStatusInput = z.object({
directoryPath: z.string(),
/**
Expand Down
18 changes: 18 additions & 0 deletions packages/workspace-server/src/services/git/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
ResetToDefaultBranchSaga,
SwitchBranchSaga,
} from "@posthog/git/sagas/branch";
import { CleanWorkingTreeSaga } from "@posthog/git/sagas/clean";
import { CloneSaga } from "@posthog/git/sagas/clone";
import { CommitSaga } from "@posthog/git/sagas/commit";
import { DiscardFileChangesSaga } from "@posthog/git/sagas/discard";
Expand Down Expand Up @@ -609,6 +610,23 @@ export class GitService extends TypedEventEmitter<GitCloneEvents> {
return { success: true, state };
}

async discardAllChanges(
directoryPath: string,
): Promise<DiscardFileChangesOutput> {
const saga = new CleanWorkingTreeSaga();
const result = await saga.run({ baseDir: directoryPath });
if (!result.success) {
return { success: false };
}

const state = await this.getStateSnapshot(directoryPath, {
includeSyncStatus: false,
includeLatestCommit: false,
});

return { success: true, state };
}

async push(
directoryPath: string,
remote = "origin",
Expand Down
8 changes: 8 additions & 0 deletions packages/workspace-server/src/trpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import {
diffStatsInput,
diffStatsSchema,
directoryPathInput,
discardAllChangesInput,
discardFileChangesInput,
discardFileChangesOutput,
filePathInput,
Expand Down Expand Up @@ -482,6 +483,13 @@ export function createAppRouter({
),
),

discardAllChanges: t.procedure
.input(discardAllChangesInput)
.output(discardFileChangesOutput)
.mutation(({ input }) =>
gitService().discardAllChanges(input.directoryPath),
Comment on lines +489 to +490

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Client Path Drives Cleanup

The new mutation takes input.directoryPath from the client and sends it straight into the clean-working-tree saga. A caller that can reach this API can point it at any local Git repository the workspace-server process can access, which runs reset/restore/clean there without first checking that the path is a registered workspace owned by the caller.

Rule Used: When implementing new features, ensure that owners... (source)

Learned From
PostHog/posthog#31236

),

push: t.procedure
.input(pushInput)
.output(pushOutput)
Expand Down
Loading