add git components

git4refactor
filip mertens 10 months ago
parent 6c72d7c231
commit df103f9870
  1. 189
      libs/remix-ui/git/src/components/gitui.tsx
  2. 52
      libs/remix-ui/git/src/components/navigation/branchedetails.tsx
  3. 30
      libs/remix-ui/git/src/components/navigation/branches.tsx
  4. 27
      libs/remix-ui/git/src/components/navigation/clone.tsx
  5. 50
      libs/remix-ui/git/src/components/navigation/commands.tsx
  6. 27
      libs/remix-ui/git/src/components/navigation/commitdetails.tsx
  7. 49
      libs/remix-ui/git/src/components/navigation/commits.tsx
  8. 29
      libs/remix-ui/git/src/components/navigation/github.tsx
  9. 49
      libs/remix-ui/git/src/components/navigation/menu/sourcecontrolmenu.tsx
  10. 30
      libs/remix-ui/git/src/components/navigation/remotes.tsx
  11. 44
      libs/remix-ui/git/src/components/navigation/remotesdetails.tsx
  12. 43
      libs/remix-ui/git/src/components/navigation/settings.tsx
  13. 54
      libs/remix-ui/git/src/components/navigation/sourcecontrol.tsx
  14. 55
      libs/remix-ui/git/src/components/navigation/sourcecontrolgroup.tsx
  15. 57
      libs/remix-ui/git/src/components/panels/branches.tsx
  16. 61
      libs/remix-ui/git/src/components/panels/branches/branchedetails.tsx
  17. 100
      libs/remix-ui/git/src/components/panels/clone.tsx
  18. 16
      libs/remix-ui/git/src/components/panels/commands.tsx
  19. 15
      libs/remix-ui/git/src/components/panels/commands/fetch.tsx
  20. 67
      libs/remix-ui/git/src/components/panels/commands/merge.tsx
  21. 146
      libs/remix-ui/git/src/components/panels/commands/pushpull.tsx
  22. 54
      libs/remix-ui/git/src/components/panels/commitmessage.tsx
  23. 50
      libs/remix-ui/git/src/components/panels/commits.tsx
  24. 40
      libs/remix-ui/git/src/components/panels/commits/commitdetails.tsx
  25. 47
      libs/remix-ui/git/src/components/panels/commits/commitdetailsitem.tsx
  26. 53
      libs/remix-ui/git/src/components/panels/commits/commitsummary.tsx
  27. 143
      libs/remix-ui/git/src/components/panels/github.tsx
  28. 55
      libs/remix-ui/git/src/components/panels/githubcredentials.tsx
  29. 58
      libs/remix-ui/git/src/components/panels/remotes.tsx
  30. 47
      libs/remix-ui/git/src/components/panels/remoteselect.tsx
  31. 100
      libs/remix-ui/git/src/components/panels/remotesimport.tsx
  32. 131
      libs/remix-ui/git/src/components/panels/repositories.tsx
  33. 52
      libs/remix-ui/git/src/components/panels/settings.tsx
  34. 38
      libs/remix-ui/git/src/components/panels/sourcecontrol/sourcecontrolgroup.tsx
  35. 67
      libs/remix-ui/git/src/components/panels/sourcecontrol/sourcecontrolitem.tsx
  36. 44
      libs/remix-ui/git/src/components/panels/sourcecontrol/sourcontrolitembuttons.tsx
  37. 58
      libs/remix-ui/git/src/components/panels/sourcontrol.tsx
  38. 37
      libs/remix-ui/git/src/hooks/useLocalStorage.tsx
  39. 4
      libs/remix-ui/git/src/index.ts
  40. 59
      libs/remix-ui/git/src/lib/fileHelpers.ts
  41. 619
      libs/remix-ui/git/src/lib/gitactions.ts
  42. 192
      libs/remix-ui/git/src/lib/listeners.ts
  43. 104
      libs/remix-ui/git/src/lib/pluginActions.ts
  44. 39
      libs/remix-ui/git/src/state/context.tsx
  45. 130
      libs/remix-ui/git/src/state/gitpayload.ts
  46. 147
      libs/remix-ui/git/src/state/gitreducer.tsx
  47. 11
      libs/remix-ui/git/src/state/loaderReducer.ts
  48. 36
      libs/remix-ui/git/src/style/index.css
  49. 214
      libs/remix-ui/git/src/types/index.ts
  50. 53
      libs/remix-ui/git/src/types/styles.ts
  51. 3
      libs/remix-ui/git/src/utils/index.ts

@ -0,0 +1,189 @@
import React, { useEffect, useReducer, useState } from 'react'
import { add, addall, checkout, checkoutfile, clone, commit, createBranch, remoteBranches, repositories, rm, getCommitChanges, diff, resolveRef, getBranchCommits, setUpstreamRemote, getGitHubUser, getBranches, getRemotes, remoteCommits } from '../lib/gitactions'
import { loadFiles, setCallBacks } from '../lib/listeners'
import { openDiff, openFile, saveToken, setModifiedDecorator, setPlugin, setUntrackedDecorator, statusChanged } from '../lib/pluginActions'
import { gitActionsContext, pluginActionsContext } from '../state/context'
import { gitReducer } from '../state/gitreducer'
import { defaultGitState, defaultLoaderState, gitState, loaderState } from '../types'
import { SourceControl } from './panels/sourcontrol'
import { Accordion } from "react-bootstrap";
import { CommitMessage } from './panels/commitmessage'
import { Commits } from './panels/commits'
import { Branches } from './panels/branches'
import { SourceControlNavigation } from './navigation/sourcecontrol'
import { BranchesNavigation } from './navigation/branches'
import { CommitslNavigation } from './navigation/commits'
import '../style/index.css'
import { CloneNavigation } from './navigation/clone'
import { Clone } from './panels/clone'
import { Commands } from './panels/commands'
import { CommandsNavigation } from './navigation/commands'
import { RemotesNavigation } from './navigation/remotes'
import { Remotes } from './panels/remotes'
import { ViewPlugin } from '@remixproject/engine-web'
import { SettingsNavigation } from './navigation/settings'
import { Settings } from './panels/settings'
import { GitHubNavigation } from './navigation/github'
import { GitHubAuth } from './panels/github'
import { GitHubCredentials } from './panels/githubcredentials'
import { loaderReducer } from '../state/loaderReducer'
export const gitPluginContext = React.createContext<gitState>(defaultGitState)
export const loaderContext = React.createContext<loaderState>(defaultLoaderState)
interface IGitUi {
plugin: ViewPlugin
}
export const GitUI = (props: IGitUi) => {
const plugin = props.plugin
const [gitState, gitDispatch] = useReducer(gitReducer, defaultGitState)
const [loaderState, loaderDispatch] = useReducer(loaderReducer, defaultLoaderState)
const [activePanel, setActivePanel] = useState<string>("0");
const [timeOut, setTimeOut] = useState<number>(null)
useEffect(() => {
setCallBacks(plugin, gitDispatch, loaderDispatch)
setPlugin(plugin, gitDispatch, loaderDispatch)
console.log(props)
}, [])
useEffect(() => {
async function setDecorators(gitState: gitState) {
await plugin.call('fileDecorator', 'clearFileDecorators')
await setModifiedDecorator(gitState.modified)
await setUntrackedDecorator(gitState.untracked)
}
console.log('gitState.fileStatusResult', gitState.fileStatusResult)
setTimeout(() => {
setDecorators(gitState)
})
}, [gitState.fileStatusResult])
useEffect(() => {
async function updatestate(){
console.log('updatestate', gitState)
if(gitState.currentBranch.remote.url){
remoteCommits(gitState.currentBranch.remote.url, gitState.currentBranch.name, 1)
}
}
setTimeout(() => {
updatestate()
})
}, [gitState.gitHubUser, gitState.currentBranch, gitState.remotes])
const gitActionsProviderValue = {
commit,
addall,
add,
checkoutfile,
rm,
checkout,
createBranch,
clone,
repositories,
remoteBranches,
getCommitChanges,
getBranchCommits,
diff,
resolveRef,
setUpstreamRemote,
getGitHubUser,
getBranches,
getRemotes
}
const pluginActionsProviderValue = {
statusChanged,
loadFiles,
openFile,
openDiff,
saveToken
}
return (
<div className="m-1">
<gitPluginContext.Provider value={gitState}>
<loaderContext.Provider value={loaderState}>
<gitActionsContext.Provider value={gitActionsProviderValue}>
<pluginActionsContext.Provider value={pluginActionsProviderValue}>
{gitState.loading && <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div>}
{!gitState.loading &&
<Accordion activeKey={activePanel} defaultActiveKey="0">
<SourceControlNavigation eventKey="0" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className='bg-light' eventKey="0">
<>
<CommitMessage />
<SourceControl />
</>
</Accordion.Collapse>
<hr></hr>
<CommandsNavigation eventKey="1" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className='bg-light' eventKey="1">
<>
<Commands></Commands>
</>
</Accordion.Collapse>
<hr></hr>
<CommitslNavigation eventKey="3" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className='bg-light' eventKey="3">
<>
<Commits />
</>
</Accordion.Collapse>
<hr></hr>
<BranchesNavigation eventKey="2" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className='bg-light' eventKey="2">
<>
<Branches /></>
</Accordion.Collapse>
<hr></hr>
<CloneNavigation eventKey="4" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className='bg-light' eventKey="4">
<>
<Clone /></>
</Accordion.Collapse>
<hr></hr>
<RemotesNavigation eventKey="5" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className='bg-light' eventKey="5">
<>
<Remotes></Remotes>
</>
</Accordion.Collapse>
<hr></hr>
<SettingsNavigation eventKey="6" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className='bg-light' eventKey="6">
<>
<Settings></Settings>
</>
</Accordion.Collapse>
<hr></hr>
<GitHubNavigation eventKey="7" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className='bg-light' eventKey="7">
<>
<GitHubAuth></GitHubAuth>
<GitHubCredentials></GitHubCredentials>
</>
</Accordion.Collapse>
</Accordion>}
</pluginActionsContext.Provider>
</gitActionsContext.Provider>
</loaderContext.Provider>
</gitPluginContext.Provider>
</div>
)
}

@ -0,0 +1,52 @@
import { faCaretUp, faCaretDown, faCaretRight, faArrowUp, faArrowDown, faArrowRotateRight, faArrowsUpDown, faGlobe, faCheckCircle, faToggleOff, faToggleOn } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { useContext, useEffect } from "react";
import { branch } from "../../types";
import { gitPluginContext } from "../gitui";
interface BrancheDetailsNavigationProps {
eventKey: string;
activePanel: string;
callback: (eventKey: string) => void;
branch: branch;
checkout: (branch: branch) => void;
}
export const BrancheDetailsNavigation = (props: BrancheDetailsNavigationProps) => {
const { eventKey, activePanel, callback, branch, checkout } = props;
const context = React.useContext(gitPluginContext)
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
const openRemote = () => {
window.open(`${branch.remote.url}/tree/${branch.name}`, '_blank');
}
return (
<>
<div className="d-flex flex-row w-100 mb-2 mt-2">
<div onClick={() => handleClick()} role={'button'} className='pointer d-flex flex-row w-100 commit-navigation'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<i className="fa fa-code-branch ml-1"></i>
<div className={`ml-1 ${context.currentBranch.name === branch.name ? 'text-success' : ''}`}>{branch.name} {branch.remote ? `on ${branch.remote.remote}` : ''}</div>
</div>
{context.currentBranch.name === branch.name ?
<FontAwesomeIcon className='ml-auto mr-1 pointer text-success' icon={faToggleOff} onClick={() => checkout(branch)}></FontAwesomeIcon>
:
<FontAwesomeIcon className='ml-auto mr-1 pointer' icon={faToggleOn} onClick={() => checkout(branch)}></FontAwesomeIcon>
}
<FontAwesomeIcon className='ml-auto pointer' icon={faArrowsUpDown} onClick={() => checkout(branch)}></FontAwesomeIcon>
{branch.remote?.url && <FontAwesomeIcon className='ml-2 pointer' icon={faGlobe} onClick={() => openRemote()}></FontAwesomeIcon>}
</div>
</>
);
}

