Skip to content

Commit

Permalink
v1.3.0 (#648)
Browse files Browse the repository at this point in the history
* Live staging/unstaging updates on Git Graph (#640)

* Fix for fetching local branches on linked worktrees returning root of main worktree

* git-porcelain.hasStatus() added for checking for specific git status in files and directories

* selectStagedFieldsByRepo selector added for obtaining partial Metafile fields related to VCS status

* Git Graph automatically updates for staging/unstaging of files tracked under selected repository

* Bump webpack from 5.67.0 to 5.68.0 (#629)

Bumps [webpack](https://github.com/webpack/webpack) from 5.67.0 to 5.68.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](webpack/webpack@v5.67.0...v5.68.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Display Synectic version number in notification from Help menu (#642)

* Bump css-loader from 6.5.1 to 6.6.0 (#630)

Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 6.5.1 to 6.6.0.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](webpack-contrib/css-loader@v6.5.1...v6.6.0)

---
updated-dependencies:
- dependency-name: css-loader
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Dependency updates

* Cloning updated to convert SSH URLs to HTTPS and properly resolve success/failure returns

* Fix for resolving git refs in non-local off-main branches

* Resolve linked worktree HEAD refs by following indicated ref path to refs/heads/{branch}

* BranchStatus component checks for empty current repository root before checking out new branches

* Repos Tracker renamed to Branch Tracker

* Card selectors use RTK createSelector instead of RTK createDraftSafeSelector

* Branch selectors use RTK createSelector instead of RTK createDraftSafeSelector

* Branch Tracker properly displays card counts related to each tracked branch in a repository

* branchSelectors.selectByRepo prefers local branches over remote branches

* Dependency updates

* v1.3.0

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
  • Loading branch information
nelsonni and dependabot[bot] authored Feb 16, 2022
1 parent 479806d commit 4ecd036
Show file tree
Hide file tree
Showing 15 changed files with 786 additions and 730 deletions.
32 changes: 16 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "synectic",
"productName": "Synectic",
"version": "1.2.0",
"version": "1.3.0",
"description": "My Electron application description",
"main": ".webpack/main",
"scripts": {
Expand Down Expand Up @@ -41,8 +41,8 @@
"@electron-forge/maker-squirrel": "^6.0.0-beta.63",
"@electron-forge/plugin-webpack": "6.0.0-beta.63",
"@testing-library/dom": "^8.11.3",
"@testing-library/jest-dom": "^5.16.1",
"@testing-library/react": "^12.1.2",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.3",
"@testing-library/react-hooks": "^7.0.2",
"@testing-library/user-event": "^13.5.0",
"@types/dagre": "^0.7.47",
Expand All @@ -52,31 +52,31 @@
"@types/ini": "^1.3.31",
"@types/jest": "^27.4.0",
"@types/luxon": "^2.0.9",
"@types/node": "^17.0.13",
"@types/node": "^17.0.18",
"@types/pako": "^1.0.3",
"@types/parse-git-config": "^3.0.1",
"@types/parse-path": "^4.0.1",
"@types/react": "^17.0.38",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
"@types/react-transition-group": "^4.4.4",
"@types/redux-mock-store": "^1.0.3",
"@types/sha1": "^1.1.3",
"@types/uuid": "^8.3.4",
"@types/valid-url": "^1.0.3",
"@types/validator": "^13.7.1",
"@typescript-eslint/eslint-plugin": "^5.10.1",
"@typescript-eslint/parser": "^5.10.1",
"@typescript-eslint/eslint-plugin": "^5.12.0",
"@typescript-eslint/parser": "^5.12.0",
"@vercel/webpack-asset-relocator-loader": "1.7.0",
"casual": "^1.6.2",
"css-loader": "^6.5.1",
"electron": "16.0.8",
"eslint": "^8.8.0",
"css-loader": "^6.6.0",
"electron": "17.0.0",
"eslint": "^8.9.0",
"eslint-plugin-import": "^2.25.4",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^7.0.0",
"fork-ts-checker-webpack-plugin": "^7.2.1",
"git-remote-protocol": "^0.1.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^27.4.7",
"jest": "^27.5.1",
"jest-serializer-path": "^0.1.15",
"node-loader": "^2.0.0",
"pako": "^2.0.4",
Expand All @@ -89,13 +89,13 @@
"typescript": "^4.5.5",
"valid-url": "^1.0.9",
"validator": "^13.7.0",
"webpack": "^5.67.0"
"webpack": "^5.69.0"
},
"dependencies": {
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
"@reduxjs/toolkit": "^1.7.1",
"@reduxjs/toolkit": "^1.7.2",
"binarnia": "^3.1.3",
"chokidar": "^3.5.3",
"dagre": "^0.8.5",
Expand All @@ -105,7 +105,7 @@
"git-config-path": "^2.0.0",
"ignore": "^5.2.0",
"ini": "^2.0.0",
"isomorphic-git": "^1.11.1",
"isomorphic-git": "^1.11.2",
"luxon": "^2.3.0",
"parse-git-config": "^3.0.0",
"parse-path": "^4.0.3",
Expand All @@ -115,7 +115,7 @@
"react-dnd-html5-backend": "^14.1.0",
"react-dnd-preview": "^6.0.2",
"react-dom": "^17.0.2",
"react-flow-renderer": "^9.7.3",
"react-flow-renderer": "^9.7.4",
"react-redux": "^7.2.6",
"react-transition-group": "^4.4.2",
"redux": "^4.1.2",
Expand Down
6 changes: 6 additions & 0 deletions src/components/CanvasComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ const CanvasComponent: React.FunctionComponent = props => {
{ label: 'Repository...', click: async () => { shell.openExternal('https://github.com/EPICLab/synectic/'); } },
{ label: 'Release Notes...', click: async () => { shell.openExternal('https://github.com/EPICLab/synectic/releases'); } },
{ label: 'View License...', click: async () => { shell.openExternal('https://github.com/EPICLab/synectic/blob/5ec51f6dc9dc857cae58c5253c3334c8f33a63c4/LICENSE'); } },
{
label: 'Version', click: () => dispatch(modalAdded({
id: v4(), type: 'Notification',
options: { 'message': `Synectic v${process.env.npm_package_version}` }
}))
}
];

return (
Expand Down
12 changes: 6 additions & 6 deletions src/components/SourceControl/CloneDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const useStyles = makeStyles((theme: Theme) =>
const CloneDialog: React.FunctionComponent<Modal> = props => {
const classes = useStyles();
const [url, setUrl] = useState('');
const [invalid, setInvalid] = useState(false);
const [invalid, setInvalid] = useState(true);
const [targetPath, setTargetPath] = useState('');
const [status, setStatus] = useState<Status>('Unchecked');
const [log, setLog] = useState('');
Expand All @@ -74,11 +74,12 @@ const CloneDialog: React.FunctionComponent<Modal> = props => {
const initiateCloning = async () => {
try {
setStatus('Running');
await dispatch(cloneRepository({
url: url,
const repo = await dispatch(cloneRepository({
url: new URL(url),
root: targetPath,
onProgress: (progress) => setLog(`cloning objects: ${progress.loaded}/${progress.total}`)
}));
})).unwrap();
if (!repo) throw new Error('Cloning failed');
setStatus('Passing');
await dispatch(loadBranchVersions());
await delay(2000);
Expand All @@ -87,7 +88,6 @@ const CloneDialog: React.FunctionComponent<Modal> = props => {
setStatus('Failing');
}
}

if (targetPath !== '' && !invalid) initiateCloning();
}, [targetPath]);

Expand Down Expand Up @@ -124,7 +124,7 @@ const CloneDialog: React.FunctionComponent<Modal> = props => {
<Typography color='textSecondary' variant='body2'>
{status === 'Running' ? log : null}
{status === 'Passing' ? `Clone completed from '${url}' to '${targetPath}'` : null}
{status === 'Failing' ? `Existing repository at ${targetPath}` : null}
{status === 'Failing' ? `Clone failed from '${url}' to '${targetPath}'` : null}
</Typography>
</div>
</div>
Expand Down
44 changes: 20 additions & 24 deletions src/components/SourceControl/ReposOverview.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,48 @@
import React from 'react';
import TreeView from '@material-ui/lab/TreeView';
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
import ArrowRightIcon from '@material-ui/icons/ArrowRight';
import ErrorIcon from '@material-ui/icons/Error';
import { ArrowDropDown, ArrowRight, Error } from '@material-ui/icons';
import { v4 } from 'uuid';
import type { Repository, Branch } from '../../types';
import { RootState } from '../../store/store';
import { isDefined } from '../../containers/format';
import { StyledTreeItem } from '../StyledTreeComponent';
import { GitRepoIcon, GitBranchIcon } from '../GitIcons';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import repoSelectors from '../../store/selectors/repos';
import cardSelectors from '../../store/selectors/cards';
import metafileSelectors from '../../store/selectors/metafiles';
import { loadCard } from '../../store/thunks/handlers';
import { checkoutBranch } from '../../store/thunks/repos';
import { fetchMetafilesByFilepath } from '../../store/thunks/metafiles';
import { fetchMetafile, fetchVersionControl, isFilebasedMetafile } from '../../store/thunks/metafiles';
import branchSelectors from '../../store/selectors/branches';

const modifiedStatuses = ['modified', '*modified', 'deleted', '*deleted', 'added', '*added', '*absent', '*undeleted', '*undeletedmodified'];
import { readDirAsync } from '../../containers/io';
import { currentBranch } from '../../containers/git-porcelain';
import { metafileUpdated } from '../../store/slices/metafiles';

const BranchStatus: React.FunctionComponent<{ repo: Repository, branch: Branch }> = props => {
const cards = useAppSelector((state: RootState) => cardSelectors.selectByRepo(state, props.repo.id, props.branch.id));
const metafiles = useAppSelector((state: RootState) => metafileSelectors.selectAll(state));
const modified = cards.map(c => metafiles.find(m => m.id === c.metafile)).filter(isDefined).filter(m => m.status && modifiedStatuses.includes(m.status));
const dispatch = useAppDispatch();

// load a new Explorer card containing the root of the repository at the specified branch
const clickHandle = async () => {
// undefined root indicates the main worktree, and any linked worktrees, are not associated with that branch
if (props.branch.root) {
dispatch(loadCard({ filepath: props.branch.root }));
} else {
const resultAction = await dispatch(fetchMetafilesByFilepath(props.repo.root));
const updated = (fetchMetafilesByFilepath.fulfilled.match(resultAction))
? await dispatch(checkoutBranch({ metafileId: metafiles[0].id, branchRef: props.branch.ref })).unwrap()
: undefined;
if (updated) {
dispatch(loadCard({ metafile: updated }));
}
const directoryContent = (await readDirAsync(props.branch.root));
const empty = directoryContent.length == 0 || (directoryContent.length == 1 && directoryContent.includes('.git')); // only a .git sub-directory counts as empty
const current = await currentBranch({ dir: props.branch.root });
let metafile = await dispatch(fetchMetafile({ filepath: props.branch.root })).unwrap();
const vcs = isFilebasedMetafile(metafile) ? await dispatch(fetchVersionControl(metafile)).unwrap() : undefined;
metafile = vcs ? dispatch(metafileUpdated({ ...metafile, ...vcs })).payload : metafile;
const updated = (empty || props.branch.ref !== current)
? await dispatch(checkoutBranch({ metafileId: metafile.id, branchRef: props.branch.ref })).unwrap()
: metafile;
if (updated) {
dispatch(loadCard({ metafile: updated }));
}
}

return (
<StyledTreeItem
key={`${props.repo}-${props.branch.id}`}
nodeId={`${props.repo}-${props.branch.id}`}
labelText={`${props.branch.ref} [${modified.length}/${cards.length}]`}
labelText={`${props.branch.ref} [${cards.length}]`}
labelIcon={GitBranchIcon}
onClick={clickHandle}
/>
Expand All @@ -72,16 +68,16 @@ const ReposOverview: React.FunctionComponent = () => {
return (
<div className='version-tracker'>
<TreeView
defaultCollapseIcon={<ArrowDropDownIcon />}
defaultExpandIcon={<ArrowRightIcon />}
defaultCollapseIcon={<ArrowDropDown />}
defaultExpandIcon={<ArrowRight />}
defaultEndIcon={<div style={{ width: 8 }} />}
expanded={expanded}
onNodeToggle={handleToggle}
>
{repos.length == 0 &&
<StyledTreeItem key={'no-repo'} nodeId={'no-repo'}
labelText={'[no repos tracked]'}
labelIcon={ErrorIcon}
labelIcon={Error}
/>
}
{repos.length > 0 && repos.map(repo => <RepoStatusComponent key={repo.id} repo={repo} />)}
Expand Down
2 changes: 1 addition & 1 deletion src/containers/branch-tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const loadBranchVersions = createAsyncThunk<void, void, AppThunkAPI>(
virtual: {
id: v4(),
modified: DateTime.local().valueOf(),
name: 'Repos Tracker', handler: 'ReposTracker'
name: 'Branch Tracker', handler: 'ReposTracker'
}
}))
.unwrap()
Expand Down
12 changes: 6 additions & 6 deletions src/containers/git-plumbing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,13 @@ export const resolveRef = async ({ dir, gitdir = path.join(dir.toString(), '.git
ref: string;
depth?: number;
}): Promise<string> => {
const worktree = await getWorktreePaths(gitdir);
const worktree = await getWorktreePaths(dir);
const optional = removeUndefinedProperties({ depth: depth });
const updatedRef = (worktree.worktreeLink && ref === 'HEAD') ? (await io.readFileAsync(path.join(worktree.worktreeLink.toString(), 'HEAD'), { encoding: 'utf-8' })).trim() : ref;

return (worktree.dir && worktree.gitdir)
? isogit.resolveRef({ fs: fs, dir: worktree.dir.toString(), gitdir: worktree.gitdir.toString(), ref: updatedRef, ...optional })
: isogit.resolveRef({ fs: fs, dir: dir.toString(), gitdir: gitdir.toString(), ref: ref, ...optional });
if (worktree.gitdir && worktree.worktreeLink && ref === 'HEAD') {
const linkedRef = (await io.readFileAsync(path.join(worktree.worktreeLink.toString(), 'HEAD'), { encoding: 'utf-8' })).slice('ref: '.length).trim();
return (await io.readFileAsync(path.join(worktree.gitdir.toString(), linkedRef), { encoding: 'utf-8' })).trim();
}
return await isogit.resolveRef({ fs: fs, dir: dir.toString(), gitdir: gitdir.toString(), ref: ref, ...optional });
}

/**
Expand Down
78 changes: 58 additions & 20 deletions src/containers/git-porcelain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { getProperty, setProperty, hasProperty, deleteProperty } from 'dot-prop'
import getGitConfigPath from 'git-config-path';
import type { Repository, GitStatus } from '../types';
import * as io from './io';
import { matrixEntry, statusMatrix } from './git-plumbing';
import { removeUndefinedProperties } from './format';
import { matrixEntry, matrixToStatus, statusMatrix } from './git-plumbing';
import { isDefined, removeUndefinedProperties } from './format';
import { getWorktreePaths } from './git-path';

export type GitConfig = { scope: 'none' } | { scope: 'local' | 'global', value: string, origin?: string };
Expand Down Expand Up @@ -38,8 +38,8 @@ export const resolveRef = async (dir: fs.PathLike, ref: string): Promise<string
* Clone a repository; this function is a wrapper to the *isomorphic-git/clone* function to inject the `fs` parameter and extend with
* additional local-only branch functionality. If the `ref` parameter or the current branch do not exist on the remote repository, then the
* local-only repository (including the *.git* directory) is copied using the *fs.copy* function (excluding the `node_modules` directory).
* @param repo A Repository object to be cloned.
* @param dir The worktree root directory to contain the cloned repo.
* @param repo A Repository object to be cloned.
* @param ref An optional branch name or SHA-1 hash to target cloning to that specific branch or commit.
* @param singleBranch Instead of the default behavior of fetching all the branches, only fetch a single branch.
* @param noCheckout Only fetch the repo without checking out a branch. Skipping checkout can save a lot of time normally spent writing
Expand All @@ -49,35 +49,53 @@ export const resolveRef = async (dir: fs.PathLike, ref: string): Promise<string
* @param exclude A list of branches or tags which should be excluded from remote server responses; specifically any commits reachable
* from these refs will be excluded.
* @param onProgress Callback for listening to GitProgressEvent occurrences during cloning.
* @return A Promise object for the clone operation.
* @return A Promise object containing a boolean representing success/failure of the cloning operation.
*/
export const clone = async ({ repo, dir, ref, singleBranch = false, noCheckout = false, noTags = false, depth, exclude, onProgress }: {
repo: Repository;
export const clone = async ({ dir, url, repo, ref, singleBranch = false, noCheckout = false, noTags = false, depth, exclude, onProgress }: {
dir: fs.PathLike;
url?: URL;
repo?: Repository;
ref?: string;
singleBranch?: boolean;
noCheckout?: boolean;
noTags?: boolean;
depth?: number;
exclude?: string[];
onProgress?: isogit.ProgressCallback | undefined;
}): Promise<void> => {
const optionals = removeUndefinedProperties({ depth: depth, exclude: exclude, onProgress: onProgress });
const worktree = await getWorktreePaths(repo.root);
const existingBranch = worktree.dir ? await currentBranch({ dir: worktree.dir, fullname: false }) : undefined;
const targetBranch = ref ? ref : existingBranch;
const remoteBranches = worktree.dir ? await isogit.listBranches({ fs: fs, dir: worktree.dir.toString(), remote: 'origin' }) : [];
}): Promise<boolean> => {
const optionals = removeUndefinedProperties({ ref, depth, exclude, onProgress });
if (dir.toString().length == 0) return false;

if (targetBranch && !remoteBranches.includes(targetBranch)) {
await fs.copy(repo.root.toString(), dir.toString(), { filter: path => !(path.indexOf('node_modules') > -1) }); // do not copy node_modules/ directory
if (targetBranch !== existingBranch)
if (url) {
// cloning a new repository from remote URL
await isogit.clone({
fs: fs, http: http, dir: dir.toString(), url: url.toString(), singleBranch: singleBranch, noCheckout: noCheckout,
noTags: noTags, ...optionals
});
return true;
}

if (repo) {
const worktree = await getWorktreePaths(repo.root);
const existingBranch = worktree.dir ? await currentBranch({ dir: worktree.dir, fullname: false }) : undefined;
const remoteBranches = worktree.dir ? await isogit.listBranches({ fs: fs, dir: worktree.dir.toString(), remote: 'origin' }) : [];
const targetBranch = ref ? ref : existingBranch;

if (targetBranch && !remoteBranches.includes(targetBranch)) {
// cloning a local-only branch via copy & checkout
await fs.copy(repo.root.toString(), dir.toString(), { filter: path => !(path.indexOf('node_modules') > -1) }); // do not copy node_modules/ directory
await checkout({ dir: dir, ref: targetBranch, noCheckout: noCheckout });
return;
return true;
} else {
// cloning an existing repository into a linked worktree root directory
await isogit.clone({
fs: fs, http: http, dir: dir.toString(), url: repo.url, singleBranch: singleBranch, noCheckout: noCheckout,
noTags: noTags, ...optionals
});
return true;
}
}
return isogit.clone({
fs: fs, http: http, dir: dir.toString(), url: repo.url, singleBranch: singleBranch, noCheckout: noCheckout,
noTags: noTags, ...optionals
});
return false;
};

/**
Expand Down Expand Up @@ -283,6 +301,26 @@ export const getStatus = async (filepath: fs.PathLike): Promise<GitStatus | unde
return matrixEntry(filepath);
}

/**
* Checks the git tracking status of a specific file or directory path for matches against a set of status filters. If the file is
* tracked by a branch in a linked worktree then status checks will look at the index file in the `GIT_DIR/worktrees/{branch}` directory
* for determining HEAD, WORKDIR, and STAGE status codes. Status codes are translated into comparable `GitStatus` type before comparison
* with the provided status filters.
* @param filepath The relative or absolute path to evaluate.
* @param statusFilters Array of `GitStatus` values to check against.
* @return A Promise object containing false if the path is not contained within a directory under version control, or a boolean
* indicating whether any file or files matched at least one of the `GitStatus` values in the provided status filter.
*/
export const hasStatus = async (filepath: fs.PathLike, statusFilters: GitStatus[]): Promise<boolean> => {
const statuses = await statusMatrix(filepath);
const found: GitStatus[] = statuses ? statuses
.map(row => matrixToStatus({ matrixEntry: row }))
.filter(isDefined)
.filter(status => statusFilters.includes(status))
: [];
return (found.length > 0) ? true : false;
}

/**
* List a remote servers branches, tags, and capabilities; this function is a wrapper to inject the `fs` parameter in to the
* *isomorphic-git/getRemoteInfo* function.
Expand Down
Loading

0 comments on commit 4ecd036

Please sign in to comment.