feat: display uncommit&unpush change - [INS-4138] (#7816)

* feat: display uncommit

* modify workspaceMeta model

* git sync uncommit

* uncommit&unpush ui

* add dot for workspace card

* UI: replace icon color

* move workspace update operation to loader&action

* del log

Co-authored-by: James Gatz <jamesgatzos@gmail.com>

---------

Co-authored-by: James Gatz <jamesgatzos@gmail.com>
This commit is contained in:
Curry Yang 2024-08-12 17:00:24 +08:00 committed by GitHub
parent df5729d6ae
commit 76976db4dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 108 additions and 22 deletions

View File

@ -19,6 +19,8 @@ export interface BaseWorkspaceMeta {
gitRepositoryId: string | null;
parentId: string | null;
pushSnapshotOnInitialize: boolean;
hasUncommittedChanges: boolean;
hasUnpushedChanges: boolean;
}
export type WorkspaceMeta = BaseWorkspaceMeta & BaseModel;
@ -40,6 +42,8 @@ export function init(): BaseWorkspaceMeta {
gitRepositoryId: null,
parentId: null,
pushSnapshotOnInitialize: false,
hasUncommittedChanges: false,
hasUnpushedChanges: false,
};
}

View File

@ -107,3 +107,8 @@ export interface Status {
stage: Stage;
unstaged: Record<DocumentKey, StageEntry>;
}
export interface Compare {
ahead: number;
behind: number;
}

View File