@ -0,0 +1,30 @@
import { faCaretDown, faCaretRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { } from "react";
import { gitActionsContext, pluginActionsContext } from "../../state/context";
export const BranchesNavigation = ({ eventKey, activePanel, callback }) => {
const pluginactions = React.useContext(pluginActionsContext)
const context = React.useContext(gitActionsContext)
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
return (
<>
<div className={'d-flex justify-content-between pt-1 ' + (activePanel === eventKey? 'bg-light': '')}>
<span onClick={()=>handleClick()} role={'button'} className='nav d-flex justify-content-start align-items-center w-75'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<label className="pl-1 nav form-check-label">BRANCHES</label>
</span>
</div>
</>
);
}

@ -0,0 +1,27 @@
import { faCaretUp, faCaretDown, faArrowUp, faArrowDown, faArrowRotateRight, faCaretRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { useContext, useEffect } from "react";
export const CloneNavigation = ({ eventKey, activePanel, callback }) => {
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
return (
<>
<div className={'d-flex justify-content-between pb-1 pt-1 ' + (activePanel === eventKey? 'bg-light': '')}>
<span onClick={()=>handleClick()} role={'button'} className='nav d-flex justify-content-start align-items-center w-75'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<label className="pl-1 nav form-check-label">CLONE</label>
</span>
</div>
</>
);
}

@ -0,0 +1,50 @@
import { faCaretUp, faCaretDown, faArrowUp, faArrowDown, faArrowRotateRight, faCaretRight, faCircleCheck, faArrowsUpDown } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { CustomTooltip } from "@remix-ui/helper";
import React, { useContext, useEffect } from "react";
import { FormattedMessage } from "react-intl";
import { pluginActionsContext } from "../../state/context";
import { SourceControlMenu } from "./menu/sourcecontrolmenu";
export const CommandsNavigation = ({ eventKey, activePanel, callback }) => {
const pluginactions = React.useContext(pluginActionsContext)
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
return (
<>
<div className={'d-flex justify-content-between ' + (activePanel === eventKey ? 'bg-light' : '')}>
<span onClick={() => handleClick()} role={'button'} className='nav d-flex justify-content-start align-items-center w-75'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<label className="pl-1 nav form-check-label">COMMANDS</label>
</span>
{
activePanel === eventKey ?
<span className='d-flex justify-content-end align-items-center w-25'>
<CustomTooltip tooltipText={<FormattedMessage id="Pull" />}>
<button onClick={async () => { await pluginactions.loadFiles() }} className='btn btn-sm'><FontAwesomeIcon icon={faArrowDown} className="" /></button>
</CustomTooltip>
<CustomTooltip tooltipText={<FormattedMessage id="Push" />}>
<button onClick={async () => { await pluginactions.loadFiles() }} className='btn btn-sm'><FontAwesomeIcon icon={faArrowUp} className="" /></button>
</CustomTooltip>
<CustomTooltip tooltipText={<FormattedMessage id="Sync changes" />}>
<button onClick={async () => { await pluginactions.loadFiles() }} className='btn btn-sm'><FontAwesomeIcon icon={faArrowsUpDown} className="" /></button>
</CustomTooltip>
</span> : null
}
</div>
</>
);
}

@ -0,0 +1,27 @@
import { faCaretUp, faCaretDown, faCaretRight, faArrowUp, faArrowDown, faArrowRotateRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { useContext, useEffect } from "react";
import { CommitSummary } from "../panels/commits/commitsummary";
export const CommitDetailsNavigation = ({ eventKey, activePanel, callback, commit, checkout }) => {
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
return (
<>
<div onClick={() => handleClick()} role={'button'} className='pointer mb-2 mt-2 w-100 d-flex flex-row commit-navigation'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<CommitSummary commit={commit} checkout={checkout}></CommitSummary>
</div>
</>
);
}

@ -0,0 +1,49 @@
import { faCaretDown, faArrowUp, faArrowDown, faArrowRotateRight, faCaretRight, faArrowsUpDown } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { CustomTooltip } from "@remix-ui/helper";
import React, { } from "react";
import { FormattedMessage } from "react-intl";
import { pluginActionsContext } from "../../state/context";
export const CommitslNavigation = ({ eventKey, activePanel, callback }) => {
const pluginactions = React.useContext(pluginActionsContext)
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
return (
<>
<div className={'d-flex justify-content-between ' + (activePanel === eventKey ? 'bg-light' : '')}>
<span onClick={() => handleClick()} role={'button'} className='nav d-flex justify-content-start align-items-center w-75'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<label className="pl-1 nav form-check-label">COMMITS</label>
</span>
{
activePanel === eventKey ?
<span className='d-flex justify-content-end align-items-center w-25'>
<CustomTooltip tooltipText={<FormattedMessage id="Pull" />}>
<button onClick={async () => { await pluginactions.loadFiles() }} className='btn btn-sm'><FontAwesomeIcon icon={faArrowDown} className="" /></button>
</CustomTooltip>
<CustomTooltip tooltipText={<FormattedMessage id="Push" />}>
<button onClick={async () => { await pluginactions.loadFiles() }} className='btn btn-sm'><FontAwesomeIcon icon={faArrowUp} className="" /></button>
</CustomTooltip>
<CustomTooltip tooltipText={<FormattedMessage id="Sync changes" />}>
<button onClick={async () => { await pluginactions.loadFiles() }} className='btn btn-sm'><FontAwesomeIcon icon={faArrowsUpDown} className="" /></button>
</CustomTooltip>
</span> : null
}
</div>
</>
);
}

@ -0,0 +1,29 @@
import { faCaretDown, faCaretRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { } from "react";
import { pluginActionsContext } from "../../state/context";
export const GitHubNavigation = ({ eventKey, activePanel, callback }) => {
const pluginactions = React.useContext(pluginActionsContext)
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
return (
<>
<div className={'d-flex justify-content-between pt-1 pb-1 ' + (activePanel === eventKey? 'bg-light': '')}>
<span onClick={()=>handleClick()} role={'button'} className='nav d-flex justify-content-start align-items-center w-75'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<label className="pl-1 nav form-check-label">GITHUB</label>
</span>
</div>
</>
);
}

@ -0,0 +1,49 @@
import { count } from "console"
import { CustomIconsToggle, CustomMenu, CustomTooltip } from "@remix-ui/helper"
import React, { useState } from "react"
import { Dropdown } from "react-bootstrap"
import { FormattedMessage } from "react-intl"
export const SourceControlMenu = () => {
const [showIconsMenu, hideIconsMenu] = useState<boolean>(false)
return (
<Dropdown id="workspacesMenuDropdown" data-id="sourceControlMenuDropdown" onToggle={() => hideIconsMenu(!showIconsMenu)} show={showIconsMenu}>
<Dropdown.Toggle
onClick={() => {
hideIconsMenu(!showIconsMenu)
}}
as={CustomIconsToggle}
icon={'fas fa-bars'}
></Dropdown.Toggle>
<Dropdown.Menu as={CustomMenu} data-id="wsdropdownMenu" className='custom-dropdown-items remixui_menuwidth' rootCloseEvent="click">
<Dropdown.Item key={0}>
<CustomTooltip
placement="right-start"
tooltipId="cloneWorkspaceTooltip"
tooltipClasses="text-nowrap"
tooltipText={<FormattedMessage id='filePanel.workspace.clone' defaultMessage='Clone Git Repository' />}
>
<div
data-id='cloneGitRepository'
onClick={() => {
hideIconsMenu(!showIconsMenu)
}}
key={`cloneGitRepository-fe-ws`}
>
<span
id='cloneGitRepository'
data-id='cloneGitRepository'
onClick={() => {
hideIconsMenu(!showIconsMenu)
}}
className='fab fa-github pl-2'
>
</span>
<span className="pl-3"><FormattedMessage id='filePanel.clone' defaultMessage='Clone' /></span>
</div>
</CustomTooltip>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
)
}

@ -0,0 +1,30 @@
import { faCaretDown, faCaretRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { } from "react";
import { gitActionsContext, pluginActionsContext } from "../../state/context";
export const RemotesNavigation = ({ eventKey, activePanel, callback }) => {
const pluginactions = React.useContext(pluginActionsContext)
const context = React.useContext(gitActionsContext)
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
return (
<>
<div className={'d-flex justify-content-between pt-1 pb-1 ' + (activePanel === eventKey? 'bg-light': '')}>
<span onClick={()=>handleClick()} role={'button'} className='nav d-flex justify-content-start align-items-center w-75'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<label className="pl-1 nav form-check-label">REMOTES</label>
</span>
</div>
</>
);
}

@ -0,0 +1,44 @@
import { faCaretDown, faCaretRight, faArrowRightArrowLeft, faGlobe } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { useContext, useEffect } from "react";
import { branch, remote } from "../../types";
import { gitPluginContext } from "../gitui";
interface RemotesDetailsNavigationProps {
eventKey: string;
activePanel: string;
callback: (eventKey: string) => void;
remote: remote;
}
export const RemotesDetailsNavigation = (props: RemotesDetailsNavigationProps) => {
const { eventKey, activePanel, callback, remote } = props;
const context = React.useContext(gitPluginContext)
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
const openRemote = () => {
window.open(`${remote.url}`, '_blank');
}
return (
<>
<div className="d-flex flex-row w-100 mb-2 mt-2">
<div onClick={() => handleClick()} role={'button'} className='pointer d-flex flex-row w-100 commit-navigation'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<div className={`ml-1`}>{remote.remote} <FontAwesomeIcon className='' icon={faArrowRightArrowLeft}></FontAwesomeIcon> {remote.url}</div>
</div>
{remote?.url && <FontAwesomeIcon className='ml-2 pointer' icon={faGlobe} onClick={() => openRemote()}></FontAwesomeIcon>}
</div>
</>
);
}

@ -0,0 +1,43 @@
import { faCaretUp, faCaretDown, faArrowUp, faArrowDown, faArrowRotateRight, faCaretRight, faArrowsUpDown, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { CustomTooltip } from "@remix-ui/helper";
import React, { useContext, useEffect } from "react";
import { FormattedMessage } from "react-intl";
import { pluginActionsContext } from "../../state/context";
export const SettingsNavigation = ({ eventKey, activePanel, callback }) => {
const pluginactions = React.useContext(pluginActionsContext)
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
return (
<>
<div className={'d-flex justify-content-between ' + (activePanel === eventKey ? 'bg-light' : '')}>
<span onClick={() => handleClick()} role={'button'} className='nav d-flex justify-content-start align-items-center w-75'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<label className="nav pl-1 form-check-label">SETTINGS</label>
</span>
<span className='d-flex justify-content-end align-items-center w-25'>
<CustomTooltip tooltipText={<FormattedMessage id="Missing values" />}>
<button onClick={async () => { await pluginactions.loadFiles() }} className='btn btn-sm text-warning'><FontAwesomeIcon icon={faTriangleExclamation} className="" /></button>
</CustomTooltip>
</span>
</div>
</>
);
}

@ -0,0 +1,54 @@
import { faCaretUp, faCaretDown, faArrowUp, faArrowDown, faArrowRotateRight, faCaretRight, faArrowsUpDown } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { CustomTooltip } from "@remix-ui/helper";
import React, { useContext, useEffect } from "react";
import { FormattedMessage } from "react-intl";
import { pluginActionsContext } from "../../state/context";
import { SourceControlMenu } from "./menu/sourcecontrolmenu";
export const SourceControlNavigation = ({ eventKey, activePanel, callback }) => {
const pluginactions = React.useContext(pluginActionsContext)
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
return (
<>
<div className={'d-flex justify-content-between ' + (activePanel === eventKey ? 'bg-light' : '')}>
<span onClick={() => handleClick()} role={'button'} className='nav d-flex justify-content-start align-items-center w-75'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<label className="nav pl-1 form-check-label">SOURCE CONTROL</label>
</span>
{
activePanel === eventKey ?
<span className='d-flex justify-content-end align-items-center w-25'>
<CustomTooltip tooltipText={<FormattedMessage id="Push" />}>
<button onClick={async () => { await pluginactions.loadFiles() }} className='btn btn-sm'><FontAwesomeIcon icon={faArrowUp} className="" /></button>
</CustomTooltip>
<CustomTooltip tooltipText={<FormattedMessage id="Pull" />}>
<button onClick={async () => { await pluginactions.loadFiles() }} className='btn btn-sm'><FontAwesomeIcon icon={faArrowDown} className="" /></button>
</CustomTooltip>
<CustomTooltip tooltipText={<FormattedMessage id="Sync changes" />}>
<button onClick={async () => { await pluginactions.loadFiles() }} className='btn btn-sm'><FontAwesomeIcon icon={faArrowsUpDown} className="" /></button>
</CustomTooltip>
<CustomTooltip tooltipText={<FormattedMessage id="Refresh" />}>
<button onClick={async () => { await pluginactions.loadFiles() }} className='btn btn-sm'><FontAwesomeIcon icon={faArrowRotateRight} className="" /></button>
</CustomTooltip>
</span> : null
}
</div>
</>
);
}

@ -0,0 +1,55 @@
import { faCaretUp, faCaretDown, faArrowUp, faArrowDown, faArrowRotateRight, faCaretRight, faArrowsUpDown, faPlus, faMinus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { CustomTooltip } from "@remix-ui/helper";
import React, { useContext, useEffect } from "react";
import { FormattedMessage } from "react-intl";
import { gitActionsContext, pluginActionsContext } from "../../state/context";
import { sourceControlGroup } from "../../types";
interface SourceControlGroupNavigationProps {
eventKey: string;
activePanel: string;
callback: (eventKey: string) => void;
group: sourceControlGroup
}
export const SourceControlGroupNavigation = (props: SourceControlGroupNavigationProps) => {
const { eventKey, activePanel, callback, group } = props;
const actions = React.useContext(gitActionsContext)
const pluginActions = React.useContext(pluginActionsContext)
const handleClick = () => {
if (!callback) return
if (activePanel === eventKey) {
callback('')
} else {
callback(eventKey)
}
}
return (
<>
<div className={'d-flex justify-content-between pt-1 ' + (activePanel === eventKey? 'bg-light': '')}>
<span onClick={()=>handleClick()} role={'button'} className='nav d-flex justify-content-start align-items-center w-75'>
{
activePanel === eventKey ? <FontAwesomeIcon className='' icon={faCaretDown}></FontAwesomeIcon> : <FontAwesomeIcon className='' icon={faCaretRight}></FontAwesomeIcon>
}
<label className="pl-1 nav form-check-label">{group.name}</label>
</span>
{
activePanel === eventKey ?
<span className='d-flex justify-content-end align-items-center w-25'>
{group.name === 'Changes' ?
<CustomTooltip tooltipText={<FormattedMessage id="Stage All Changes" />}>
<button onClick={async () => { await actions.addall() }} className='btn btn-sm'><FontAwesomeIcon icon={faPlus} className="" /></button>
</CustomTooltip>: null}
{group.name === 'Staged' ?
<CustomTooltip tooltipText={<FormattedMessage id="Unstage All Changes" />}>
<button onClick={async () => { await pluginActions.loadFiles() }} className='btn btn-sm'><FontAwesomeIcon icon={faMinus} className="" /></button>
</CustomTooltip>: null}
</span> : null
}
</div>
</>
);
}

@ -0,0 +1,57 @@
import React, { useState } from "react";
import { Alert } from "react-bootstrap";
import { gitActionsContext } from "../../state/context";
import { remote } from "../../types";
import { gitPluginContext } from "../gitui";
import { BranchDetails } from "./branches/branchedetails";
export const Branches = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [newBranch, setNewBranch] = useState({ value: "" });
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewBranch({ value: e.currentTarget.value });
};
const checkout = async (oid: string, remote: remote) => {
try {
actions.checkout({ ref: oid, remote: remote.remote });
} catch (e) {
// do nothing
}
};
return (
<>
<div className="pt-1">
{context.branches && context.branches.length ?
<div>
{context.branches && context.branches.map((branch, index) => {
return (
<BranchDetails key={index} branch={branch}></BranchDetails>
);
})}
<hr />
<label>create branch</label>
<div className="form-group">
<input
placeholder="branch name"
onChange={handleChange}
className="form-control w-md-25 w-100"
type="text"
id="newbranchname"
/>
</div>
<button
onClick={async () => actions.createBranch(newBranch.value)}
className="btn w-md-25 w-100 btn-primary"
id="createbranch-btn"
>
create new branch
</button>
</div> : <div className="text-muted">No branches</div>}
</div>
</>
);
}

@ -0,0 +1,61 @@
import { ReadCommitResult } from "isomorphic-git"
import React, { useEffect, useState } from "react";
import { Accordion } from "react-bootstrap";
import { CommitDetailsNavigation } from "../../navigation/commitdetails";
import { gitActionsContext } from "../../../state/context";
import { gitPluginContext } from "../../gitui";
import { branch } from "../../../types";
import { BrancheDetailsNavigation } from "../../navigation/branchedetails";
import { CommitDetailsItems } from "../commits/commitdetailsitem";
import { CommitDetails } from "../commits/commitdetails";
export interface BrancheDetailsProps {
branch: branch;
}
export const BranchDetails = (props: BrancheDetailsProps) => {
const { branch } = props;
const actions = React.useContext(gitActionsContext)
const context = React.useContext(gitPluginContext)
const [activePanel, setActivePanel] = useState<string>("");
useEffect(() => {
if (activePanel === "0") {
console.log('GET BRANCH COMMITS', branch)
actions.getBranchCommits(branch)
}
}, [activePanel])
const checkout = (branch: branch) => {
actions.checkout({
ref: branch.name,
remote: branch.remote && branch.remote.remote || null
});
}
const checkoutCommit = async (oid: string) => {
try {
//await ModalRef.current?.show();
actions.checkout({ ref: oid })
//Utils.log("yes");
} catch (e) {
//Utils.log("no");
}
};
return (<Accordion activeKey={activePanel} defaultActiveKey="">
<BrancheDetailsNavigation checkout={checkout} branch={branch} eventKey="0" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className="pl-2 border-left ml-1" eventKey="0">
<div className="ml-1">
{context.branchCommits && Object.entries(context.branchCommits).map(([key, value]) => {
if(key == branch.name){
return value.map((commit, index) => {
return(<CommitDetails key={index} checkout={checkoutCommit} commit={commit}></CommitDetails>)
})
}
})}
</div>
</Accordion.Collapse>
</Accordion>)
}

@ -0,0 +1,100 @@
import React, { useState } from "react";
import { Alert, Form, FormControl, InputGroup } from "react-bootstrap";
import { useLocalStorage } from "../../hooks/useLocalStorage";
import { gitActionsContext } from "../../state/context";
import { gitPluginContext } from "../gitui";
import { Repositories } from "./repositories";
import { RemixUiCheckbox } from "@remix-ui/checkbox";
export const Clone = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [cloneUrl, setCloneUrl] = useLocalStorage(
"CLONE_URL",
''
);
const [cloneDepth, setCloneDepth] = useLocalStorage(
"CLONE_DEPTH",
1
);
const [cloneBranch, setCloneBranch] = useLocalStorage(
"CLONE_BRANCH",
''
);
const [url, setUrl] = useLocalStorage(
"GITHUB_URL",
''
);
const clone = async () => {
try {
setTimeout(() => actions.clone(cloneUrl, cloneBranch, cloneDepth, !cloneAllBranches), 1500)
} catch (e) {
}
}
const onCloneBranchChange = (value: string) => {
setCloneBranch(value)
}
const onGitHubCloneUrlChange = (value: string) => {
setCloneUrl(value)
}
const onDepthChange = (value: number) => {
setCloneDepth(value)
}
const [cloneAllBranches, setcloneAllBranches] = useLocalStorage(
"GITHUB_CLONE_ALL_BRANCES",
false
);
const onAllBranchChange = () => {
setcloneAllBranches((e: any) => !e)
}
return (
<>
<InputGroup className="mb-1">
<FormControl id="cloneulr" placeholder="url" name='cloneurl' value={cloneUrl} onChange={e => onGitHubCloneUrlChange(e.target.value)} aria-describedby="urlprepend" />
</InputGroup>
<input name='clonebranch' onChange={e => onCloneBranchChange(e.target.value)} value={cloneBranch} className="form-control mb-1 mt-2" placeholder="branch" type="text" id="clonebranch" />
<button disabled={!cloneUrl || !cloneBranch} data-id='clonebtn' className='btn btn-primary mt-1 w-100' onClick={async () => {
clone()
}}>clone</button>
<hr />
<Repositories cloneAllBranches={cloneAllBranches} cloneDepth={cloneDepth} />
<hr />
<label>options</label>
<InputGroup className="mt-1 mb-1">
<InputGroup.Prepend>
<InputGroup.Text id="clonedepthprepend">
--depth
</InputGroup.Text>
</InputGroup.Prepend>
<FormControl id="clonedepth" type="number" value={cloneDepth} onChange={e => onDepthChange(parseInt(e.target.value))} aria-describedby="clonedepthprepend" />
</InputGroup>
<RemixUiCheckbox
id={`cloneAllBranches`}
inputType="checkbox"
name="cloneAllBranches"
label={`Clone all branches`}
onClick={() => onAllBranchChange()}
checked={cloneAllBranches}
onChange={() => { }}
/>
<hr></hr>
</>)
}

@ -0,0 +1,16 @@
import React, { useEffect, useState } from "react";
import { PushPull } from "./commands/pushpull";
import { Fetch } from "./commands/fetch";
import { Merge } from "./commands/merge";
export const Commands = () => {
return (
<>
<PushPull></PushPull>
<hr></hr>
<Fetch></Fetch>
<hr></hr>
<Merge></Merge>
</>)
}

@ -0,0 +1,15 @@
import React, { useEffect, useState } from "react";
export const Fetch = () => {
const fetch = async () => {
//gitservice.fetch(currentRemote, '', '')
}
return (
<>
<button type="button" onClick={async () => fetch()} className="btn btn-primary w-100">Fetch</button>
</>)
}

@ -0,0 +1,67 @@
import React, { useEffect, useState } from "react";
import { gitActionsContext } from "../../../state/context";
import { gitPluginContext } from "../../gitui";
import { selectStyles, selectTheme } from "../../../types/styles";
import Select from 'react-select'
export const Merge = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [localBranch, setLocalBranch] = useState('')
const [localBranchOptions, setLocalBranchOptions] = useState<any>([]);
useEffect(() => {
setLocalBranch(context.currentBranch.name)
}, [context.currentBranch])
const onLocalBranchChange = (value: any) => {
console.log('onLocalBranchChange', value)
setLocalBranch(value)
}
const merge = async () => {
//gitservice.push(currentRemote, branch || '', remoteBranch, force)
}
useEffect(() => {
console.log('context', context.repositories)
// map context.repositories to options
const localBranches = context.branches && context.branches.length > 0 && context.branches
.filter(branch => !branch.remote)
.map(repo => {
return { value: repo.name, label: repo.name }
})
setLocalBranchOptions(localBranches)
}, [context.branches])
return (
<>
<div className="btn-group w-100" role="group" aria-label="Basic example">
<button type="button" onClick={async () => merge()} className="btn btn-primary mr-1">Merge</button>
</div>
<label>Merge from Branch</label>
<Select
options={localBranchOptions}
onChange={(e: any) => e && onLocalBranchChange(e.value)}
theme={selectTheme}
styles={selectStyles}
isClearable={true}
value={{ value: localBranch, label: localBranch }}
placeholder="Type to search for a branch..."
/>
</>)
}

@ -0,0 +1,146 @@
import React, { useEffect, useState } from "react";
import { gitActionsContext } from "../../../state/context";
import { gitPluginContext } from "../../gitui";
import { selectStyles, selectTheme } from "../../../types/styles";
import Select, { Options, OptionsOrGroups } from 'react-select'
import { setUpstream } from "../../../state/gitpayload";
export const PushPull = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [remoteBranch, setRemoteBranch] = useState('')
const [localBranch, setLocalBranch] = useState('')
const [localBranchOptions, setLocalBranchOptions] = useState<any>([]);
const [remoteBranchOptions, setRemoteBranchOptions] = useState<any>([]);
const [localRemotesOptions, setLocalRemotesOptions] = useState<any>([]);
const [force, setForce] = useState(false)
useEffect(() => {
setRemoteBranch(context.currentBranch.name)
setLocalBranch(context.currentBranch.name)
if ((!context.upstream || context.upstream === '') && context.currentBranch && context.currentBranch.remote && context.currentBranch.remote.remote) {
actions.setUpstreamRemote(context.currentBranch.remote.remote)
}
}, [context.currentBranch])
const onRemoteBranchChange = (value: string) => {
setRemoteBranch(value)
}
const onLocalBranchChange = (value: any) => {
console.log('onLocalBranchChange', value)
setLocalBranch(value)
}
const onRemoteChange = (value: any) => {
console.log('onRemoteChange', value)
actions.setUpstreamRemote(value)
}
useEffect(() => {
console.log('UPSTREAM', context.upstream)
}, [context.upstream])
const onForceChange = (event: any) => {
const target = event.target;
const value = target.checked;
setForce(value)
}
const push = async () => {
//gitservice.push(currentRemote, branch || '', remoteBranch, force)
}
const pull = async () => {
//gitservice.pull(currentRemote, branch || '', remoteBranch)
}
useEffect(() => {
console.log('context', context.repositories)
// map context.repositories to options
const localBranches = context.branches && context.branches.length > 0 && context.branches
.filter(branch => !branch.remote)
.map(repo => {
return { value: repo.name, label: repo.name }
})
setLocalBranchOptions(localBranches)
const remoteBranches = context.branches && context.branches.length > 0 && context.branches
.filter(branch => branch.remote)
.map(repo => {
return { value: repo.name, label: repo.name }
}
)
setRemoteBranchOptions(remoteBranches)
}, [context.branches])
useEffect(() => {
console.log('context', context.remotes)
// map context.repositories to options
const options = context.remotes && context.remotes.length > 0 && context.remotes
.map(repo => {
return { value: repo.remote, label: repo.remote }
})
setLocalRemotesOptions(options)
}, [context.remotes])
return (
<>
<div className="btn-group w-100" role="group" aria-label="Basic example">
<button type="button" onClick={async () => push()} className="btn btn-primary mr-1">Push</button>
<button type="button" onClick={async () => pull()} className="btn btn-primary">Pull</button>
</div>
<label>Local Branch</label>
<Select
options={localBranchOptions}
onChange={(e: any) => e && onLocalBranchChange(e.value)}
theme={selectTheme}
styles={selectStyles}
isClearable={true}
value={{ value: localBranch, label: localBranch }}
placeholder="Type to search for a branch..."
/>
<label>Remote Branch</label>
<Select
options={remoteBranchOptions}
onChange={(e: any) => e && onRemoteBranchChange(e.value)}
theme={selectTheme}
styles={selectStyles}
isClearable={true}
value={{ value: remoteBranch, label: remoteBranch }}
placeholder="Type to search for a branch..."
/>
<label>Upstream</label>
<Select
options={localRemotesOptions}
onChange={(e: any) => e && onRemoteChange(e.value)}
theme={selectTheme}
styles={selectStyles}
isClearable={true}
value={{ value: context.upstream, label: context.upstream }}
placeholder="Type to search for a branch..."
/>
<div className="mt-2 remixui_compilerConfig custom-control custom-checkbox">
<input checked={force} onChange={e => onForceChange(e)} className="remixui_autocompile custom-control-input" type="checkbox" data-id="compilerContainerAutoCompile" id="forcepush" title="Force Push" />
<label className="form-check-label custom-control-label" htmlFor="forcepush">Force push</label>
</div>
</>)
}

@ -0,0 +1,54 @@
import React, { useEffect } from "react"
import { useState } from "react"
import { gitActionsContext } from "../../state/context"
import { gitPluginContext } from "../gitui"
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export const CommitMessage = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [message, setMessage] = useState({ value: '' })
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setMessage({ value: e.currentTarget.value })
}
useEffect(() => {
if (context.fileStatusResult) {
console.log(context.staged.length + ' staged')
}
}, [context.fileStatusResult])
const commit = async() => {
if(context.staged.length === 0 && context.allchangesnotstaged.length == 0) return
if(context.staged.length === 0)
await actions.addall()
await actions.commit(message.value)
}
const commitAllowed = () => {
return context.canCommit === false || message.value === "" || ( context.staged.length === 0 && context.allchangesnotstaged.length == 0 )
}
const commitMessagePlaceholder = () => {
if(context.currentBranch === undefined || context.currentBranch.name === "")
return `message`
return `message ( commit on ${context.currentBranch.name} )`
}
return (
<>
<div className="form-group">
<input placeholder={commitMessagePlaceholder()} data-id='commitMessage' className="form-control" type="text" onChange={handleChange} value={message.value} />
</div>
{context.canCommit ? <></> : <div className='alert alert-warning'>Cannot commit in detached state! Create a new branch and check it out first or checkout main.<br></br></div>}
<button data-id='commitButton' className="btn btn-primary w-100" disabled={commitAllowed()} onClick={async () => await commit()} >
<FontAwesomeIcon icon={faCheck} className="mr-1" />
Commit
</button>
<hr></hr>
</>
);
}

