| ... | @@ -1,6 +1,6 @@ | ... | @@ -1,6 +1,6 @@ |
| 1 | import { type GitStatusResult, type GitStackedAction, type NativeApi } from "@t3tools/contracts"; | 1 | import { type GitStatusResult, type GitStackedAction, type NativeApi } from "@t3tools/contracts"; |
| 2 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | 2 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; |
| 3 | import { useCallback, useMemo, useState } from "react"; | 3 | import { useCallback, useEffect, useMemo, useState } from "react"; |
| 4 | import { ChevronDownIcon, CloudUploadIcon, GitCommitIcon, InfoIcon } from "lucide-react"; | 4 | import { ChevronDownIcon, CloudUploadIcon, GitCommitIcon, InfoIcon } from "lucide-react"; |
| 5 | import { GitHubIcon } from "./Icons"; | 5 | import { GitHubIcon } from "./Icons"; |
| 6 | import { | 6 | import { |
| ... | @@ -154,6 +154,16 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp | ... | @@ -154,6 +154,16 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp |
| 154 | const { data: branchList = null } = useQuery(gitBranchesQueryOptions(api, gitCwd)); | 154 | const { data: branchList = null } = useQuery(gitBranchesQueryOptions(api, gitCwd)); |
| 155 | // Default to true while loading so we don't flash init controls. | 155 | // Default to true while loading so we don't flash init controls. |
| 156 | const isRepo = branchList?.isRepo ?? true; | 156 | const isRepo = branchList?.isRepo ?? true; |
| | 157 | const currentBranch = branchList?.branches.find((branch) => branch.current)?.name ?? null; |
| | 158 | const isGitStatusOutOfSync = |
| | 159 | !!gitStatus?.branch && !!currentBranch && gitStatus.branch !== currentBranch; |
| | 160 | |
| | 161 | useEffect(() => { |
| | 162 | if (!isGitStatusOutOfSync) return; |
| | 163 | void invalidateGitQueries(queryClient); |
| | 164 | }, [isGitStatusOutOfSync, queryClient]); |
| | 165 | |
| | 166 | const gitStatusForActions = isGitStatusOutOfSync ? null : gitStatus; |
| 157 | | 167 | |
| 158 | const initMutation = useMutation(gitInitMutationOptions({ api, cwd: gitCwd, queryClient })); | 168 | const initMutation = useMutation(gitInitMutationOptions({ api, cwd: gitCwd, queryClient })); |
| 159 | | 169 | |
| ... | @@ -164,19 +174,19 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp | ... | @@ -164,19 +174,19 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp |
| 164 | | 174 | |
| 165 | const isGitActionRunning = runImmediateGitActionMutation.isPending || pullMutation.isPending; | 175 | const isGitActionRunning = runImmediateGitActionMutation.isPending || pullMutation.isPending; |
| 166 | const isDefaultBranch = useMemo(() => { | 176 | const isDefaultBranch = useMemo(() => { |
| 167 | const currentBranch = gitStatus?.branch; | 177 | const branchName = gitStatusForActions?.branch; |
| 168 | if (!currentBranch) return false; | 178 | if (!branchName) return false; |
| 169 | const current = branchList?.branches.find((branch) => branch.name === currentBranch); | 179 | const current = branchList?.branches.find((branch) => branch.name === branchName); |
| 170 | return current?.isDefault ?? (currentBranch === "main" || currentBranch === "master"); | 180 | return current?.isDefault ?? (branchName === "main" || branchName === "master"); |
| 171 | }, [branchList?.branches, gitStatus?.branch]); | 181 | }, [branchList?.branches, gitStatusForActions?.branch]); |
| 172 | | 182 | |
| 173 | const gitActionMenuItems = useMemo( | 183 | const gitActionMenuItems = useMemo( |
| 174 | () => buildMenuItems(gitStatus, isGitActionRunning), | 184 | () => buildMenuItems(gitStatusForActions, isGitActionRunning), |
| 175 | [gitStatus, isGitActionRunning], | 185 | [gitStatusForActions, isGitActionRunning], |
| 176 | ); | 186 | ); |
| 177 | const quickAction = useMemo( | 187 | const quickAction = useMemo( |
| 178 | () => resolveQuickAction(gitStatus, isGitActionRunning, isDefaultBranch), | 188 | () => resolveQuickAction(gitStatusForActions, isGitActionRunning, isDefaultBranch), |
| 179 | [gitStatus, isDefaultBranch, isGitActionRunning], | 189 | [gitStatusForActions, isDefaultBranch, isGitActionRunning], |
| 180 | ); | 190 | ); |
| 181 | const quickActionDisabledReason = quickAction.disabled | 191 | const quickActionDisabledReason = quickAction.disabled |
| 182 | ? (quickAction.hint ?? "This action is currently unavailable.") | 192 | ? (quickAction.hint ?? "This action is currently unavailable.") |
| ... | @@ -185,14 +195,17 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp | ... | @@ -185,14 +195,17 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp |
| 185 | const maybeConfirmPushToDefaultBranch = useCallback( | 195 | const maybeConfirmPushToDefaultBranch = useCallback( |
| 186 | async (action: GitStackedAction): Promise<boolean> => { | 196 | async (action: GitStackedAction): Promise<boolean> => { |
| 187 | if (!api) return false; | 197 | if (!api) return false; |
| 188 | if (!requiresDefaultBranchConfirmation(action, isDefaultBranch) || !gitStatus?.branch) { | 198 | if ( |
| | 199 | !requiresDefaultBranchConfirmation(action, isDefaultBranch) || |
| | 200 | !gitStatusForActions?.branch |
| | 201 | ) { |
| 189 | return true; | 202 | return true; |
| 190 | } | 203 | } |
| 191 | return api.dialogs.confirm( | 204 | return api.dialogs.confirm( |
| 192 | `You're about to push to the default branch "${gitStatus.branch}". Continue?`, | 205 | `You're about to push to the default branch "${gitStatusForActions.branch}". Continue?`, |
| 193 | ); | 206 | ); |
| 194 | }, | 207 | }, |
| 195 | [api, gitStatus?.branch, isDefaultBranch], | 208 | [api, gitStatusForActions?.branch, isDefaultBranch], |
| 196 | ); | 209 | ); |
| 197 | | 210 | |
| 198 | const openExistingPr = useCallback(async () => { | 211 | const openExistingPr = useCallback(async () => { |
| ... | @@ -204,7 +217,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp | ... | @@ -204,7 +217,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp |
| 204 | }); | 217 | }); |
| 205 | return; | 218 | return; |
| 206 | } | 219 | } |
| 207 | const prUrl = gitStatus?.openPr?.url ?? null; | 220 | const prUrl = gitStatusForActions?.openPr?.url ?? null; |
| 208 | if (!prUrl) { | 221 | if (!prUrl) { |
| 209 | toastManager.add({ | 222 | toastManager.add({ |
| 210 | type: "error", | 223 | type: "error", |
| ... | @@ -224,7 +237,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp | ... | @@ -224,7 +237,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp |
| 224 | }), | 237 | }), |
| 225 | }); | 238 | }); |
| 226 | void promise.catch(() => undefined); | 239 | void promise.catch(() => undefined); |
| 227 | }, [api, gitStatus?.openPr?.url, threadToastData]); | 240 | }, [api, gitStatusForActions?.openPr?.url, threadToastData]); |
| 228 | | 241 | |
| 229 | const runGitActionWithToast = useCallback( | 242 | const runGitActionWithToast = useCallback( |
| 230 | async ({ | 243 | async ({ |
| ... | @@ -242,11 +255,13 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp | ... | @@ -242,11 +255,13 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp |
| 242 | if (!confirmed) return; | 255 | if (!confirmed) return; |
| 243 | onConfirmed?.(); | 256 | onConfirmed?.(); |
| 244 | | 257 | |
| 245 | const pushTarget = gitStatus?.branch ? `origin/${gitStatus.branch}` : undefined; | 258 | const pushTarget = gitStatusForActions?.branch |
| | 259 | ? `origin/${gitStatusForActions.branch}` |
| | 260 | : undefined; |
| 246 | const progressStages = buildGitActionProgressStages({ | 261 | const progressStages = buildGitActionProgressStages({ |
| 247 | action, | 262 | action, |
| 248 | hasCustomCommitMessage: !!commitMessage?.trim(), | 263 | hasCustomCommitMessage: !!commitMessage?.trim(), |
| 249 | hasWorkingTreeChanges: !!gitStatus?.hasWorkingTreeChanges, | 264 | hasWorkingTreeChanges: !!gitStatusForActions?.hasWorkingTreeChanges, |
| 250 | forcePushOnly: forcePushOnlyProgress, | 265 | forcePushOnly: forcePushOnlyProgress, |
| 251 | ...(pushTarget ? { pushTarget } : {}), | 266 | ...(pushTarget ? { pushTarget } : {}), |
| 252 | }); | 267 | }); |
| ... | @@ -282,7 +297,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp | ... | @@ -282,7 +297,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp |
| 282 | stopProgressUpdates(); | 297 | stopProgressUpdates(); |
| 283 | const resultToast = summarizeGitResult(result); | 298 | const resultToast = summarizeGitResult(result); |
| 284 | | 299 | |
| 285 | const prUrl = result.pr.url ?? gitStatus?.openPr?.url; | 300 | const prUrl = result.pr.url ?? gitStatusForActions?.openPr?.url; |
| 286 | const shouldOfferPushCta = action === "commit" && result.commit.status === "created"; | 301 | const shouldOfferPushCta = action === "commit" && result.commit.status === "created"; |
| 287 | const shouldOfferOpenPrCta = | 302 | const shouldOfferOpenPrCta = |
| 288 | (action === "commit_push" || action === "commit_push_pr") && !!prUrl && !isDefaultBranch; | 303 | (action === "commit_push" || action === "commit_push_pr") && !!prUrl && !isDefaultBranch; |
| ... | @@ -345,9 +360,9 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp | ... | @@ -345,9 +360,9 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp |
| 345 | }, | 360 | }, |
| 346 | [ | 361 | [ |
| 347 | api, | 362 | api, |
| 348 | gitStatus?.branch, | 363 | gitStatusForActions?.branch, |
| 349 | gitStatus?.hasWorkingTreeChanges, | 364 | gitStatusForActions?.hasWorkingTreeChanges, |
| 350 | gitStatus?.openPr?.url, | 365 | gitStatusForActions?.openPr?.url, |
| 351 | isDefaultBranch, | 366 | isDefaultBranch, |
| 352 | maybeConfirmPushToDefaultBranch, | 367 | maybeConfirmPushToDefaultBranch, |
| 353 | runImmediateGitActionMutation, | 368 | runImmediateGitActionMutation, |
| ... | @@ -517,7 +532,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp | ... | @@ -517,7 +532,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp |
| 517 | {gitActionMenuItems.map((item) => { | 532 | {gitActionMenuItems.map((item) => { |
| 518 | const disabledReason = getMenuActionDisabledReason( | 533 | const disabledReason = getMenuActionDisabledReason( |
| 519 | item, | 534 | item, |
| 520 | gitStatus, | 535 | gitStatusForActions, |
| 521 | isGitActionRunning, | 536 | isGitActionRunning, |
| 522 | ); | 537 | ); |
| 523 | if (item.disabled && disabledReason) { | 538 | if (item.disabled && disabledReason) { |
| ... | @@ -553,19 +568,25 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp | ... | @@ -553,19 +568,25 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp |
| 553 | </MenuItem> | 568 | </MenuItem> |
| 554 | ); | 569 | ); |
| 555 | })} | 570 | })} |
| 556 | {gitStatus?.branch === null && ( | 571 | {gitStatusForActions?.branch === null && ( |
| 557 | <p className="px-2 py-1.5 text-xs text-warning"> | 572 | <p className="px-2 py-1.5 text-xs text-warning"> |
| 558 | Detached HEAD: create and checkout a branch to enable push and PR actions. | 573 | Detached HEAD: create and checkout a branch to enable push and PR actions. |
| 559 | </p> | 574 | </p> |
| 560 | )} | 575 | )} |
| 561 | {gitStatus && | 576 | {gitStatusForActions && |
| 562 | gitStatus.branch !== null && | 577 | gitStatusForActions.branch !== null && |
| 563 | !gitStatus.hasWorkingTreeChanges && | 578 | !gitStatusForActions.hasWorkingTreeChanges && |
| 564 | gitStatus.behindCount > 0 && ( | 579 | gitStatusForActions.behindCount > 0 && |
| | 580 | gitStatusForActions.aheadCount === 0 && ( |
| 565 | <p className="px-2 py-1.5 text-xs text-warning"> | 581 | <p className="px-2 py-1.5 text-xs text-warning"> |
| 566 | Behind upstream. Pull/rebase first. | 582 | Behind upstream. Pull/rebase first. |
| 567 | </p> | 583 | </p> |
| 568 | )} | 584 | )} |
| | 585 | {isGitStatusOutOfSync && ( |
| | 586 | <p className="px-2 py-1.5 text-xs text-muted-foreground"> |
| | 587 | Refreshing git status... |
| | 588 | </p> |
| | 589 | )} |
| 569 | {gitStatusError && ( | 590 | {gitStatusError && ( |
| 570 | <p className="px-2 py-1.5 text-xs text-destructive">{gitStatusError.message}</p> | 591 | <p className="px-2 py-1.5 text-xs text-destructive">{gitStatusError.message}</p> |
| 571 | )} | 592 | )} |
| ... | @@ -594,37 +615,42 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp | ... | @@ -594,37 +615,42 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp |
| 594 | <div className="space-y-3 rounded-lg border border-input bg-muted/40 p-3 text-xs"> | 615 | <div className="space-y-3 rounded-lg border border-input bg-muted/40 p-3 text-xs"> |
| 595 | <div className="grid grid-cols-[auto_1fr] items-center gap-x-2 gap-y-1"> | 616 | <div className="grid grid-cols-[auto_1fr] items-center gap-x-2 gap-y-1"> |
| 596 | <span className="text-muted-foreground">Branch</span> | 617 | <span className="text-muted-foreground">Branch</span> |
| 597 | <span className="font-medium">{gitStatus?.branch ?? "(detached HEAD)"}</span> | 618 | <span className="font-medium"> |
| | 619 | {gitStatusForActions?.branch ?? "(detached HEAD)"} |
| | 620 | </span> |
| 598 | {activeDialogAction !== "commit" && ( | 621 | {activeDialogAction !== "commit" && ( |
| 599 | <> | 622 | <> |
| 600 | <span className="text-muted-foreground">Upstream</span> | 623 | <span className="text-muted-foreground">Upstream</span> |
| 601 | <span className="font-medium"> | 624 | <span className="font-medium"> |
| 602 | {!gitStatus || !gitStatus.hasUpstream | 625 | {!gitStatusForActions || !gitStatusForActions.hasUpstream |
| 603 | ? "No upstream configured" | 626 | ? "No upstream configured" |
| 604 | : gitStatus.aheadCount === 0 && gitStatus.behindCount === 0 | 627 | : gitStatusForActions.aheadCount === 0 && |
| | 628 | gitStatusForActions.behindCount === 0 |
| 605 | ? "Up to date" | 629 | ? "Up to date" |
| 606 | : gitStatus.aheadCount > 0 && gitStatus.behindCount > 0 | 630 | : gitStatusForActions.aheadCount > 0 && gitStatusForActions.behindCount > 0 |
| 607 | ? `Diverged (+${gitStatus.aheadCount} / -${gitStatus.behindCount})` | 631 | ? `Diverged (+${gitStatusForActions.aheadCount} / -${gitStatusForActions.behindCount})` |
| 608 | : gitStatus.aheadCount > 0 | 632 | : gitStatusForActions.aheadCount > 0 |
| 609 | ? `Ahead by ${gitStatus.aheadCount}` | 633 | ? `Ahead by ${gitStatusForActions.aheadCount}` |
| 610 | : `Behind by ${gitStatus.behindCount}`} | 634 | : `Behind by ${gitStatusForActions.behindCount}`} |
| 611 | </span> | 635 | </span> |
| 612 | <span className="text-muted-foreground">Working tree</span> | 636 | <span className="text-muted-foreground">Working tree</span> |
| 613 | <span className="font-medium"> | 637 | <span className="font-medium"> |
| 614 | {!gitStatus || !gitStatus.hasWorkingTreeChanges | 638 | {!gitStatusForActions || !gitStatusForActions.hasWorkingTreeChanges |
| 615 | ? "Clean" | 639 | ? "Clean" |
| 616 | : `${gitStatus.workingTree.files.length} file(s)`} | 640 | : `${gitStatusForActions.workingTree.files.length} file(s)`} |
| 617 | </span> | 641 | </span> |
| 618 | <span className="text-muted-foreground">Diff</span> | 642 | <span className="text-muted-foreground">Diff</span> |
| 619 | <span className="font-mono"> | 643 | <span className="font-mono"> |
| 620 | {!gitStatus || !gitStatus.hasWorkingTreeChanges ? ( | 644 | {!gitStatusForActions || !gitStatusForActions.hasWorkingTreeChanges ? ( |
| 621 | "none" | 645 | "none" |
| 622 | ) : ( | 646 | ) : ( |
| 623 | <> | 647 | <> |
| 624 | <span className="text-success">+{gitStatus.workingTree.insertions}</span> | 648 | <span className="text-success"> |
| | 649 | +{gitStatusForActions.workingTree.insertions} |
| | 650 | </span> |
| 625 | <span className="text-muted-foreground"> / </span> | 651 | <span className="text-muted-foreground"> / </span> |
| 626 | <span className="text-destructive"> | 652 | <span className="text-destructive"> |
| 627 | -{gitStatus.workingTree.deletions} | 653 | -{gitStatusForActions.workingTree.deletions} |
| 628 | </span> | 654 | </span> |
| 629 | </> | 655 | </> |
| 630 | )} | 656 | )} |
| ... | @@ -634,13 +660,13 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp | ... | @@ -634,13 +660,13 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp |
| 634 | </div> | 660 | </div> |
| 635 | <div className="space-y-1"> | 661 | <div className="space-y-1"> |
| 636 | <p className="text-muted-foreground">Files</p> | 662 | <p className="text-muted-foreground">Files</p> |
| 637 | {!gitStatus || gitStatus.workingTree.files.length === 0 ? ( | 663 | {!gitStatusForActions || gitStatusForActions.workingTree.files.length === 0 ? ( |
| 638 | <p className="font-medium">none</p> | 664 | <p className="font-medium">none</p> |
| 639 | ) : ( | 665 | ) : ( |
| 640 | <div className="space-y-2"> | 666 | <div className="space-y-2"> |
| 641 | <ScrollArea className="h-44 rounded-md border border-input bg-background"> | 667 | <ScrollArea className="h-44 rounded-md border border-input bg-background"> |
| 642 | <div className="space-y-1 p-1"> | 668 | <div className="space-y-1 p-1"> |
| 643 | {gitStatus.workingTree.files.map((file) => ( | 669 | {gitStatusForActions.workingTree.files.map((file) => ( |
| 644 | <button | 670 | <button |
| 645 | type="button" | 671 | type="button" |
| 646 | key={file.path} | 672 | key={file.path} |
| ... | @@ -658,9 +684,13 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp | ... | @@ -658,9 +684,13 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp |
| 658 | </div> | 684 | </div> |
| 659 | </ScrollArea> | 685 | </ScrollArea> |
| 660 | <div className="flex justify-end font-mono"> | 686 | <div className="flex justify-end font-mono"> |
| 661 | <span className="text-success">+{gitStatus.workingTree.insertions}</span> | 687 | <span className="text-success"> |
| | 688 | +{gitStatusForActions.workingTree.insertions} |
| | 689 | </span> |
| 662 | <span className="text-muted-foreground"> / </span> | 690 | <span className="text-muted-foreground"> / </span> |
| 663 | <span className="text-destructive">-{gitStatus.workingTree.deletions}</span> | 691 | <span className="text-destructive"> |
| | 692 | -{gitStatusForActions.workingTree.deletions} |
| | 693 | </span> |
| 664 | </div> | 694 | </div> |
| 665 | </div> | 695 | </div> |
| 666 | )} | 696 | )} |