@ -7,6 +7,7 @@ import { deleteKeys, resetKeys, shouldIgnoreKey } from '../ignore-keys';
import { deterministicStringify } from '../lib/deterministicStringify';
import type {
Branch,
Compare,
DocumentKey,
MergeConflict,
Snapshot,
@ -241,10 +242,7 @@ export function threeWayMerge(
export function compareBranches(
a: Branch | null,
b: Branch | null,
): {
ahead: number;
behind: number;
} {
): Compare {
const snapshotsA = a ? a.snapshots : [];
const snapshotsB = b ? b.snapshots : [];
const latestA = snapshotsA[snapshotsA.length - 1] || null;

View File

@ -9,6 +9,8 @@ import type { GitRepository } from '../../../models/git-repository';
import { deleteGitRepository } from '../../../models/helpers/git-repository-operations';
import { getOauth2FormatName } from '../../../sync/git/utils';
import type {
GitCanPushLoaderData,
GitChangesLoaderData,
GitFetchLoaderData,
GitRepoLoaderData,
GitStatusResult,
@ -56,6 +58,8 @@ export const GitSyncDropdown: FC<Props> = ({ gitRepository, isInsomniaSyncEnable
const gitRepoDataFetcher = useFetcher<GitRepoLoaderData>();
const gitFetchFetcher = useFetcher<GitFetchLoaderData>();
const gitStatusFetcher = useFetcher<GitStatusResult>();
const gitChangesFetcher = useFetcher<GitChangesLoaderData>();
const gitCanPushFetcher = useFetcher<GitCanPushLoaderData>();
const loadingPush = gitPushFetcher.state === 'loading';
const loadingPull = gitPullFetcher.state === 'loading';
@ -81,6 +85,18 @@ export const GitSyncDropdown: FC<Props> = ({ gitRepository, isInsomniaSyncEnable
workspaceId,
]);
useEffect(() => {
if (gitRepository?.uri && gitRepository?._id && gitChangesFetcher.state === 'idle' && !gitChangesFetcher.data && gitRepoDataFetcher.data) {
gitChangesFetcher.load(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/changes`);
}
}, [gitChangesFetcher, gitRepoDataFetcher.data, gitRepository?._id, gitRepository?.uri, organizationId, projectId, workspaceId]);
useEffect(() => {
if (gitRepository?.uri && gitRepository?._id && gitCanPushFetcher.state === 'idle' && !gitCanPushFetcher.data && gitRepoDataFetcher.data) {
gitCanPushFetcher.load(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/can-push`);
}
}, [gitCanPushFetcher, gitRepoDataFetcher.data, gitRepository?._id, gitRepository?.uri, organizationId, projectId, workspaceId]);
// Only fetch the repo status if we have a repo uri and we don't have the status already
const shouldFetchGitRepoStatus = Boolean(gitRepository?.uri && gitRepository?._id && gitStatusFetcher.state === 'idle' && !gitStatusFetcher.data && gitRepoDataFetcher.data);

View File

@ -853,6 +853,11 @@ async function renderApp() {
loader: async (...args) =>
(await import('./routes/git-actions')).gitChangesLoader(...args),
},
{
path: 'can-push',
loader: async (...args) =>
(await import('./routes/git-actions')).canPushLoader(...args),
},
{
path: 'log',
loader: async (...args) =>

View File

@ -302,7 +302,10 @@ export const gitChangesLoader: LoaderFunction = async ({
const branch = await GitVCS.getCurrentBranch();
try {
const { changes, statusNames } = await getGitChanges(GitVCS, workspace);
// update workspace meta with git sync data, use for show uncommit changes on collection card
models.workspaceMeta.updateByParentId(workspaceId, {
hasUncommittedChanges: changes.length > 0,
});
return {
branch,
changes,
@ -318,6 +321,35 @@ export const gitChangesLoader: LoaderFunction = async ({
}
};
export interface GitCanPushLoaderData {
canPush: boolean;
}
export const canPushLoader: LoaderFunction = async ({ params }): Promise<GitCanPushLoaderData> => {
const { workspaceId } = params;
invariant(workspaceId, 'Workspace ID is required');
const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId);
const repoId = workspaceMeta?.gitRepositoryId;
invariant(repoId, 'Workspace is not linked to a git repository');
const gitRepository = await models.gitRepository.getById(repoId);
invariant(gitRepository, 'Git Repository not found');
let canPush = false;
try {
canPush = await GitVCS.canPush(gitRepository.credentials);
// update workspace meta with git sync data, use for show unpushed changes on collection card
models.workspaceMeta.update(workspaceMeta, {
hasUnpushedChanges: canPush,
});
} catch (err) { }
return { canPush };
};
// Actions
type CloneGitActionResult =
| Response

View File

@ -263,6 +263,8 @@ export interface InsomniaFile {
mockServer?: MockServer;
workspace?: Workspace;
apiSpec?: ApiSpec;
hasUncommittedChanges?: boolean;
hasUnpushedChanges?: boolean;
}
export interface ProjectIdLoaderData {
@ -371,9 +373,10 @@ async function getAllLocalFiles({
mockServer,
apiSpec,
workspace,
hasUncommittedChanges: workspaceMeta?.hasUncommittedChanges,
hasUnpushedChanges: workspaceMeta?.hasUnpushedChanges,
};
});
return files;
}
@ -1008,6 +1011,10 @@ const ProjectRoute: FC = () => {
}
}, [projectId]);
const showUnCommitOrUnpushIndicator = useMemo(() => {
return filesWithPresence.some(file => file?.hasUncommittedChanges || file?.hasUnpushedChanges);
}, [filesWithPresence]);
return (
<ErrorBoundary>
<Fragment>
@ -1123,6 +1130,7 @@ const ProjectRoute: FC = () => {
icon={
isRemoteProject(item) ? 'globe-americas' : 'laptop'
}
className={(showUnCommitOrUnpushIndicator && item._id === activeProject?._id) ? 'text-[--color-warning]' : ''}
/>
<span className="truncate">{item.name}</span>
<span className="flex-1" />
@ -1453,6 +1461,14 @@ const ProjectRoute: FC = () => {
</span>
</div>
)}
{(item.hasUncommittedChanges || item.hasUnpushedChanges) && (
<div className="text-sm text-[--color-warning] flex items-center gap-2">
<div className='rounded-full bg-[--color-warning] w-3 h-3 flex-shrink-0' />
<span>
{item.hasUncommittedChanges ? 'Uncommitted changes' : 'Unpushed changes'}
</span>
</div>
)}
</div>
</GridListItem>
);

View File

@ -19,6 +19,7 @@ import type { WebSocketRequest } from '../../models/websocket-request';
import { scopeToActivity, type Workspace } from '../../models/workspace';
import type {
BackendProject,
Compare,
Snapshot,
Status,
StatusCandidate,
@ -250,14 +251,11 @@ interface SyncData {
historyCount: number;
status: Status;
syncItems: StatusCandidate[];
compare: {
ahead: number;
behind: number;
};
compare: Compare;
}
const remoteBranchesCache: Record<string, string[]> = {};
const remoteCompareCache: Record<string, { ahead: number; behind: number }> =
const remoteCompareCache: Record<string, Compare> =
{};
const remoteBackendProjectsCache: Record<string, BackendProject[]> = {};
@ -332,17 +330,29 @@ export const syncDataLoader: LoaderFunction = async ({
remoteBranches = (
remoteBranchesCache[workspaceId] || (await vcs.getRemoteBranchNames())
).sort();
compare =
remoteCompareCache[workspaceId] || (await vcs.compareRemoteBranch());
const remoteBackendProjects =
remoteBackendProjectsCache[workspaceId] ||
(await vcs.remoteBackendProjects({
teamId: project.parentId,
teamProjectId: project.remoteId,
}));
remoteBranchesCache[workspaceId] = remoteBranches;
remoteCompareCache[workspaceId] = compare;
remoteBackendProjectsCache[workspaceId] = remoteBackendProjects;
compare = remoteCompareCache[workspaceId] || (await vcs.compareRemoteBranch());
const remoteBackendProjects =
remoteBackendProjectsCache[workspaceId] ||
(await vcs.remoteBackendProjects({
teamId: project.parentId,
teamProjectId: project.remoteId,
}));
remoteBranchesCache[workspaceId] = remoteBranches;
remoteCompareCache[workspaceId] = compare;
remoteBackendProjectsCache[workspaceId] = remoteBackendProjects;
let hasUncommittedChanges = false;
if (status?.unstaged && Object.keys(status.unstaged).length > 0) {
hasUncommittedChanges = true;
}
if (status?.stage && Object.keys(status.stage).length > 0) {
hasUncommittedChanges = true;
}
// update workspace meta with sync data, use for show unpushed changes on collection card
models.workspaceMeta.updateByParentId(workspaceId, {
hasUncommittedChanges,
hasUnpushedChanges: compare?.ahead > 0,
});
} catch (e) { }
return {