@ -0,0 +1,50 @@
import { checkout, ReadCommitResult } from "isomorphic-git";
import React from "react";
import { gitActionsContext } from "../../state/context";
import { gitPluginContext } from "../gitui";
import { CommitDetails } from "./commits/commitdetails";
import { CommitSummary } from "./commits/commitsummary";
export const Commits = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const checkout = async (oid: string) => {
try {
//await ModalRef.current?.show();
actions.checkout({ ref: oid })
//Utils.log("yes");
} catch (e) {
//Utils.log("no");
}
};
return (
<>
{context.commits && context.commits.length ?
<div>
<div className="pt-1">
{context.commits && context.commits.map((commit, index) => {
return (
<CommitDetails key={index} checkout={checkout} commit={commit}></CommitDetails>
);
})}
<div
onClick={async () => await checkout("main")}
className="btn btn-primary btn-sm checkout-btn mt-2 d-none"
data-oid="main"
>
git checkout main
</div>
</div>
</div>
: <div className="text-muted">No commits</div>}
</>
)
}

@ -0,0 +1,40 @@
import { ReadCommitResult } from "isomorphic-git"
import React, { useEffect, useState } from "react";
import { Accordion } from "react-bootstrap";
import { CommitDetailsNavigation } from "../../navigation/commitdetails";
import { gitActionsContext } from "../../../state/context";
import { gitPluginContext } from "../../gitui";
import { CommitDetailsItems } from "./commitdetailsitem";
export interface CommitDetailsProps {
commit: ReadCommitResult;
checkout: (oid: string) => void;
}
export const CommitDetails = (props: CommitDetailsProps) => {
const { commit, checkout } = props;
const actions = React.useContext(gitActionsContext)
const context = React.useContext(gitPluginContext)
const [activePanel, setActivePanel] = useState<string>("");
useEffect(() => {
if (activePanel === "0") {
console.log(commit.oid, commit.commit.parent)
actions.getCommitChanges(commit.oid, commit.commit.parent[0])
}
}, [activePanel])
return (<Accordion activeKey={activePanel} defaultActiveKey="">
<CommitDetailsNavigation commit={commit} checkout={checkout} eventKey="0" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className="pl-2 border-left ml-1" eventKey="0">
<>
{context.commitChanges && context.commitChanges.filter(
(change) => change.hashModified === commit.oid && change.hashOriginal === commit.commit.parent[0]
).map((change, index) => {
return(<CommitDetailsItems key={index} commitChange={change}></CommitDetailsItems>)
})}
</>
</Accordion.Collapse>
</Accordion>)
}

@ -0,0 +1,47 @@
import { commitChange } from "../../../types";
import React from "react";
import path from "path";
import { gitActionsContext, pluginActionsContext } from "../../../state/context";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGlobe } from "@fortawesome/free-solid-svg-icons";
export interface CCommitDetailsItemsProps {
commitChange: commitChange;
}
export const CommitDetailsItems = (props: CCommitDetailsItemsProps) => {
const { commitChange } = props;
const actions = React.useContext(gitActionsContext)
const pluginActions = React.useContext(pluginActionsContext)
const openChanges = async (change: commitChange) => {
console.log("open changes", change);
await actions.diff(change)
console.log("open changes", change);
await pluginActions.openDiff(change)
}
function FunctionStatusIcons() {
const status = commitChange.type
return (<>
{status && status.indexOf("modified") === -1 ? <></> : <span>M</span>}
{status && status.indexOf("deleted") === -1 ? <></> : <span>D</span>}
{status && status.indexOf("added") === -1 ? <></> : <span>A</span>}
</>)
}
return (<>
<div className="d-flex w-100 d-flex flex-row commitdetailsitem">
<div className='pointer gitfile long-and-truncated' onClick={async () => await openChanges(commitChange)}>
<span className='font-weight-bold long-and-truncated'>{path.basename(commitChange.path)}</span>
<div className='text-secondary long-and-truncated'> {commitChange.path}</div>
</div>
<div className="d-flex align-items-end">
<FontAwesomeIcon icon={faGlobe} className="mr-1 align-self-center" />
<FunctionStatusIcons></FunctionStatusIcons>
</div>
</div>
</>)
}

@ -0,0 +1,53 @@
import { ReadCommitResult } from "isomorphic-git"
import { default as dateFormat } from "dateformat";
import React from "react";
export interface CommitSummaryProps {
commit: ReadCommitResult;
checkout: (oid: string) => void;
}
export const CommitSummary = (props: CommitSummaryProps) => {
const { commit, checkout } = props;
const getDate = (commit: ReadCommitResult) => {
const timestamp = commit.commit.author.timestamp;
if (timestamp) {
// calculate the difference between the current time and the commit time in days or hours or minutes
const diff = Math.floor((Date.now() - timestamp * 1000) / 1000 / 60 / 60 / 24);
if (diff == 0) {
return "today at " + dateFormat(timestamp * 1000, "HH:MM");
} else
if (diff < 1) {
// return how many hours ago
return `${Math.floor(diff * 24)} hour(s) ago`;
}
if (diff < 7) {
// return how many days ago
return `${diff} day(s) ago`;
}
if (diff < 365) {
return dateFormat(timestamp * 1000, "mmm dd");
}
return dateFormat(timestamp * 1000, "mmm dd yyyy");
}
return "";
};
return (
<>
<div className="long-and-truncated ml-2">
{commit.commit.message}
</div>
{commit.commit.author.name || ""}
<span className="ml-1">{getDate(commit)}</span>
</>
)
}

@ -0,0 +1,143 @@
import React, { useEffect } from "react";
import { gitActionsContext, pluginActionsContext } from "../../state/context";
import { gitPluginContext } from "../gitui";
import axios from "axios";
import { CopyToClipboard } from "@remix-ui/clipboard";
import { Card } from "react-bootstrap";
export const GitHubAuth = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const pluginActions = React.useContext(pluginActionsContext)
const [gitHubResponse, setGitHubResponse] = React.useState<any>(null)
const [authorized, setAuthorized] = React.useState<boolean>(false)
const getDeviceCodeFromGitHub = async () => {
setAuthorized(false)
// Send a POST request
const response = await axios({
method: 'post',
url: 'http://0.0.0.0:3000/github.com/login/device/code',
data: {
client_id: 'Iv1.12fc02c69c512462'
},
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
});
// convert response to json
const githubrespone = await response.data;
console.log('json', githubrespone)
setGitHubResponse(githubrespone)
}
const connectApp = async () => {
// poll https://github.com/login/oauth/access_token
const accestokenresponse = await axios({
method: 'post',
url: 'http://0.0.0.0:3000/github.com/login/oauth/access_token',
data: {
client_id: 'Iv1.12fc02c69c512462',
device_code: gitHubResponse.device_code,
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
},
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
});
// convert response to json
const response = await accestokenresponse.data;
console.log('json2', response)
if (response.error) {
}
if (response.access_token) {
setAuthorized(true)
await pluginActions.saveToken(response.access_token)
await actions.getGitHubUser()
}
}
const disconnect = async () => {
setAuthorized(false)
setGitHubResponse(null)
}
const checkConnection = async () => {
//await actions.getGitHubUser()
}
useEffect(() => {
},[])
useEffect(() => {
console.log('context.rateLimit', context.rateLimit)
}, [context.rateLimit])
return (
<>
{(context.gitHubUser && context.gitHubUser.login) ? null :
<button className='btn btn-primary mt-1 w-100' onClick={async () => {
getDeviceCodeFromGitHub();
}}>Login in with github</button>
}
{gitHubResponse && !authorized &&
<div className="pt-2">
Step 1: Copy this code:
<div className="input-group text-secondary mb-0 h6">
<input disabled type="text" className="form-control" value={gitHubResponse.user_code} />
<div className="input-group-append">
<CopyToClipboard content={gitHubResponse.user_code} data-id='copyToClipboardCopyIcon' className='far fa-copy ml-1 p-2 mt-1' direction={"top"} />
</div>
</div>
<br></br>
Step 2: Authorize the app here
<br></br><a target="_blank" href={gitHubResponse.verification_uri}>{gitHubResponse.verification_uri}</a>
<br /><br></br>
Step 3: When you are done, click on the button below:
<button className='btn btn-primary mt-1 w-100' onClick={async () => {
connectApp()
}}>Connect</button>
</div>
}
{
(context.gitHubUser && context.gitHubUser.login) ?
<div className="pt-2">
<button className='btn btn-primary mt-1 w-100' onClick={async () => {
disconnect()
}}>Disconnect</button>
</div>: null
}
{
(context.gitHubUser && context.gitHubUser.login) ?
<div className="pt-2">
<Card>
<Card.Body>
<Card.Title>Connected as {context.gitHubUser.login}</Card.Title>
<Card.Text>
<img src={context.gitHubUser.avatar_url} className="w-100" />
<a target="_blank" href={context.gitHubUser.html_url}>{context.gitHubUser.html_url}</a>
</Card.Text>
</Card.Body>
</Card>
</div>: null
}
</>)
}

@ -0,0 +1,55 @@
import { checkout, clone, ReadCommitResult } from "isomorphic-git";
import React from "react";
import { gitActionsContext } from "../../state/context";
import { gitPluginContext } from "../gitui";
import { CustomTooltip } from "@remix-ui/helper";
import { useIntl, FormattedMessage } from "react-intl";
import { CopyToClipboard } from "@remix-ui/clipboard";
import { FormControl, InputGroup } from "react-bootstrap";
export const GitHubCredentials = () => {
const context = React.useContext(gitPluginContext)
const [githubToken, setGithubToken] = React.useState('')
const [githubUsername, setGithubUsername] = React.useState('')
const [githubEmail, setGithubEmail] = React.useState('')
const intl = useIntl()
const gitAccessTokenLink = 'https://github.com/settings/tokens/new?scopes=gist,repo&description=Remix%20IDE%20Token'
function handleChangeTokenState(e: string): void {
throw new Error("Function not implemented.");
}
function handleChangeUserNameState(e: string): void {
throw new Error("Function not implemented.");
}
function handleChangeEmailState(e: string): void {
throw new Error("Function not implemented.");
}
function saveGithubToken(): void {
throw new Error("Function not implemented.");
}
function removeToken(): void {
throw new Error("Function not implemented.");
}
return (
<>
<div className="input-group text-secondary mb-0 h6">
<input type="text" className="form-control" name='githubToken' />
<div className="input-group-append">
<CopyToClipboard content={''} data-id='copyToClipboardCopyIcon' className='far fa-copy ml-1 p-2 mt-1' direction={"top"} />
</div>
</div>
<input name='githubUsername' onChange={e => handleChangeUserNameState(e.target.value)} value={githubUsername} className="form-control mb-2" placeholder="GitHub username" type="text" id="githubUsername" />
<input name='githubEmail' onChange={e => handleChangeEmailState(e.target.value)} value={githubEmail} className="form-control mb-1" placeholder="GitHub email" type="text" id="githubEmail" />
<hr />
</>
);
}

@ -0,0 +1,58 @@
import React, { useEffect } from "react";
import { gitActionsContext } from "../../state/context";
import { gitPluginContext } from "../gitui";
import { Remoteselect } from "./remoteselect";
import { RemotesImport } from "./remotesimport";
export const Remotes = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [remoteName, setRemoteName] = React.useState<string>('')
const [url, setUrl] = React.useState<string>('')
const onRemoteNameChange = (value: string) => {
setRemoteName(value)
}
const onUrlChange = (value: string) => {
setUrl(value)
}
const addRemote = async () => {
//await gitservice.addRemote(remoteName, url)
//setCurrentRemote(remoteName)
//await gitservice.getRemotes()
}
useEffect(() => {
console.log('SHOW REMOTES', context.remotes)
}, [context.remotes])
return (
<>
{context.remotes && context.remotes.length ?
<>
{context.remotes && context.remotes.map((remote, index) => {
return (
<Remoteselect key={index} remote={remote}></Remoteselect>
);
})}
</> : <>No remotes</>}
<hr></hr>
<input placeholder="remote name" name='remotename' onChange={e => onRemoteNameChange(e.target.value)} value={remoteName} className="form-control mb-2" type="text" id="remotename" />
<input placeholder="remote url" name='remoteurl' onChange={e => onUrlChange(e.target.value)} value={url} className="form-control" type="text" id="remoteurl" />
<button className='btn btn-primary mt-1 w-100' onClick={async () => {
addRemote();
}}>add remote</button>
<hr />
<RemotesImport />
<hr />
</>)
}

@ -0,0 +1,47 @@
import { branch, checkout, ReadCommitResult } from "isomorphic-git";
import React, { useEffect, useState } from "react";
import { gitActionsContext } from "../../state/context";
import { gitPluginContext } from "../gitui";
import { default as dateFormat } from "dateformat";
import { RemotesDetailsNavigation } from "../navigation/remotesdetails";
import { Accordion } from "react-bootstrap";
import { remote } from "../../types";
import { BranchDetails } from "./branches/branchedetails";
export interface RemoteSelectProps {
remote: remote
}
export const Remoteselect = (props: RemoteSelectProps) => {
const { remote } = props;
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [activePanel, setActivePanel] = useState<string>("");
useEffect(() => {
if (activePanel === "0") {
console.log('fetching', remote)
}
}, [activePanel])
useEffect(() => {
console.log('remote branches', context.branches)
}, [context.branches])
return (
<>
<Accordion activeKey={activePanel} defaultActiveKey="">
<RemotesDetailsNavigation callback={setActivePanel} eventKey="0" activePanel={activePanel} remote={remote} />
<Accordion.Collapse className="pl-2 border-left ml-1" eventKey="0">
<>
{context.branches && context.branches.filter((branch, index) => branch.remote && branch.remote.remote === remote.remote ).map((branch, index) => {
return (
<BranchDetails key={index} branch={branch}></BranchDetails>
);
})}</>
</Accordion.Collapse>
</Accordion>
</>
)
}

@ -0,0 +1,100 @@
import React, { useEffect, useState } from "react";
import { Alert, Button } from "react-bootstrap";
import { gitActionsContext } from "../../state/context";
import { repository } from "../../types";
import { gitPluginContext } from "../gitui";
import Select from 'react-select'
import { selectStyles, selectTheme } from "../../types/styles";
export const RemotesImport = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [repo, setRepo] = useState<repository>(null);
const [repoOtions, setRepoOptions] = useState<any>([]);
const [loading, setLoading] = useState(false)
const [show, setShow] = useState(false)
const [remoteName, setRemoteName] = useState('')
useEffect(() => {
console.log('context', context.repositories)
// map context.repositories to options
const options = context.repositories && context.repositories.length > 0 && context.repositories.map(repo => {
return { value: repo.id, label: repo.full_name }
})
setLoading(false)
setRepoOptions(options)
}, [context.repositories])
const fetchRepositories = async () => {
try {
setShow(true)
setLoading(true)
setRepoOptions([])
console.log(await actions.repositories())
} catch (e) {
// do nothing
}
};
const selectRepo = async (value: number | string) => {
// find repo
console.log('setRepo', value, context.repositories)
const repo = context.repositories.find(repo => {
return repo.id.toString() === value.toString()
})
console.log('repo', repo)
if (repo) {
setRepo(repo)
}
}
const addRemote = async () => {
try {
} catch (e) {
// do nothing
}
};
const onRemoteNameChange = (value: string) => {
setRemoteName(value)
}
return (
<>
<Button onClick={fetchRepositories} className="w-100 mt-1">
<i className="fab fa-github mr-1"></i>Fetch Remotes from GitHub
</Button>
{show ?
<Select
options={repoOtions}
className="mt-1"
onChange={(e: any) => e && selectRepo(e.value)}
theme={selectTheme}
styles={selectStyles}
isClearable={true}
placeholder="Type to search for a repository..."
isLoading={loading}
/> : null}
{repo ?
<input placeholder="remote name" name='remotename' onChange={e => onRemoteNameChange(e.target.value)} value={remoteName} className="form-control mb-2" type="text" id="remotename" />
: null}
{repo && remoteName ?
<button data-id='clonebtn' className='btn btn-primary mt-1 w-100' onClick={async () => {
await addRemote()
}}>add {remoteName}:{repo.full_name}</button> : null}
</>
)
}

@ -0,0 +1,131 @@
import React, { useEffect, useState } from "react";
import { Alert, Button } from "react-bootstrap";
import { gitActionsContext } from "../../state/context";
import { repository } from "../../types";
import { gitPluginContext } from "../gitui";
import Select from 'react-select'
import { selectStyles, selectTheme } from "../../types/styles";
interface RepositoriesProps {
cloneDepth?: number
cloneAllBranches?: boolean
}
export const Repositories = (props: RepositoriesProps) => {
const { cloneDepth, cloneAllBranches } = props
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const [branch, setBranch] = useState({ name: "" });
const [repo, setRepo] = useState<repository>(null);
const [repoOtions, setRepoOptions] = useState<any>([]);
const [branchOptions, setBranchOptions] = useState<any>([]);
const [loading, setLoading] = useState(false)
const [show, setShow] = useState(false)
useEffect(() => {
console.log('context', context.repositories)
// map context.repositories to options
const options = context.repositories && context.repositories.length > 0 && context.repositories.map(repo => {
return { value: repo.id, label: repo.full_name }
})
setLoading(false)
setRepoOptions(options)
}, [context.repositories])
useEffect(() => {
// map context.branches to options
const options = context.remoteBranches && context.remoteBranches.length > 0 && context.remoteBranches.map(branch => {
return { value: branch.name, label: branch.name }
}
)
setBranchOptions(options)
}, [context.remoteBranches])
useEffect(() => {
console.log('show', show)
},[show])
const fetchRepositories = async () => {
try {
setShow(true)
setLoading(true)
setRepoOptions([])
setBranchOptions([])
console.log(await actions.repositories())
} catch (e) {
// do nothing
}
};
const selectRepo = async (value: number | string) => {
// find repo
console.log('setRepo', value, context.repositories)
const repo = context.repositories.find(repo => {
return repo.id.toString() === value.toString()
})
console.log('repo', repo)
if (repo) {
setBranchOptions([])
setBranch({ name: "" })
setRepo(repo)
await actions.remoteBranches(repo.owner.login, repo.name)
}
}
const selectRemoteBranch = async (value: number | string) => {
console.log('setRemoteBranch', value)
setBranch({ name: value.toString() })
}
const clone = async () => {
try {
console.log('clone', repo, branch)
actions.clone(repo.html_url, branch.name, cloneDepth, !cloneAllBranches)
} catch (e) {
// do nothing
}
};
return (
<>
<Button onClick={fetchRepositories} className="w-100 mt-1">
<i className="fab fa-github mr-1"></i>Fetch Repositories from GitHub
</Button>
{show?
<Select
options={repoOtions}
className="mt-1"
onChange={(e: any) => e && selectRepo(e.value)}
theme={selectTheme}
styles={selectStyles}
isClearable={true}
placeholder="Type to search for a repository..."
isLoading={loading}
/>:null}
{branchOptions && branchOptions.length ?
<Select
options={branchOptions}
className="mt-1"
onChange={(e: any) => e && selectRemoteBranch(e.value)}
theme={selectTheme}
styles={selectStyles}
isClearable={true}
placeholder="Type to search for a branch..."
/> : null}
{repo && branch.name && branch.name !== '0' ?
<button data-id='clonebtn' className='btn btn-primary mt-1 w-100' onClick={async () => {
await clone()
}}>clone {repo.full_name}:{branch.name}</button> : null}
</>
)
}

@ -0,0 +1,52 @@
import { checkout, clone, ReadCommitResult } from "isomorphic-git";
import React from "react";
import { gitActionsContext } from "../../state/context";
import { gitPluginContext } from "../gitui";
import { CustomTooltip } from "@remix-ui/helper";
import { useIntl, FormattedMessage } from "react-intl";
import { CopyToClipboard } from "@remix-ui/clipboard";
import { FormControl, InputGroup } from "react-bootstrap";
export const Settings = () => {
const [githubToken, setGithubToken] = React.useState('')
const [githubUsername, setGithubUsername] = React.useState('')
const [githubEmail, setGithubEmail] = React.useState('')
const intl = useIntl()
const gitAccessTokenLink = 'https://github.com/settings/tokens/new?scopes=gist,repo&description=Remix%20IDE%20Token'
function handleChangeTokenState(e: string): void {
throw new Error("Function not implemented.");
}
function handleChangeUserNameState(e: string): void {
throw new Error("Function not implemented.");
}
function handleChangeEmailState(e: string): void {
throw new Error("Function not implemented.");
}
function saveGithubToken(): void {
throw new Error("Function not implemented.");
}
function removeToken(): void {
throw new Error("Function not implemented.");
}
return (
<>
<input name='githubToken' onChange={e => handleChangeUserNameState(e.target.value)} value={githubToken} className="form-control mb-2" placeholder="GitHub token" type="text" id="githubToken" />
<input name='githubUsername' onChange={e => handleChangeUserNameState(e.target.value)} value={githubUsername} className="form-control mb-2" placeholder="GitHub username" type="text" id="githubUsername" />
<input name='githubEmail' onChange={e => handleChangeEmailState(e.target.value)} value={githubEmail} className="form-control mb-1" placeholder="GitHub email" type="text" id="githubEmail" />
<hr />
</>
);
}

@ -0,0 +1,38 @@
import { ReadCommitResult } from "isomorphic-git"
import React, { useEffect, useState } from "react";
import { Accordion } from "react-bootstrap";
import { gitActionsContext } from "../../../state/context";
import { gitPluginContext } from "../../gitui";
import { sourceControlGroup } from "../../../types";
import { SourceControlGroupNavigation } from "../../navigation/sourcecontrolgroup";
import { SourceControlItem } from "./sourcecontrolitem";
export interface SourceControGroupProps {
group: sourceControlGroup
}
export const SourceControGroup = (props: SourceControGroupProps) => {
const { group } = props;
const actions = React.useContext(gitActionsContext)
const context = React.useContext(gitPluginContext)
const [activePanel, setActivePanel] = useState<string>("0");
useEffect(() => {
if (activePanel === "0") {
}
}, [activePanel])
return (<>
{group.group.length > 0 ?
<Accordion activeKey={activePanel} defaultActiveKey="">
<SourceControlGroupNavigation group={group} eventKey="0" activePanel={activePanel} callback={setActivePanel} />
<Accordion.Collapse className="pl-2 border-left ml-1" eventKey="0">
<>
{group.group.map((file, index) => {
return (<SourceControlItem key={index} group={group} file={file}></SourceControlItem>)
})}
</>
</Accordion.Collapse>
</Accordion> : <></>}
</>)
}

@ -0,0 +1,67 @@
import { commitChange, fileStatusResult, sourceControlGroup } from "../../../types";
import React from "react";
import path from "path";
import { gitActionsContext, pluginActionsContext } from "../../../state/context";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGlobe } from "@fortawesome/free-solid-svg-icons";
import { SourceControlItemButtons } from "./sourcontrolitembuttons";
import { removeSlash } from "../../../utils";
export interface SourceControlItemProps {
file: fileStatusResult;
group: sourceControlGroup;
}
export const SourceControlItem = (props: SourceControlItemProps) => {
const { file, group } = props;
const actions = React.useContext(gitActionsContext)
const pluginActions = React.useContext(pluginActionsContext)
async function fileClick(file: fileStatusResult) {
console.log(file)
//let status = fileservice.getFileStatusForFile(file.filename || "");
if (file.statusNames && file.statusNames.indexOf("modified") !== -1) {
const headHash = await actions.resolveRef("HEAD")
const change: commitChange = {
path: removeSlash(file.filename),
type: "modified",
hashOriginal: headHash,
hashModified: '',
readonly: false,
}
await actions.diff(change)
await pluginActions.openDiff(change)
console.log("diff", change)
} else {
await pluginActions.openFile(file.filename)
//await client.call('fileManager', 'open', file.filename)
}
}
function FunctionStatusIcons() {
const status = file.statusNames
return (<>
{status && status.indexOf("modified") === -1 ? <></> : <div>M</div>}
{status && status.indexOf("deleted") === -1 ? <></> : <span>D</span>}
{status && status.indexOf("added") === -1 ? <></> : <span>A</span>}
{status && status.indexOf("untracked") === -1 ? <></> : <span>U</span>}
</>)
}
return (<>
<div className="d-flex w-100 d-flex flex-row align-items-center">
<div className='pointer gitfile long-and-truncated' onClick={async () => await fileClick(file)}>
<span className='font-weight-bold long-and-truncated'>{path.basename(file.filename)}</span>
<div className='text-secondary long-and-truncated'> {file.filename}</div>
</div>
<div className="d-flex align-items-center ml-1">
<SourceControlItemButtons group={group} file={file}></SourceControlItemButtons>
<FunctionStatusIcons></FunctionStatusIcons>
</div>
</div>
</>)
}

@ -0,0 +1,44 @@
import { commitChange, fileStatusResult, sourceControlGroup } from "../../../types";
import React from "react";
import path from "path";
import { gitActionsContext, pluginActionsContext } from "../../../state/context";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGlobe, faMinus, faPlus, faUndo } from "@fortawesome/free-solid-svg-icons";
export interface SourceControlItemButtonsProps {
file: fileStatusResult,
group: sourceControlGroup
}
export const SourceControlItemButtons = (props: SourceControlItemButtonsProps) => {
const { file, group } = props;
const actions = React.useContext(gitActionsContext)
const pluginActions = React.useContext(pluginActionsContext)
function RenderButtons() {
const status = file.statusNames
if (group.name === 'Staged') {
return <>
{status && status.indexOf("modified") === -1 ? <></> : <button onClick={async () => await actions.checkoutfile(file.filename)} className='btn btn-sm btn-secondary mr-1 '><FontAwesomeIcon icon={faUndo} className="" /></button>}
{status && status.indexOf("deleted") === -1 ? <></> : <button onClick={async () => await actions.checkoutfile(file.filename)} className='btn btn-sm btn-secondary mr-1 '><FontAwesomeIcon icon={faUndo} className="" /></button>}
{status && status.indexOf("deleted") !== -1 ? <></> : <button data-id={`unStage${group.name}${path.basename(file.filename)}`} onClick={async () => await actions.rm(file.filename)} className='btn btn-sm btn-secondary mr-1 '><FontAwesomeIcon icon={faMinus} className="" /></button>}
</>
}
if (group.name === 'Changes') {
return <>
{status && status.indexOf("deleted") === -1 ? <></> : <><button onClick={async () => await actions.checkoutfile(file.filename)} data-id={`undo${group.name}${path.basename(file.filename)}`} className='btn btn-sm btn-secondary mr-1 '><FontAwesomeIcon icon={faUndo} className="" /></button><button data-id={`addToGit${group.name}${path.basename(file.filename)}`} onClick={async () => await actions.rm(file.filename)} className='btn btn-sm btn-secondary mr-1 '><FontAwesomeIcon icon={faPlus} className="" /></button></>}
{status && status.indexOf("modified") === -1 ? <></> : <button onClick={async () => await actions.checkoutfile(file.filename)} data-id={`undo${group.name}${path.basename(file.filename)}`} className='btn btn-sm btn-secondary mr-1 '><FontAwesomeIcon icon={faUndo} className="" /></button>}
{(status && status.indexOf("unstaged") !== -1 || status && status.indexOf("deleted") !== -1) ? <></> : <button data-id={`addToGit${group.name}${path.basename(file.filename)}`} onClick={async () => await actions.add(file.filename)} className='btn btn-sm btn-secondary mr-1 '><FontAwesomeIcon icon={faPlus} className="" /></button>}
{(status && status.indexOf("unstaged") !== -1 && status && status.indexOf("modified") !== -1) ? <button data-id={`addToGit${group.name}${path.basename(file.filename)}`} onClick={async () => await actions.add(file.filename)} className='btn btn-sm btn-secondary mr-1 '><FontAwesomeIcon icon={faPlus} className="" /></button> : <></>}
</>
}
return <></>
}
return <RenderButtons />
}

@ -0,0 +1,58 @@
import React, { useEffect, useState } from 'react'
import { gitActionsContext, pluginActionsContext } from '../../state/context'
import { gitPluginContext } from '../gitui'
import { sourceControlGroup } from '../../types'
import { SourceControGroup } from './sourcecontrol/sourcecontrolgroup'
export const SourceControl = () => {
const context = React.useContext(gitPluginContext)
const actions = React.useContext(gitActionsContext)
const pluginactions = React.useContext(pluginActionsContext)
const [show, setShow] = useState(false)
useEffect(() => {
if (context.fileStatusResult) {
console.log(context)
const total = context.allchangesnotstaged.length
const badges = total + context.staged.length
pluginactions.statusChanged(badges)
//console.log("allchangesnotstaged", context.allchangesnotstaged, context.fileStatusResult)
setShow((context.deleted.length > 0 || context.staged.length > 0 || context.untracked.length > 0 || context.modified.length > 0))
}
}, [context.fileStatusResult, context.modified, context.allchangesnotstaged, context.untracked, context.deleted])
useEffect(() => {
if (context.commits) {
console.log("SC commits", context.localCommitCount, context.currentBranch)
}
}, [context.localCommitCount, context.currentBranch])
function RenderGroups() {
const groups: sourceControlGroup[] = [{ name: 'Staged', group: context.staged }, { name: 'Changes', group: context.allchangesnotstaged }]
return (<>
{
groups.map((ob: sourceControlGroup, index: number) => {
return (
<SourceControGroup key={index} group={ob}></SourceControGroup>
)
})
}
</>)
}
return (
<>
{show ?
<>
<div>
<RenderGroups></RenderGroups>
</div></>
: <>
</>}
</>
);
}

@ -0,0 +1,37 @@
import { useState } from "react"
export function useLocalStorage(key: string, initialValue: any) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
try {
// Get from local storage by key
const item = window.localStorage.getItem(key)
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue
} catch (error) {
// If error also return initialValue
console.error(error)
return initialValue
}
})
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value: any) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value
// Save state
setStoredValue(valueToStore)
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
// A more advanced implementation would handle the error case
console.error(error)
}
}
return [storedValue, setValue]
}

@ -0,0 +1,4 @@
export * from './types'
export { GitUI } from './components/gitui'
export { commitChange, commitChangeType, remote, branch } from './types'
export * from './types/styles'

@ -0,0 +1,59 @@
import { fileStatusResult, gitState } from "../types";
export const getFilesCountByStatus = (status: string, files: fileStatusResult[]) => {
let count = 0;
files.map((m) => {
if (m.statusNames !== undefined) {
if (m.statusNames && m.statusNames.indexOf(status) > -1) {
count++;
}
}
});
return count;
}
export const getFilesByStatus = (status: string, files: fileStatusResult[]): fileStatusResult[] => {
const result: fileStatusResult[] = []
files.map((m) => {
if (m.statusNames !== undefined) {
if (m.statusNames && m.statusNames.indexOf(status) > -1) {
result.push(m)
}
}
});
return result;
}
export const getFilesWithNotModifiedStatus = (files: fileStatusResult[]): fileStatusResult[] => {
const result: fileStatusResult[] = []
files.map((m) => {
if (m.statusNames !== undefined) {
if (m.statusNames && m.statusNames.indexOf("unmodified") === -1) {
result.push(m)
}
}
});
return result;
}
export const allChangedButNotStagedFiles = (files: fileStatusResult[]): fileStatusResult[] => {
let allfiles = getFilesWithNotModifiedStatus(files)
const staged = getFilesByStatus("staged", files)
allfiles = allfiles.filter((trackedFile) => {
return staged.findIndex((stagedFile) => stagedFile.filename === trackedFile.filename) === -1
})
return allfiles;
}
export const getFileStatusForFile = (filename: string, files: fileStatusResult[]) => {
for (let i: number = 0; i < files.length; i++) {
if (files[i].filename === filename)
return files[i].statusNames;
}
}

@ -0,0 +1,619 @@
import { ViewPlugin } from "@remixproject/engine-web";
import { ReadBlobResult, ReadCommitResult } from "isomorphic-git";
import React from "react";
import { fileStatus, fileStatusMerge, setBranchCommits, setBranches, setCanCommit, setCommitChanges, setCommits, setCurrentBranch, setGitHubUser, setLoading, setRateLimit, setRemoteBranches, setRemotes, setRepos, setUpstream } from "../state/gitpayload";
import { GitHubUser, RateLimit, branch, commitChange, gitActionDispatch, statusMatrixType } from '../types';
import { removeSlash } from "../utils";
import { disableCallBacks, enableCallBacks } from "./listeners";
import { AlertModal, ModalTypes } from "@remix-ui/app";
import { gitActionsContext } from "../state/context";
import { gitPluginContext } from "../components/gitui";
import { setFileDecorators } from "./pluginActions";
export const fileStatuses = [
["new,untracked", 0, 2, 0], // new, untracked
["added,staged", 0, 2, 2], //
["added,staged,with unstaged changes", 0, 2, 3], // added, staged, with unstaged changes
["unmodified", 1, 1, 1], // unmodified
["modified,unstaged", 1, 2, 1], // modified, unstaged
["modified,staged", 1, 2, 2], // modified, staged
["modified,staged,with unstaged changes", 1, 2, 3], // modified, staged, with unstaged changes
["deleted,unstaged", 1, 0, 1], // deleted, unstaged
["deleted,staged", 1, 0, 0],
//["deleted", 1, 1, 0], // deleted, staged
["unmodified", 1, 1, 3],
["deleted,not in git", 0, 0, 3],
["unstaged,modified", 1, 2, 0]
];
const statusmatrix: statusMatrixType[] = fileStatuses.map((x: any) => {
return {
matrix: x.shift().split(","),
status: x,
};
});
let plugin: ViewPlugin, dispatch: React.Dispatch<gitActionDispatch>
export const setPlugin = (p: ViewPlugin, dispatcher: React.Dispatch<gitActionDispatch>) => {
plugin = p
dispatch = dispatcher
}
export const getBranches = async () => {
console.log('getBranches')
const branches = await plugin.call("dGitProvider", "branches");
console.log('branches :>>', branches)
dispatch(setBranches(branches));
}
export const getRemotes = async () => {
console.log('getRemotes')
const remotes = await plugin.call("dGitProvider", "remotes" as any);
console.log('remotes :>>', remotes)
dispatch(setRemotes(remotes));
}
export const setUpstreamRemote = async (remote: string) => {
dispatch(setUpstream(remote));
}
export const getFileStatusMatrix = async (filepaths: string[]) => {
const fileStatusResult = await statusMatrix(filepaths);
fileStatusResult.map((m) => {
statusmatrix.map((sm) => {
if (JSON.stringify(sm.status) === JSON.stringify(m.status)) {
//Utils.log(m, sm);
m.statusNames = sm.matrix;
}
});
});
//console.log(fileStatusResult);
if (!filepaths) {
dispatch(fileStatus(fileStatusResult))
} else {
dispatch(fileStatusMerge(fileStatusResult))
setFileDecorators(fileStatusResult)
}
}
export const getCommits = async () => {
//Utils.log("get commits");
console.log('getCommits')
try {
const commits: ReadCommitResult[] = await plugin.call(
"dGitProvider",
"log",
{ ref: "HEAD" }
);
console.log('commits :>>', commits)
return commits;
} catch (e) {
return [];
}
}
export const gitlog = async () => {
let commits = []
try {
commits = await getCommits()
} catch (e) {
}
dispatch(setCommits(commits))
await showCurrentBranch()
await getGitHubUser()
}
export const showCurrentBranch = async () => {
try {
const branch = await currentBranch();
const currentcommitoid = await getCommitFromRef("HEAD");
if (typeof branch === "undefined" || branch.name === "") {
//toast.warn(`You are in a detached state`);
plugin.call('notification', 'alert', {
type: 'warning',
title: 'You are in a detached state',
})
branch.name = `HEAD detached at ${currentcommitoid}`;
//canCommit = false;
dispatch(setCanCommit(false));
} else {
//canCommit = true;
dispatch(setCanCommit(true));
dispatch(setCurrentBranch(branch));
}
} catch (e) {
// show empty branch
}
}
export const currentBranch = async () => {
// eslint-disable-next-line no-useless-catch
try {
const branch: branch =
(await plugin.call("dGitProvider", "currentbranch")) || "";
return branch;
} catch (e) {
throw e;
}
}
export const createBranch = async (name: string = "") => {
if (name) {
await plugin.call("dGitProvider", "branch", { ref: name });
await plugin.call("dGitProvider", "checkout", { ref: name });
}
}
export const getCommitFromRef = async (ref: string) => {
const commitOid = await plugin.call("dGitProvider", "resolveref", {
ref: ref,
});
return commitOid;
}
const settingsWarning = async () => {
const username = await plugin.call('config' as any, 'getAppParameter', 'settings/github-user-name')
const email = await plugin.call('config' as any, 'getAppParameter', 'settings/github-email')
if (!username || !email) {
plugin.call('notification', 'toast', 'Please set your github username and email in the settings')
return false;
} else {
return {
username,
email
};
}
}
export const commit = async (message: string = "") => {
try {
const credentials = await settingsWarning()
if (!credentials) {
dispatch(setLoading(false))
return
}
const sha = await plugin.call("dGitProvider", "commit", {
author: {
name: credentials.username,
email: credentials.email,
},
message: message,
});
plugin.call('notification', 'toast', `Commited: ${sha}`)
} catch (err) {
plugin.call('notification', 'toast', `${err}`)
}
}
export const addall = async () => {
try {
await plugin
.call("dGitProvider", "status", { ref: "HEAD" })
.then((status) =>
Promise.all(
status.map(([filepath, , worktreeStatus]) =>
worktreeStatus
? plugin.call("dGitProvider", "add", {
filepath: removeSlash(filepath),
})
: plugin.call("dGitProvider", "rm", {
filepath: removeSlash(filepath),
})
)
)
);
plugin.call('notification', 'toast', `Added all files to git`)
} catch (e) {
plugin.call('notification', 'toast', `${e}`)
}
}
export const add = async (args: string | undefined) => {
if (args !== undefined) {
let filename = args; // $(args[0].currentTarget).data('file')
let stagingfiles;
if (filename !== "/") {
filename = removeSlash(filename);
stagingfiles = [filename];
} else {
await addall();
return;
}
try {
for (const filepath of stagingfiles) {
try {
await plugin.call("dGitProvider", "add", {
filepath: removeSlash(filepath),
});
} catch (e) { }
}
plugin.call('notification', 'toast', `Added ${filename} to git`);
} catch (e) {
plugin.call('notification', 'toast', `${e}`)
}
}
}
const getLastCommmit = async () => {
try {
let currentcommitoid = "";
currentcommitoid = await getCommitFromRef("HEAD");
return currentcommitoid;
} catch (e) {
return false;
}
}
export const rm = async (args: any) => {
const filename = args;
await plugin.call("dGitProvider", "rm", {
filepath: removeSlash(filename),
});
plugin.call('notification', 'toast', `Removed ${filename} from git`)
}
export const checkoutfile = async (filename: string) => {
const oid = await getLastCommmit();
if (oid)
try {
const commitOid = await plugin.call("dGitProvider", "resolveref", {
ref: oid,
});
const { blob } = await plugin.call("dGitProvider", "readblob", {
oid: commitOid,
filepath: removeSlash(filename),
});
const original = Buffer.from(blob).toString("utf8");
//(original, filename);
await disableCallBacks();
await plugin.call(
"fileManager",
"setFile",
removeSlash(filename),
original
);
await enableCallBacks();
} catch (e) {
plugin.call('notification', 'toast', `No such file`)
}
}
export const checkout = async (cmd: any) => {
await disableCallBacks();
await plugin.call('fileManager', 'closeAllFiles')
try {
await plugin.call("dGitProvider", "checkout", cmd);
gitlog();
} catch (e) {
plugin.call('notification', 'toast', `${e}`)
}
await enableCallBacks();
}
export const clone = async (url: string, branch: string, depth: number, singleBranch: boolean) => {
console.log(url, branch, depth, singleBranch)
dispatch(setLoading(true))
try {
await disableCallBacks()
// get last part of url
const urlParts = url.split("/");
const lastPart = urlParts[urlParts.length - 1];
const repoName = lastPart.split(".")[0];
// add timestamp to repo name
const timestamp = new Date().getTime();
const repoNameWithTimestamp = `${repoName}-${timestamp}`;
//const token = await tokenWarning();
const token = await plugin.call('config' as any, 'getAppParameter', 'settings/gist-access-token')
//if (!token) {
// dispatch(setLoading(false))
// return
//} else {
await plugin.call('dGitProvider' as any, 'clone', { url, branch, token, depth, singleBranch }, repoNameWithTimestamp);
await enableCallBacks()
plugin.call('notification', 'toast', `Cloned ${url} to ${repoNameWithTimestamp}`)
//}
} catch (e: any) {
await parseError(e)
}
dispatch(setLoading(false))
}
const tokenWarning = async () => {
const token = await plugin.call('config' as any, 'getAppParameter', 'settings/gist-access-token')
if (!token) {
const modalContent: AlertModal = {
message: 'Please set a token first in the GitHub settings of REMIX',
title: 'No GitHub token set',
id: 'no-token-set',
}
plugin.call('notification', 'alert', modalContent)
return false;
} else {
return token;
}
}
const parseError = async (e: any) => {
// if message conttains 401 Unauthorized, show token warning
if (e.message.includes('401')) {
const result = await plugin.call('notification', 'modal', {
title: 'The GitHub token may be missing or invalid',
message: 'Please check the GitHub token and try again. Error: 401 Unauthorized',
okLabel: 'Go to settings',
cancelLabel: 'Close',
type: ModalTypes.confirm
})
console.log(result)
}
// if message contains 404 Not Found, show repo not found
else if (e.message.includes('404')) {
await plugin.call('notification', 'modal', {
title: 'Repository not found',
message: 'Please check the URL and try again.',
okLabel: 'Go to settings',
cancelLabel: 'Close',
type: ModalTypes.confirm
})
}
// if message contains 403 Forbidden
else if (e.message.includes('403')) {
await plugin.call('notification', 'modal', {
title: 'The GitHub token may be missing or invalid',
message: 'Please check the GitHub token and try again. Error: 403 Forbidden',
okLabel: 'Go to settings',
cancelLabel: 'Close',
type: ModalTypes.confirm
})
} else {
await plugin.call('notification', 'alert', {
title: 'Error',
message: e.message
})
}
}
export const repositories = async () => {
try {
const token = await tokenWarning();
if (token) {
const repos = await plugin.call('dGitProvider' as any, 'repositories', { token });
dispatch(setRepos(repos))
}
} catch (e) {
console.log(e)
plugin.call('notification', 'alert', {
title: 'Error getting repositories',
message: `${e.message}: Please check your GitHub token in the GitHub settings.`
})
}
}
export const remoteBranches = async (owner: string, repo: string) => {
try {
const token = await tokenWarning();
if (token) {
const branches = await plugin.call('dGitProvider' as any, 'remotebranches', { token, owner, repo });
dispatch(setRemoteBranches(branches))
}
} catch (e) {
console.log(e)
plugin.call('notification', 'alert', {
title: 'Error',
message: e.message
})
}
}
export const remoteCommits = async (url: string, branch: string, length: number) => {
const urlParts = url.split("/");
console.log(urlParts, 'urlParts')
// check if it's github
if(!urlParts[urlParts.length - 3].includes('github')) {
return
}
const owner = urlParts[urlParts.length - 2];
const repo = urlParts[urlParts.length - 1].split(".")[0];
try {
const token = await tokenWarning();
if (token) {
console.log(token, owner, repo, branch, length)
const commits = await plugin.call('dGitProvider' as any, 'remotecommits', { token, owner, repo, branch, length });
console.log(commits, 'remote commits')
}
} catch (e) {
console.log(e)
plugin.call('notification', 'alert', {
title: 'Error',
message: e.message
})
}
}
export const getGitHubUser = async () => {
try {
const token = await tokenWarning();
if (token) {
const data: {
user: GitHubUser,
ratelimit: RateLimit
} = await plugin.call('dGitProvider' as any, 'getGitHubUser', { token });
console.log('GET USER"', data)
dispatch(setGitHubUser(data.user))
dispatch(setRateLimit(data.ratelimit))
}
} catch (e) {
console.log(e)
}
}
export const statusMatrix = async (filepaths: string[]) => {
const matrix = await plugin.call("dGitProvider", "status", { ref: "HEAD", filepaths: filepaths || ['.'] });
const result = (matrix || []).map((x) => {
return {
filename: `/${x.shift()}`,
status: x,
};
});
return result;
}
export const diffFiles = async (filename: string | undefined) => {
}
export const resolveRef = async (ref: string) => {
const oid = await plugin.call("dGitProvider", "resolveref", {
ref,
});
return oid;
}
export const diff = async (commitChange: commitChange) => {
if (!commitChange.hashModified) {
const newcontent = await plugin.call(
"fileManager",
"readFile",//
removeSlash(commitChange.path)
);
commitChange.modified = newcontent;
commitChange.readonly = false;
} else {
try {
const modifiedContentReadBlobResult: ReadBlobResult = await plugin.call("dGitProvider", "readblob", {
oid: commitChange.hashModified,
filepath: removeSlash(commitChange.path),
});
const modifiedContent = Buffer.from(modifiedContentReadBlobResult.blob).toString("utf8");
console.log(modifiedContent)
commitChange.modified = modifiedContent;
commitChange.readonly = true;
} catch (e) {
commitChange.modified = "";
}
}
try {
const originalContentReadBlobResult: ReadBlobResult = await plugin.call("dGitProvider", "readblob", {
oid: commitChange.hashOriginal,
filepath: removeSlash(commitChange.path),
});
const originalContent = Buffer.from(originalContentReadBlobResult.blob).toString("utf8");
console.log(originalContent)
commitChange.original = originalContent;
} catch (e) {
commitChange.original = "";
}
/*
const fullfilename = args; // $(args[0].currentTarget).data('file')
try {
const commitOid = await client.call(
"dGitProvider",
"resolveref",
{ ref: "HEAD" }
);
const { blob } = await client.call("dGitProvider", "readblob", {
oid: commitOid,
filepath: removeSlash(fullfilename),
});
const newcontent = await client.call(
"fileManager",
"readFile",//
removeSlash(fullfilename)
);
// Utils.log(original);
//Utils.log(newcontent);
//const filediff = createPatch(filename, original, newcontent); // diffLines(original,newcontent)
////Utils.log(filediff)
const filediff: diffObject = {
originalFileName: fullfilename,
updatedFileName: fullfilename,
current: newcontent,
past: original,
};
return filediff;
} catch (e) {
const filediff: diffObject = {
originalFileName: "",
updatedFileName: "",
current: "",
past: "",
};
return filediff;
}
*/
}
export const getCommitChanges = async (oid1: string, oid2: string) => {
const result: commitChange[] = await plugin.call('dGitProvider', 'getCommitChanges', oid1, oid2)
dispatch(setCommitChanges(result))
return result
}
export const fetchBranch = async (branch: branch) => {
const r = await plugin.call('dGitProvider', 'fetch', {
ref: branch.name,
remoteRef: branch.name,
singleBranch: true,
remote: branch.remote.remote,
depth: 10
})
const commits: ReadCommitResult[] = await plugin.call('dGitProvider', 'log', {
ref: r.fetchHead
})
console.log(r, commits)
dispatch(setBranchCommits({ branch, commits }))
}
export const getBranchCommits = async (branch: branch) => {
try {
console.log(branch)
const commits: ReadCommitResult[] = await plugin.call('dGitProvider', 'log', {
ref: branch.name,
})
console.log(commits)
dispatch(setBranchCommits({ branch, commits }))
return commits
} catch (e) {
console.log(e)
await fetchBranch(branch)
}
}

@ -0,0 +1,192 @@
import { ViewPlugin } from "@remixproject/engine-web";
import React from "react";
import { setCanUseApp, setLoading, setRepoName } from "../state/gitpayload";
import { gitActionDispatch } from "../types";
import { diffFiles, getBranches, getFileStatusMatrix, getGitHubUser, getRemotes, gitlog, setPlugin } from "./gitactions";
let plugin: ViewPlugin, gitDispatch: React.Dispatch<gitActionDispatch>, loaderDispatch: React.Dispatch<any>
let callBackEnabled: boolean = false
let syncTimer: NodeJS.Timer = null
export const setCallBacks = (viewPlugin: ViewPlugin, gitDispatcher: React.Dispatch<gitActionDispatch>, loaderDispatcher: React.Dispatch<any>) => {
plugin = viewPlugin
gitDispatch = gitDispatcher
loaderDispatch = loaderDispatcher
setPlugin(viewPlugin, gitDispatcher)
plugin.on("fileManager", "fileSaved", async (file: string) => {
console.log(file)
loadFiles([file])
//await synTimerStart();
});
plugin.on('dGitProvider', 'checkout' as any, async () => {
await synTimerStart();
})
plugin.on('dGitProvider', 'branch' as any, async () => {
await synTimerStart();
})
plugin.on("fileManager", "fileAdded", async (e) => {
await synTimerStart();
});
plugin.on("fileManager", "fileRemoved", async (e) => {
await synTimerStart();
});
plugin.on("fileManager", "currentFileChanged", async (e) => {
console.log('current file change', e)
//await synTimerStart();
});
plugin.on("fileManager", "fileRenamed", async (oldfile, newfile) => {
await synTimerStart();
});
plugin.on("filePanel", "setWorkspace", async (x: any) => {
await synTimerStart();
});
plugin.on("filePanel", "deleteWorkspace" as any, async (x: any) => {
await synTimerStart();
});
plugin.on("filePanel", "renameWorkspace" as any, async (x: any) => {
await synTimerStart();
});
plugin.on('dGitProvider', 'checkout', async () => {
await loadFiles();
})
plugin.on('dGitProvider', 'init', async () => {
await loadFiles();
})
plugin.on('dGitProvider', 'add', async () => {
await loadFiles();
})
plugin.on('dGitProvider', 'rm', async () => {
await loadFiles();
})
plugin.on('dGitProvider', 'commit', async () => {
await loadFiles();
})
plugin.on('dGitProvider', 'branch', async () => {
await loadFiles();
})
plugin.on('dGitProvider', 'clone', async () => {
await loadFiles();
})
plugin.on('manager', 'pluginActivated', async (p: Plugin) => {
if (p.name === 'dGitProvider') {
getGitHubUser();
plugin.off('manager', 'pluginActivated');
}
})
plugin.on('config', 'configChanged', async () => {
await getGitConfig()
})
plugin.on('settings', 'configChanged', async () => {
await getGitConfig()
})
callBackEnabled = true;
}
export const getGitConfig = async () => {
const username = await plugin.call('settings', 'get', 'settings/github-user-name')
const email = await plugin.call('settings', 'get', 'settings/github-email')
const token = await plugin.call('settings', 'get', 'settings/gist-access-token')
const config = { username, email, token }
//dispatch(setGitConfig(config))
return config
}
const syncFromWorkspace = async (isLocalhost = false) => {
//gitDispatch(setLoading(true));
await disableCallBacks();
if (isLocalhost) {
gitDispatch(setCanUseApp(false));
gitDispatch(setLoading(false));
await enableCallBacks();
return;
}
try {
const workspace = await plugin.call(
"filePanel",
"getCurrentWorkspace"
);
if (workspace.isLocalhost) {
gitDispatch(setCanUseApp(false));
await enableCallBacks();
return
}
gitDispatch(setRepoName(workspace.name));
gitDispatch(setCanUseApp(true));
} catch (e) {
gitDispatch(setCanUseApp(false));
}
await loadFiles();
await enableCallBacks();
}
export const loadFiles = async (filepaths: string[] = null) => {
//gitDispatch(setLoading(true));
try {
await getFileStatusMatrix(filepaths);
} catch (e) {
// TODO: handle error
console.error(e);
}
try {
await gitlog();
} catch (e) { }
try {
await getBranches();
} catch (e) { }
try {
await getRemotes();
} catch (e) { }
try {
//await getStorageUsed();
} catch (e) { }
try {
//await diffFiles('');
} catch (e) { }
//gitDispatch(setLoading(false));
}
const getStorageUsed = async () => {
try {
const storageUsed = await plugin.call("storage" as any, "getStorage" as any);
} catch (e) {
const storage: string = await plugin.call("dGitProvider", "localStorageUsed" as any);
const storageUsed = {
usage: parseFloat(storage) * 1000,
quota: 10000000,
};
}
}
export const disableCallBacks = async () => {
callBackEnabled = false;
}
export const enableCallBacks = async () => {
callBackEnabled = true;
}
const synTimerStart = async () => {
if (!callBackEnabled) return
clearTimeout(syncTimer)
syncTimer = setTimeout(async () => {
await syncFromWorkspace();
}, 1000)
}

@ -0,0 +1,104 @@
import { ViewPlugin } from "@remixproject/engine-web"
import { commitChange, fileStatusResult, gitActionDispatch, gitState } from "../types"
import { fileDecoration, fileDecorationType } from "@remix-ui/file-decorators"
import { removeSlash } from "../utils"
import path from "path"
import { getFilesByStatus, getFilesWithNotModifiedStatus } from "./fileHelpers"
let plugin: ViewPlugin, gitDispatch: React.Dispatch<gitActionDispatch>, loaderDispatch: React.Dispatch<any>
export const setPlugin = (p: ViewPlugin, gitDispatcher: React.Dispatch<gitActionDispatch>, loaderDispatcher: React.Dispatch<any>) => {
plugin = p
gitDispatch = gitDispatcher
loaderDispatch = loaderDispatcher
}
export const statusChanged = (badges: number) => {
if(!plugin) return
plugin.emit('statusChanged', {
key: badges === 0 ? 'none' : badges,
type: badges === 0 ? '' : 'success',
title: 'Git changes'
})
}
export const openFile = async (path: string) => {
if(!plugin) return
await plugin.call('fileManager', 'open', path)
}
export const openDiff = async (change: commitChange) => {
console.log('openDiff', change)
if(!plugin) return
plugin.call('fileManager', 'diff', change)
}
export const saveToken = async (token: string) => {
if(!plugin) return
await plugin.call('config', 'setAppParameter', 'settings/gist-access-token', token)
}
export const setFileDecorators = async (files: fileStatusResult[]) => {
if(!plugin) return
const modified = getFilesByStatus('modified', files)
const untracked = getFilesByStatus('untracked', files)
const unmodified = getFilesByStatus('unmodified', files)
await setModifiedDecorator(modified)
await setUntrackedDecorator(untracked)
unmodified.forEach((file) => {
clearFileDecorator(removeSlash(file.filename))
})
}
export const setModifiedDecorator = async (files: fileStatusResult[]) => {
const decorators: fileDecoration[] = []
for (const file of files) {
const decorator: fileDecoration = {
path: removeSlash(file.filename),
isDirectory: false,
fileStateType: fileDecorationType.Custom,
fileStateLabelClass: 'text-warning',
fileStateIconClass: 'text-warning',
fileStateIcon: '',
text: 'M',
owner: 'git',
bubble: true,
comment: 'Modified'
}
decorators.push(decorator)
}
await plugin.call('fileDecorator', 'setFileDecorators', decorators)
}
export const setUntrackedDecorator = async (files: fileStatusResult[]) => {
const decorators: fileDecoration[] = []
for (const file of files) {
const decorator: fileDecoration = {
path: removeSlash(file.filename),
isDirectory: false,
fileStateType: fileDecorationType.Custom,
fileStateLabelClass: 'text-success',
fileStateIconClass: 'text-success',
fileStateIcon: '',
text: 'U',
owner: 'git',
bubble: true,
comment: 'Untracked'
}
decorators.push(decorator)
}
await plugin.call('fileDecorator', 'setFileDecorators', decorators)
}
export const clearFileDecorator = async(path: string) => {
await plugin.call('fileDecorator', 'clearFileDecorators', path)
}

@ -0,0 +1,39 @@
import { ReadCommitResult } from "isomorphic-git"
import React from "react"
import { branch, commitChange } from "../types"
export interface gitActions {
clone(url: string, path: string, depth: number, singleBranch: boolean): Promise<void>
add(path: string): Promise<void>
rm(path: string): Promise<void>
commit(message: string): Promise<any>
addall(): Promise<void>
//push(): Promise<void>
//pull(): Promise<void>
//fetch(): Promise<void>
repositories(): Promise<any>
checkoutfile(file: string): Promise<void>
checkout(cmd: any): Promise<void>
createBranch(branch: string): Promise<void>
remoteBranches(owner: string, repo: string): Promise<any>
getCommitChanges(oid1: string, oid2: string): Promise<commitChange[]>
getBranchCommits(branch: branch): Promise<ReadCommitResult[]>
getGitHubUser(): Promise<any>
diff(commitChange: commitChange): Promise<void>
resolveRef(ref: string): Promise<string>
setUpstreamRemote(upstream: string): Promise<void>
getBranches: () => Promise<void>
getRemotes: () => Promise<void>
}
export const gitActionsContext = React.createContext<gitActions>(null)
export interface pluginActions {
statusChanged(data: any): void
loadFiles(): void
openFile(path: string): Promise<void>
openDiff(change: commitChange): Promise<void>
saveToken(token: string): Promise<void>
}
export const pluginActionsContext = React.createContext<pluginActions>(null)

@ -0,0 +1,130 @@
import { ReadCommitResult } from "isomorphic-git"
import { GitHubUser, branch, commitChange, fileStatusResult, remote } from "../types"
import { Endpoints } from "@octokit/types"
export const fileStatus = (files: fileStatusResult[]) => {
return {
type: 'FILE_STATUS',
payload: files
}
}
export const fileStatusMerge = (files: fileStatusResult[]) => {
return {
type: 'FILE_STATUS_MERGE',
payload: files
}
}
export const setCommits = (commits: ReadCommitResult[]) => {
return {
type: 'SET_COMMITS',
payload: commits
}
}
export const setBranches = (branches: any[]) => {
return {
type: 'SET_BRANCHES',
payload: branches
}
}
export const setRepos = (repos: any[]) => {
return {
type: 'SET_REPOS',
payload: repos
}
}
export const setRemoteBranches = (branches: any[]) => {
return {
type: 'SET_REMOTE_BRANCHES',
payload: branches
}
}
export const setGitHubUser = (user: any) => {
return {
type: 'SET_GITHUB_USER',
payload: user
}
}
export const setRateLimit = (rateLimit: any) => {
return {
type: 'SET_RATE_LIMIT',
payload: rateLimit
}
}
export const setGitHubAccessToken = (token: string) => {
return {
type: 'SET_GITHUB_ACCESS_TOKEN',
payload: token
}
}
export const setLoading = (loading: boolean) => {
return {
type: 'SET_LOADING',
payload: loading
}
}
export const setCanUseApp = (canUseApp: boolean) => {
return {
type: 'SET_CAN_USE_APP',
payload: canUseApp
}
}
export const setRepoName = (reponame: string) => {
return {
type: 'SET_REPO_NAME',
payload: reponame
}
}
export const setCurrentBranch = (currentBranch: branch) => {
return {
type: 'SET_CURRENT_BRANCH',
payload: currentBranch
}
}
export const setCanCommit = (canCommit: boolean) => {
return {
type: 'SET_CAN_COMMIT',
payload: canCommit
}
}
export const setRemotes = (remotes: remote[]) => {
return {
type: 'SET_REMOTES',
payload: remotes
}
}
export const setUpstream = (upstream: string) => {
return {
type: 'SET_UPSTREAM',
payload: upstream
}
}
export const setCommitChanges = (commitChanges: commitChange[]) => {
return {
type: 'SET_COMMIT_CHANGES',
payload: commitChanges
}
}
export const setBranchCommits =({branch, commits}) => {
return {
type: 'SET_BRANCH_COMMITS',
payload: { branch, commits }
}
}

@ -0,0 +1,147 @@
import { ReadCommitResult } from "isomorphic-git"
import { allChangedButNotStagedFiles, getFilesByStatus, getFilesWithNotModifiedStatus } from "../lib/fileHelpers"
import { branch, commitChange, defaultGitState, fileStatusResult, gitState, setBranchCommitsAction } from "../types"
interface Action {
type: string
payload: any
}
export const gitReducer = (state: gitState = defaultGitState, action: Action): gitState => {
///console.log(action, state)
switch (action.type) {
case 'FILE_STATUS':
return {
...state,
fileStatusResult: action.payload,
staged: getFilesByStatus("staged", action.payload),
modified: getFilesByStatus("modified", action.payload),
untracked: getFilesByStatus("untracked", action.payload),
deleted: getFilesByStatus("deleted", action.payload),
allchangesnotstaged: allChangedButNotStagedFiles(action.payload)
}
case 'FILE_STATUS_MERGE':
const filesStatusResults: fileStatusResult[] = action.payload
filesStatusResults.map((fileStatusResult: fileStatusResult) => {
const file = state.fileStatusResult.find( stateFile => {
return stateFile.filename === fileStatusResult.filename
})
if(file){
file.status = fileStatusResult.status
file.statusNames = fileStatusResult.statusNames
}
})
return {...state,
staged: getFilesByStatus("staged", state.fileStatusResult),
modified: getFilesByStatus("modified", state.fileStatusResult),
untracked: getFilesByStatus("untracked", state.fileStatusResult),
deleted: getFilesByStatus("deleted", state.fileStatusResult),
allchangesnotstaged: allChangedButNotStagedFiles(state.fileStatusResult)
}
case 'SET_COMMITS':
return {
...state,
commits: action.payload,
localCommitCount: action.payload.length
}
case 'SET_BRANCHES':
return {
...state,
branches: action.payload
}
case 'SET_CURRENT_BRANCH':
return {
...state,
currentBranch: action.payload
}
case 'SET_CAN_USE_APP':
return {
...state,
canUseApp: action.payload
}
case 'SET_REPO_NAME':
return {
...state,
reponame: action.payload
}
case 'SET_LOADING':
return {
...state,
loading: action.payload
}
case 'SET_REPOS':
return {
...state,
repositories: action.payload
}
case 'SET_REMOTE_BRANCHES':
return {
...state,
remoteBranches: action.payload
}
case 'SET_CAN_COMMIT':
return {
...state,
canCommit: action.payload
}
case 'SET_REMOTES':
return {
...state,
remotes: action.payload
}
case 'SET_UPSTREAM':
return {
...state,
upstream: action.payload
}
case 'SET_COMMIT_CHANGES':
action.payload.forEach((change: commitChange) => {
state.commitChanges.find((c) => c.hashModified === change.hashModified && c.hashOriginal === change.hashOriginal && c.path === change.path) ? null : state.commitChanges.push(change)
})
return {
...state,
commitChanges: [...state.commitChanges]
}
case 'SET_BRANCH_COMMITS':
state.branchCommits[(action as setBranchCommitsAction).payload.branch.name] = (action as setBranchCommitsAction).payload.commits
return {
...state,
branchCommits: {...state.branchCommits}
}
case 'SET_GITHUB_USER':
return {
...state,
gitHubUser: action.payload
}
case 'SET_RATE_LIMIT':
console.log("rate limit", action.payload)
return {
...state,
rateLimit: action.payload
}
case 'SET_GITHUB_ACCESS_TOKEN':
return {
...state,
gitHubAccessToken: action.payload
}
}
}

@ -0,0 +1,11 @@
import { defaultLoaderState, loaderState } from "../types";
interface Action {
type: string
payload: any
}
export const loaderReducer = (state: loaderState = defaultLoaderState, action: Action): loaderState => {
state[action.type] = action.payload
return state
}

@ -0,0 +1,36 @@
.nav {
cursor: pointer;
}
.pointer {
cursor: pointer;
}
.long-and-truncated {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.commit-navigation{
align-items: center;
}
.commit-navigation:hover {
background-color: var(--custom-select);
}
.commitdetailsitem {
align-items: baseline;
}
.gitfile:hover {
background-color : var(--custom-select);
}
hr {
background-color: var(--custom-select);
}

@ -0,0 +1,214 @@
import { Endpoints } from "@octokit/types"
import { CommitObject, ReadCommitResult } from "isomorphic-git"
export type GitHubUser = Endpoints["GET /user"]["response"]['data']
export type RateLimit = Endpoints["GET /rate_limit"]["response"]["data"]
export type gitState = {
currentBranch: branch
commits: ReadCommitResult[]
branch: string
canCommit: boolean
branches: branch[]
remotes: remote[]
fileStatusResult: fileStatusResult[]
canUseApp: boolean
loading: boolean
storageUsed: any
reponame: string
staged: fileStatusResult[]
untracked: fileStatusResult[]
deleted: fileStatusResult[]
modified: fileStatusResult[]
allchangesnotstaged: fileStatusResult[],
repositories: repository[]
remoteBranches: remoteBranch[]
commitChanges: commitChange[]
branchCommits: Record<string, ReadCommitResult[]>
syncStatus: syncStatus,
localCommitCount: number
remoteCommitCount: number
upstream: string
gitHubUser: GitHubUser
rateLimit: RateLimit
gitHubAccessToken: string
}
export type loaderState = {
branches: boolean
remotes: boolean
commits: boolean
sourcecontrol: boolean
}
export type commitChangeTypes = {
"deleted": "D"
"modified": "M"
"added": "A",
"unknown": "?"
}
export enum syncStatus {
"sync" = "sync",
"publishBranch" = "publishBranch",
"none" = "none",
}
export type commitChangeType = keyof commitChangeTypes
export type commitChange = {
type: commitChangeType
path: string,
hashModified : string,
hashOriginal : string,
original?: string,
modified?: string,
readonly?: boolean
}
export type repository = {
name: string
html_url: string
owner: {
login: string
},
full_name: string
default_branch: string
id: number
}
export type branch = {
name: string
remote: remote
}
export type remote = {
remote: string
url: string
}
export type remoteBranch = {
name: string
}
export const defaultGitState: gitState = {
currentBranch: { name: "", remote: { remote: "", url: "" } },
commits: [],
branch: "",
canCommit: true,
branches: [],
remotes: [],
fileStatusResult: [],
staged: [],
untracked: [],
deleted: [],
modified: [],
allchangesnotstaged: [],
canUseApp: false,
loading: false,
storageUsed: {},
reponame: "",
repositories: [],
remoteBranches: [],
commitChanges: [],
branchCommits: {},
syncStatus: syncStatus.none,
localCommitCount: 0,
remoteCommitCount: 0,
upstream: "",
gitHubUser: {} as GitHubUser,
rateLimit: {} as RateLimit,
gitHubAccessToken: ""
}
export const defaultLoaderState: loaderState = {
branches: false,
commits: false,
sourcecontrol: false,
remotes: false
}
export type fileStatusResult = {
filename:string,
status?: fileStatus
statusNames?:string[]
}
export type fileStatus =[string, 0 | 1, 0 | 1 | 2, 0 | 1 | 2 | 3]
export type statusMatrixType = { matrix: string[] | undefined; status: string[] }
export type sourceControlGroup = {
group: fileStatusResult[],
name: string
}
export interface fileStatusAction {
type: string,
payload: fileStatusResult[]
}
export interface setCommitsAction {
type: string,
payload: ReadCommitResult[]
}
export interface setBranchesAction {
type: string,
payload: any[]
}
export interface setReposAction {
type: string,
payload: any[]
}
export interface setRemoteBranchesAction {
type: string,
payload: any[]
}
export interface setGitHubUserAction {
type: string,
payload: any
}
export interface setLoadingAction {
type: string,
payload: boolean
}
export interface setCanUseAppAction {
type: string,
payload: boolean
}
export interface setRepoNameAction {
type: string,
payload: string
}
export interface setCurrentBranchAction {
type: string,
payload: branch
}
export interface setRemotesAction {
type: string,
payload: remote[]
}
export interface setUpstreamAction {
type: string,
payload: string
}
export interface setBranchCommitsAction {
type: string,
payload: {
branch: branch,
commits: ReadCommitResult[]
}
}
export type gitActionDispatch = setUpstreamAction | setBranchCommitsAction | setRemotesAction | setCurrentBranchAction | fileStatusAction | setLoadingAction | setCanUseAppAction | setRepoNameAction | setCommitsAction | setBranchesAction | setReposAction | setRemoteBranchesAction

@ -0,0 +1,53 @@
import { StylesConfig } from 'react-select'
export const selectStyles: StylesConfig = {
option: (baseStyles, state) => {
return {
...baseStyles,
color: 'var(--text)',
}
},
input(base, props) {
return {
...base,
color: 'var(--text)',
}
},
singleValue: (baseStyles, state) => {
return {
...baseStyles,
color: 'var(--text)',
}
},
control: (baseStyles, state) => ({
...baseStyles,
color: 'var(--text)',
backgroundColor: 'var(--custom-select)',
border: 'none',
}),
menu: (baseStyles, state) => {
return {
...baseStyles,
backgroundColor: 'var(--custom-select)',
color: 'var(--text)',
}
},
menuList: (baseStyles, props) => {
return {
...baseStyles,
backgroundColor: 'var(--custom-select)',
color: 'var(--text)',
}
},
}
export const selectTheme = (theme) => ({
...theme,
borderRadius: 0,
colors: {
...theme.colors,
primary25: 'var(--primary)',
primary: 'var(--primary)',
primary50: 'var(--primary)',
primary75: 'var(--primary)',
},
})

@ -0,0 +1,3 @@
export const removeSlash = (s: string) => {
return s.replace(/^\/+/, "");
};
Loading…
Cancel
Save