From 32a558a16ceaa21b3c244d11eabd6d0057dc460c Mon Sep 17 00:00:00 2001 From: tiddlygit-test Date: Tue, 5 Jan 2021 23:03:16 +0800 Subject: [PATCH] refactor: GitService --- .vscode/settings.json | 1 + src/services/libs/git/github.ts | 43 +++ src/services/libs/git/index.ts | 241 +++++++++++- src/services/libs/git/inspect.ts | 16 +- src/services/libs/git/sync.ts | 345 +++--------------- .../libs/i18n/i18next-electron-fs-backend.ts | 1 + 6 files changed, 350 insertions(+), 297 deletions(-) create mode 100644 src/services/libs/git/github.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index e15bbdb7..4d699c47 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "cSpell.words": [ + "dugite", "fullscreenable", "maximizable", "minimizable", diff --git a/src/services/libs/git/github.ts b/src/services/libs/git/github.ts new file mode 100644 index 00000000..58fb9a49 --- /dev/null +++ b/src/services/libs/git/github.ts @@ -0,0 +1,43 @@ +import { compact, trim } from 'lodash'; +import { GitProcess } from 'dugite'; + +import { IUserInfo } from '@/services/types'; + +const getGitUrlWithCredential = (rawUrl: string, username: string, accessToken: string): string => + trim(`${rawUrl}.git`.replace(/\n/g, '').replace('https://github.com/', `https://${username}:${accessToken}@github.com/`)); +const getGitUrlWithOutCredential = (urlWithCredential: string): string => trim(urlWithCredential.replace(/.+@/, 'https://')); + +export async function getRemoteUrl(wikiFolderPath: string): Promise { + const { stdout: remoteStdout } = await GitProcess.exec(['remote'], wikiFolderPath); + const remotes = compact(remoteStdout.split('\n')); + const githubRemote = remotes.find((remote) => remote === 'origin') ?? remotes[0] ?? ''; + if (githubRemote.length > 0) { + const { stdout: remoteUrlStdout } = await GitProcess.exec(['remote', 'get-url', githubRemote], wikiFolderPath); + return remoteUrlStdout.replace('.git', ''); + } + return ''; +} + +/** + * Add remote with credential + * @param {string} wikiFolderPath + * @param {string} githubRepoUrl + * @param {{ login: string, email: string, accessToken: string }} userInfo + */ +export async function credentialOn(wikiFolderPath: string, githubRepoUrl: string, userInfo: IUserInfo): Promise { + const { login: username, accessToken } = userInfo; + const gitUrlWithCredential = getGitUrlWithCredential(githubRepoUrl, username, accessToken); + await GitProcess.exec(['remote', 'add', 'origin', gitUrlWithCredential], wikiFolderPath); + await GitProcess.exec(['remote', 'set-url', 'origin', gitUrlWithCredential], wikiFolderPath); +} +/** + * Add remote without credential + * @param {string} wikiFolderPath + * @param {string} githubRepoUrl + * @param {{ login: string, email: string, accessToken: string }} userInfo + */ +export async function credentialOff(wikiFolderPath: string): Promise { + const githubRepoUrl = await getRemoteUrl(wikiFolderPath); + const gitUrlWithOutCredential = getGitUrlWithOutCredential(githubRepoUrl); + await GitProcess.exec(['remote', 'set-url', 'origin', gitUrlWithOutCredential], wikiFolderPath); +} diff --git a/src/services/libs/git/index.ts b/src/services/libs/git/index.ts index 5d178304..bbd96968 100644 --- a/src/services/libs/git/index.ts +++ b/src/services/libs/git/index.ts @@ -1 +1,240 @@ -export { initWikiGit, commitAndSync, getRemoteUrl, clone } from './sync'; +import { ipcMain } from 'electron'; +import { injectable, inject } from 'inversify'; +import { truncate } from 'lodash'; +import { GitProcess } from 'dugite'; +import isDev from 'electron-is-dev'; + +import * as gitSync from './sync'; +import * as github from './github'; +import serviceIdentifiers from '@services/serviceIdentifier'; +import { View } from '@services/view'; +import { logger } from '@/services/libs/log'; +import i18n from '@/services/libs/i18n'; +import { IUserInfo } from '@/services/types'; + +/** + * System Preferences are not stored in storage but stored in macOS Preferences. + * It can be retrieved and changed using Electron APIs + */ +@injectable() +export class Git { + disableSyncOnDevelopment = true; + + constructor(@inject(serviceIdentifiers.View) private readonly viewService: View) { + this.init(); + } + + init(): void {} + + /** + * + * @param {string} githubRepoName similar to "linonetwo/wiki", string after "https://github.com/" + */ + public async updateGitInfoTiddler(githubRepoName: string): Promise { + const browserView = this.viewService.getActiveBrowserView(); + if (browserView !== undefined) { + const tiddlerText = await new Promise((resolve) => { + browserView.webContents.send('wiki-get-tiddler-text', '$:/GitHub/Repo'); + ipcMain.once('wiki-get-tiddler-text-done', (_, value) => resolve(value)); + }); + if (tiddlerText !== githubRepoName) { + await new Promise((resolve) => { + browserView.webContents.send('wiki-add-tiddler', '$:/GitHub/Repo', githubRepoName, { + type: 'text/vnd.tiddlywiki', + }); + ipcMain.once('wiki-add-tiddler-done', () => resolve()); + }); + } + return; + } + logger.error('no browserView in updateGitInfoTiddler'); + } + + /** + * + * @param {string} wikiFolderPath + * @param {string} githubRepoUrl + * @param {{ login: string, email: string, accessToken: string }} userInfo + * @param {boolean} isMainWiki + * @param {{ info: Function, notice: Function }} logger Logger instance from winston + */ + public async initWikiGit(wikiFolderPath: string, githubRepoUrl: string, userInfo: IUserInfo, isMainWiki: boolean): Promise { + const logProgress = (message: string): unknown => logger.notice(message, { handler: 'createWikiProgress', function: 'initWikiGit' }); + const logInfo = (message: string): unknown => logger.info(message, { function: 'initWikiGit' }); + + logProgress(i18n.t('Log.StartGitInitialization')); + const { login: username, email, accessToken } = userInfo; + logInfo( + `Using gitUrl ${githubRepoUrl} with username ${username} and accessToken ${truncate(accessToken, { + length: 24, + })}`, + ); + await GitProcess.exec(['init'], wikiFolderPath); + await gitSync.commitFiles(wikiFolderPath, username, email); + logProgress(i18n.t('Log.StartConfiguringGithubRemoteRepository')); + await github.credentialOn(wikiFolderPath, githubRepoUrl, userInfo); + logProgress(i18n.t('Log.StartBackupToGithubRemote')); + const defaultBranchName = await gitSync.getDefaultBranchName(wikiFolderPath); + const { stderr: pushStdError, exitCode: pushExitCode } = await GitProcess.exec( + ['push', 'origin', `${defaultBranchName}:${defaultBranchName}`], + wikiFolderPath, + ); + await github.credentialOff(wikiFolderPath); + if (isMainWiki && pushExitCode !== 0) { + logInfo(pushStdError); + const CONFIG_FAILED_MESSAGE = i18n.t('Log.GitRepositoryConfigurateFailed'); + logProgress(CONFIG_FAILED_MESSAGE); + throw new Error(CONFIG_FAILED_MESSAGE); + } else { + logProgress(i18n.t('Log.GitRepositoryConfigurationFinished')); + } + } + + /** + * + * @param {string} wikiFolderPath + * @param {string} githubRepoUrl + * @param {{ login: string, email: string, accessToken: string }} userInfo + */ + public async commitAndSync(wikiFolderPath: string, githubRepoUrl: string, userInfo: IUserInfo): Promise { + /** functions to send data to main thread */ + const logProgress = (message: string): unknown => + logger.notice(message, { handler: 'wikiSyncProgress', function: 'commitAndSync', wikiFolderPath, githubRepoUrl }); + const logInfo = (message: string): unknown => logger.info(message, { function: 'commitAndSync', wikiFolderPath, githubRepoUrl }); + + if (this.disableSyncOnDevelopment && isDev) { + return; + } + const { login: username, email } = userInfo; + const commitMessage = 'Wiki updated with TiddlyGit-Desktop'; + const defaultBranchName = await gitSync.getDefaultBranchName(wikiFolderPath); + const branchMapping = `${defaultBranchName}:${defaultBranchName}`; + // update git info tiddler for plugins to use, for example, linonetwo/github-external-image + let wikiRepoName = new URL(githubRepoUrl).pathname; + if (wikiRepoName.startsWith('/')) { + wikiRepoName = wikiRepoName.replace('/', ''); + } + if (wikiRepoName.length > 0) { + await this.updateGitInfoTiddler(wikiRepoName); + } + // preflight check + const repoStartingState = await gitSync.getGitRepositoryState(wikiFolderPath, logInfo, logProgress); + if (repoStartingState.length > 0 || repoStartingState === '|DIRTY') { + const SYNC_MESSAGE = i18n.t('Log.PrepareSync'); + logProgress(SYNC_MESSAGE); + logInfo(`${SYNC_MESSAGE} ${wikiFolderPath} , ${username} <${email}>`); + } else if (repoStartingState === 'NOGIT') { + const CANT_SYNC_MESSAGE = i18n.t('Log.CantSyncGitNotInitialized'); + logProgress(CANT_SYNC_MESSAGE); + throw new Error(CANT_SYNC_MESSAGE); + } else { + // we may be in middle of a rebase, try fix that + await gitSync.continueRebase(wikiFolderPath, username, email, logInfo, logProgress); + } + if (await gitSync.haveLocalChanges(wikiFolderPath)) { + const SYNC_MESSAGE = i18n.t('Log.HaveThingsToCommit'); + logProgress(SYNC_MESSAGE); + logInfo(`${SYNC_MESSAGE} ${commitMessage}`); + const { exitCode: commitExitCode, stderr: commitStdError } = await gitSync.commitFiles(wikiFolderPath, username, email, commitMessage); + if (commitExitCode !== 0) { + logInfo('commit failed'); + logInfo(commitStdError); + } + logProgress(i18n.t('Log.CommitComplete')); + } + logProgress(i18n.t('Log.PreparingUserInfo')); + await github.credentialOn(wikiFolderPath, githubRepoUrl, userInfo); + logProgress(i18n.t('Log.FetchingData')); + await GitProcess.exec(['fetch', 'origin', defaultBranchName], wikiFolderPath); + // + switch (await gitSync.getSyncState(wikiFolderPath, logInfo)) { + case 'noUpstream': { + logProgress(i18n.t('Log.CantSyncGitNotInitialized')); + await github.credentialOff(wikiFolderPath); + return; + } + case 'equal': { + logProgress(i18n.t('Log.NoNeedToSync')); + await github.credentialOff(wikiFolderPath); + return; + } + case 'ahead': { + logProgress(i18n.t('Log.LocalAheadStartUpload')); + const { exitCode, stderr } = await GitProcess.exec(['push', 'origin', branchMapping], wikiFolderPath); + if (exitCode === 0) { + break; + } + logProgress(i18n.t('Log.GitPushFailed')); + logInfo(`exitCode: ${exitCode}, stderr of git push:`); + logInfo(stderr); + break; + } + case 'behind': { + logProgress(i18n.t('Log.LocalStateBehindSync')); + const { exitCode, stderr } = await GitProcess.exec(['merge', '--ff', '--ff-only', `origin/${defaultBranchName}`], wikiFolderPath); + if (exitCode === 0) { + break; + } + logProgress(i18n.t('Log.GitMergeFailed')); + logInfo(`exitCode: ${exitCode}, stderr of git merge:`); + logInfo(stderr); + break; + } + case 'diverged': { + logProgress(i18n.t('Log.LocalStateDivergeRebase')); + const { exitCode } = await GitProcess.exec(['rebase', `origin/${defaultBranchName}`], wikiFolderPath); + if ( + exitCode === 0 && + (await gitSync.getGitRepositoryState(wikiFolderPath, logInfo, logProgress)).length === 0 && + (await gitSync.getSyncState(wikiFolderPath, logInfo)) === 'ahead' + ) { + logProgress(i18n.t('Log.RebaseSucceed')); + } else { + await gitSync.continueRebase(wikiFolderPath, username, email, logInfo, logProgress); + logProgress(i18n.t('Log.RebaseConflictNeedsResolve')); + } + await GitProcess.exec(['push', 'origin', branchMapping], wikiFolderPath); + break; + } + default: { + logProgress(i18n.t('Log.SyncFailedSystemError')); + } + } + await github.credentialOff(wikiFolderPath); + logProgress(i18n.t('Log.PerformLastCheckBeforeSynchronizationFinish')); + await gitSync.assumeSync(wikiFolderPath, logInfo, logProgress); + logProgress(i18n.t('Log.SynchronizationFinish')); + } + + public async clone(githubRepoUrl: string, repoFolderPath: string, userInfo: IUserInfo): Promise { + const logProgress = (message: string): unknown => logger.notice(message, { handler: 'createWikiProgress', function: 'clone' }); + const logInfo = (message: string): unknown => logger.info(message, { function: 'clone' }); + logProgress(i18n.t('Log.PrepareCloneOnlineWiki')); + logProgress(i18n.t('Log.StartGitInitialization')); + const { login: username, accessToken } = userInfo; + logInfo( + i18n.t('Log.UsingUrlAndUsername', { + githubRepoUrl, + username, + accessToken: truncate(accessToken, { + length: 24, + }), + }), + ); + await GitProcess.exec(['init'], repoFolderPath); + logProgress(i18n.t('Log.StartConfiguringGithubRemoteRepository')); + await github.credentialOn(repoFolderPath, githubRepoUrl, userInfo); + logProgress(i18n.t('Log.StartFetchingFromGithubRemote')); + const defaultBranchName = await gitSync.getDefaultBranchName(repoFolderPath); + const { stderr, exitCode } = await GitProcess.exec(['pull', 'origin', `${defaultBranchName}:${defaultBranchName}`], repoFolderPath); + await github.credentialOff(repoFolderPath); + if (exitCode !== 0) { + logInfo(stderr); + const CONFIG_FAILED_MESSAGE = i18n.t('Log.GitRepositoryConfigurateFailed'); + logProgress(CONFIG_FAILED_MESSAGE); + throw new Error(CONFIG_FAILED_MESSAGE); + } else { + logProgress(i18n.t('Log.GitRepositoryConfigurationFinished')); + } + } +} diff --git a/src/services/libs/git/inspect.ts b/src/services/libs/git/inspect.ts index 95bd4d80..79c94fb9 100644 --- a/src/services/libs/git/inspect.ts +++ b/src/services/libs/git/inspect.ts @@ -1,23 +1,19 @@ import path from 'path'; import { compact } from 'lodash'; import { GitProcess } from 'dugite'; -// const { logger } = require('../log'); -// const i18n = require('../i18n'); /** * Get modified files and modify type in a folder * @param {string} wikiFolderPath location to scan git modify state */ -async function getModifiedFileList(wikiFolderPath: any) { +async function getModifiedFileList(wikiFolderPath: string): Promise> { const { stdout } = await GitProcess.exec(['status', '--porcelain'], wikiFolderPath); const stdoutLines = stdout.split('\n'); - return compact(stdoutLines) - .map((line: any) => line.match(/^\s?(\?\?|[ACMR]|[ACMR][DM])\s?(\S+)$/)) - .map(([_, type, fileRelativePath]) => ({ - type, - fileRelativePath, - filePath: path.join(wikiFolderPath, fileRelativePath), - })); + return compact(compact(stdoutLines).map((line) => /^\s?(\?\?|[ACMR]|[ACMR][DM])\s?(\S+)$/.exec(line))).map(([_, type, fileRelativePath]) => ({ + type, + fileRelativePath, + filePath: path.join(wikiFolderPath, fileRelativePath), + })); } export { getModifiedFileList }; diff --git a/src/services/libs/git/sync.ts b/src/services/libs/git/sync.ts index 63f3c0c1..b914540e 100644 --- a/src/services/libs/git/sync.ts +++ b/src/services/libs/git/sync.ts @@ -1,139 +1,76 @@ -/* eslint-disable sonarjs/no-duplicate-string */ /* eslint-disable unicorn/consistent-function-scoping */ /* eslint-disable no-await-in-loop */ import fs from 'fs-extra'; import path from 'path'; -import { compact, truncate, trim } from 'lodash'; -import { GitProcess } from 'dugite'; -import isDev from 'electron-is-dev'; -import { ipcMain } from 'electron'; -import { logger } from '../log'; -import i18n from '../i18n'; -const disableSyncOnDevelopment = true; -const getGitUrlWithCredential = (rawUrl: any, username: any, accessToken: any) => - trim(`${rawUrl}.git`.replace(/\n/g, '').replace('https://github.com/', `https://${username}:${accessToken}@github.com/`)); -const getGitUrlWithOutCredential = (urlWithCredential: any) => trim(urlWithCredential.replace(/.+@/, 'https://')); -/** - * Add remote with credential - * @param {string} wikiFolderPath - * @param {string} githubRepoUrl - * @param {{ login: string, email: string, accessToken: string }} userInfo - */ -async function credentialOn(wikiFolderPath: any, githubRepoUrl: any, userInfo: any) { - const { login: username, accessToken } = userInfo; - const gitUrlWithCredential = getGitUrlWithCredential(githubRepoUrl, username, accessToken); - await GitProcess.exec(['remote', 'add', 'origin', gitUrlWithCredential], wikiFolderPath); - await GitProcess.exec(['remote', 'set-url', 'origin', gitUrlWithCredential], wikiFolderPath); -} -/** - * Add remote without credential - * @param {string} wikiFolderPath - * @param {string} githubRepoUrl - * @param {{ login: string, email: string, accessToken: string }} userInfo - */ -async function credentialOff(wikiFolderPath: any) { - const githubRepoUrl = await getRemoteUrl(wikiFolderPath); - const gitUrlWithOutCredential = getGitUrlWithOutCredential(githubRepoUrl); - await GitProcess.exec(['remote', 'set-url', 'origin', gitUrlWithOutCredential], wikiFolderPath); -} +import { compact } from 'lodash'; +import { GitProcess, IGitResult } from 'dugite'; + +import i18n from '@/services/libs/i18n'; + /** * Get "master" or "main" from git repo - * @param {string} wikiFolderPath + * @param wikiFolderPath */ -async function getDefaultBranchName(wikiFolderPath: any) { +export async function getDefaultBranchName(wikiFolderPath: string): Promise { const { stdout } = await GitProcess.exec(['remote', 'show', 'origin'], wikiFolderPath); const lines = stdout.split('\n'); - const lineWithHEAD = lines.find((line: any) => line.includes('HEAD branch: ')); + const lineWithHEAD = lines.find((line) => line.includes('HEAD branch: ')); const branchName = lineWithHEAD?.replace('HEAD branch: ', '')?.replace(/\s/g, ''); - if (!branchName || branchName.includes('(unknown)')) { + if (branchName === undefined || branchName.includes('(unknown)')) { return 'master'; } return branchName; } /** * Git add and commit all file - * @param {string} wikiFolderPath - * @param {string} username - * @param {string} email - * @param {?string} message + * @param wikiFolderPath + * @param username + * @param email + * @param message */ -async function commitFiles(wikiFolderPath: any, username: any, email: any, message = 'Initialize with TiddlyGit-Desktop') { +export async function commitFiles(wikiFolderPath: string, username: string, email: string, message = 'Initialize with TiddlyGit-Desktop'): Promise { await GitProcess.exec(['add', '.'], wikiFolderPath); return await GitProcess.exec(['commit', '-m', message, `--author="${username} <${email}>"`], wikiFolderPath); } -/** - * - * @param {string} wikiFolderPath - * @param {string} githubRepoUrl - * @param {{ login: string, email: string, accessToken: string }} userInfo - * @param {boolean} isMainWiki - * @param {{ info: Function, notice: Function }} logger Logger instance from winston - */ -async function initWikiGit(wikiFolderPath: any, githubRepoUrl: any, userInfo: any, isMainWiki: any) { - const logProgress = (message: any) => logger.notice(message, { handler: 'createWikiProgress', function: 'initWikiGit' }); - const logInfo = (message: any) => logger.info(message, { function: 'initWikiGit' }); - logProgress(i18n.t('Log.StartGitInitialization')); - const { login: username, email, accessToken } = userInfo; - logInfo( - `Using gitUrl ${githubRepoUrl} with username ${username} and accessToken ${truncate(accessToken, { - length: 24, - })}`, - ); - await GitProcess.exec(['init'], wikiFolderPath); - await commitFiles(wikiFolderPath, username, email); - logProgress(i18n.t('Log.StartConfiguringGithubRemoteRepository')); - await credentialOn(wikiFolderPath, githubRepoUrl, userInfo); - logProgress(i18n.t('Log.StartBackupToGithubRemote')); - const defaultBranchName = await getDefaultBranchName(wikiFolderPath); - const { stderr: pushStdError, exitCode: pushExitCode } = await GitProcess.exec( - ['push', 'origin', `${defaultBranchName}:${defaultBranchName}`], - wikiFolderPath, - ); - await credentialOff(wikiFolderPath); - if (isMainWiki && pushExitCode !== 0) { - logInfo(pushStdError); - const CONFIG_FAILED_MESSAGE = i18n.t('Log.GitRepositoryConfigurateFailed'); - logProgress(CONFIG_FAILED_MESSAGE); - throw new Error(CONFIG_FAILED_MESSAGE); - } else { - logProgress(i18n.t('Log.GitRepositoryConfigurationFinished')); - } -} + /** * See if there is any file not being committed * @param {string} wikiFolderPath repo path to test */ -async function haveLocalChanges(wikiFolderPath: any) { +export async function haveLocalChanges(wikiFolderPath: string): Promise { const { stdout } = await GitProcess.exec(['status', '--porcelain'], wikiFolderPath); const matchResult = stdout.match(/^(\?\?|[ACMR] |[ ACMR][DM])*/gm); - // @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'. - return matchResult.some((match: any) => !!match); + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + return !!matchResult?.some((match) => Boolean(match)); } + +export type SyncState = 'noUpstream' | 'equal' | 'ahead' | 'behind' | 'diverged'; /** * determine sync state of repository, i.e. how the remote relates to our HEAD * 'ahead' means our local state is ahead of remote, 'behind' means local state is behind of the remote - * @param {string} wikiFolderPath repo path to test + * @param wikiFolderPath repo path to test */ -async function getSyncState(wikiFolderPath: any, logInfo: any) { +export async function getSyncState(wikiFolderPath: string, logInfo: (message: string) => unknown): Promise { const defaultBranchName = await getDefaultBranchName(wikiFolderPath); const { stdout } = await GitProcess.exec(['rev-list', '--count', '--left-right', `origin/${defaultBranchName}...HEAD`], wikiFolderPath); logInfo('Checking sync state with upstream'); - logInfo('stdout:', stdout, '(stdout end)'); + logInfo(`stdout:\n${stdout}\n(stdout end)`); if (stdout === '') { return 'noUpstream'; } - if (stdout.match(/0\t0/)) { + if (/0\t0/.exec(stdout) !== null) { return 'equal'; } - if (stdout.match(/0\t\d+/)) { + if (/0\t\d+/.exec(stdout) !== null) { return 'ahead'; } - if (stdout.match(/\d+\t0/)) { + if (/\d+\t0/.exec(stdout) !== null) { return 'behind'; } return 'diverged'; } -async function assumeSync(wikiFolderPath: any, logInfo: any, logProgress: any) { + +export async function assumeSync(wikiFolderPath: string, logInfo: (message: string) => unknown, logProgress: (message: string) => unknown): Promise { if ((await getSyncState(wikiFolderPath, logInfo)) === 'equal') { return; } @@ -141,19 +78,20 @@ async function assumeSync(wikiFolderPath: any, logInfo: any, logProgress: any) { logProgress(SYNC_ERROR_MESSAGE); throw new Error(SYNC_ERROR_MESSAGE); } + /** * echo the git dir - * @param {string} wikiFolderPath repo path + * @param wikiFolderPath repo path */ -async function getGitDirectory(wikiFolderPath: any, logInfo: any, logProgress: any) { +async function getGitDirectory(wikiFolderPath: string, logInfo: (message: string) => unknown, logProgress: (message: string) => unknown): Promise { const { stdout, stderr } = await GitProcess.exec(['rev-parse', '--is-inside-work-tree', wikiFolderPath], wikiFolderPath); - if (stderr) { + if (typeof stderr === 'string' && stderr.length > 0) { logInfo(stderr); } if (stdout.startsWith('true')) { const { stdout: stdout2 } = await GitProcess.exec(['rev-parse', '--git-dir', wikiFolderPath], wikiFolderPath); const [gitPath2, gitPath1] = compact(stdout2.split('\n')); - if (gitPath1 && gitPath2) { + if (gitPath1.length > 0 && gitPath2.length > 0) { return path.resolve(`${gitPath1}/${gitPath2}`); } } @@ -161,32 +99,38 @@ async function getGitDirectory(wikiFolderPath: any, logInfo: any, logProgress: a logProgress(CONFIG_FAILED_MESSAGE); throw new Error(`${wikiFolderPath} ${CONFIG_FAILED_MESSAGE}`); } + /** * get various repo state in string format - * @param {string} wikiFolderPath repo path to check - * @returns {string} gitState + * @param wikiFolderPath repo path to check + * @returns gitState + * // TODO: use template literal type to get exact type of git state */ -async function getGitRepositoryState(wikiFolderPath: any, logInfo: any, logProgress: any) { +export async function getGitRepositoryState( + wikiFolderPath: string, + logInfo: (message: string) => unknown, + logProgress: (message: string) => unknown, +): Promise { const gitDirectory = await getGitDirectory(wikiFolderPath, logInfo, logProgress); - if (!gitDirectory) { + if (typeof gitDirectory !== 'string' || gitDirectory.length === 0) { return 'NOGIT'; } let result = ''; - if (((await fs.lstat(path.join(gitDirectory, 'rebase-merge', 'interactive')).catch(() => {})) as any)?.isFile()) { + if (((await fs.lstat(path.join(gitDirectory, 'rebase-merge', 'interactive')).catch(() => ({}))) as fs.Stats)?.isFile()) { result += 'REBASE-i'; - } else if (((await fs.lstat(path.join(gitDirectory, 'rebase-merge')).catch(() => {})) as any)?.isDirectory()) { + } else if (((await fs.lstat(path.join(gitDirectory, 'rebase-merge')).catch(() => ({}))) as fs.Stats)?.isDirectory()) { result += 'REBASE-m'; } else { - if (((await fs.lstat(path.join(gitDirectory, 'rebase-apply')).catch(() => {})) as any)?.isDirectory()) { + if (((await fs.lstat(path.join(gitDirectory, 'rebase-apply')).catch(() => ({}))) as fs.Stats)?.isDirectory()) { result += 'AM/REBASE'; } - if (((await fs.lstat(path.join(gitDirectory, 'MERGE_HEAD')).catch(() => {})) as any)?.isFile()) { + if (((await fs.lstat(path.join(gitDirectory, 'MERGE_HEAD')).catch(() => ({}))) as fs.Stats)?.isFile()) { result += 'MERGING'; } - if (((await fs.lstat(path.join(gitDirectory, 'CHERRY_PICK_HEAD')).catch(() => {})) as any)?.isFile()) { + if (((await fs.lstat(path.join(gitDirectory, 'CHERRY_PICK_HEAD')).catch(() => ({}))) as fs.Stats)?.isFile()) { result += 'CHERRY-PICKING'; } - if (((await fs.lstat(path.join(gitDirectory, 'BISECT_LOG')).catch(() => {})) as any)?.isFile()) { + if (((await fs.lstat(path.join(gitDirectory, 'BISECT_LOG')).catch(() => ({}))) as fs.Stats)?.isFile()) { result += 'BISECTING'; } } @@ -203,11 +147,17 @@ async function getGitRepositoryState(wikiFolderPath: any, logInfo: any, logProgr } /** * try to continue rebase, simply adding and committing all things, leave them to user to resolve in the TiddlyWiki later. - * @param {*} wikiFolderPath - * @param {string} username - * @param {string} email + * @param wikiFolderPath + * @param username + * @param email */ -async function continueRebase(wikiFolderPath: any, username: any, email: any, logInfo: any, logProgress: any) { +export async function continueRebase( + wikiFolderPath: string, + username: string, + email: string, + logInfo: (message: string) => unknown, + logProgress: (message: string) => unknown, +): Promise { let hasNotCommittedConflict = true; let rebaseContinueExitCode = 0; let rebaseContinueStdError = ''; @@ -247,180 +197,3 @@ async function continueRebase(wikiFolderPath: any, username: any, email: any, lo } logProgress(i18n.t('Log.CantSyncInSpecialGitStateAutoFixSucceed')); } -/** - * - * @param {string} githubRepoName similar to "linonetwo/wiki", string after "https://github.com/" - */ -async function updateGitInfoTiddler(githubRepoName: any) { - // TODO: prevent circle require, use lib like typedi to prevent this - // eslint-disable-next-line global-require - const { getActiveBrowserView } = require('../views'); - const browserView = getActiveBrowserView(); - if (browserView && browserView?.webContents?.send) { - const tiddlerText = await new Promise((resolve) => { - browserView.webContents.send('wiki-get-tiddler-text', '$:/GitHub/Repo'); - ipcMain.once('wiki-get-tiddler-text-done', (_, value) => resolve(value)); - }); - if (tiddlerText !== githubRepoName) { - return new Promise((resolve) => { - browserView.webContents.send('wiki-add-tiddler', '$:/GitHub/Repo', githubRepoName, { - type: 'text/vnd.tiddlywiki', - }); - ipcMain.once('wiki-add-tiddler-done', resolve); - }); - } - return Promise.resolve(); - } - return logger.error('no browserView in updateGitInfoTiddler'); -} -/** - * - * @param {string} wikiFolderPath - * @param {string} githubRepoUrl - * @param {{ login: string, email: string, accessToken: string }} userInfo - */ -async function commitAndSync(wikiFolderPath: any, githubRepoUrl: any, userInfo: any) { - /** functions to send data to main thread */ - const logProgress = (message: any) => logger.notice(message, { handler: 'wikiSyncProgress', function: 'commitAndSync', wikiFolderPath, githubRepoUrl }); - const logInfo = (message: any) => logger.info(message, { function: 'commitAndSync', wikiFolderPath, githubRepoUrl }); - if (disableSyncOnDevelopment && isDev) { - return; - } - const { login: username, email } = userInfo; - const commitMessage = 'Wiki updated with TiddlyGit-Desktop'; - const defaultBranchName = await getDefaultBranchName(wikiFolderPath); - const branchMapping = `${defaultBranchName}:${defaultBranchName}`; - // update git info tiddler for plugins to use, for example, linonetwo/github-external-image - let wikiRepoName = new URL(githubRepoUrl).pathname; - if (wikiRepoName.startsWith('/')) { - wikiRepoName = wikiRepoName.replace('/', ''); - } - if (wikiRepoName) { - await updateGitInfoTiddler(wikiRepoName); - } - // preflight check - const repoStartingState = await getGitRepositoryState(wikiFolderPath, logInfo, logProgress); - if (!repoStartingState || repoStartingState === '|DIRTY') { - const SYNC_MESSAGE = i18n.t('Log.PrepareSync'); - logProgress(SYNC_MESSAGE); - logInfo(`${SYNC_MESSAGE} ${wikiFolderPath} , ${username} <${email}>`); - } else if (repoStartingState === 'NOGIT') { - const CANT_SYNC_MESSAGE = i18n.t('Log.CantSyncGitNotInitialized'); - logProgress(CANT_SYNC_MESSAGE); - throw new Error(CANT_SYNC_MESSAGE); - } else { - // we may be in middle of a rebase, try fix that - await continueRebase(wikiFolderPath, username, email, logInfo, logProgress); - } - if (await haveLocalChanges(wikiFolderPath)) { - const SYNC_MESSAGE = i18n.t('Log.HaveThingsToCommit'); - logProgress(SYNC_MESSAGE); - logInfo(`${SYNC_MESSAGE} ${commitMessage}`); - const { exitCode: commitExitCode, stderr: commitStdError } = await commitFiles(wikiFolderPath, username, email, commitMessage); - if (commitExitCode !== 0) { - logInfo('commit failed'); - logInfo(commitStdError); - } - logProgress(i18n.t('Log.CommitComplete')); - } - logProgress(i18n.t('Log.PreparingUserInfo')); - await credentialOn(wikiFolderPath, githubRepoUrl, userInfo); - logProgress(i18n.t('Log.FetchingData')); - await GitProcess.exec(['fetch', 'origin', defaultBranchName], wikiFolderPath); - // - switch (await getSyncState(wikiFolderPath, logInfo)) { - case 'noUpstream': { - logProgress(i18n.t('Log.CantSyncGitNotInitialized')); - await credentialOff(wikiFolderPath); - return; - } - case 'equal': { - logProgress(i18n.t('Log.NoNeedToSync')); - await credentialOff(wikiFolderPath); - return; - } - case 'ahead': { - logProgress(i18n.t('Log.LocalAheadStartUpload')); - const { exitCode, stderr } = await GitProcess.exec(['push', 'origin', branchMapping], wikiFolderPath); - if (exitCode === 0) { - break; - } - logProgress(i18n.t('Log.GitPushFailed')); - logInfo(`exitCode: ${exitCode}, stderr of git push:`); - logInfo(stderr); - break; - } - case 'behind': { - logProgress(i18n.t('Log.LocalStateBehindSync')); - const { exitCode, stderr } = await GitProcess.exec(['merge', '--ff', '--ff-only', `origin/${defaultBranchName}`], wikiFolderPath); - if (exitCode === 0) { - break; - } - logProgress(i18n.t('Log.GitMergeFailed')); - logInfo(`exitCode: ${exitCode}, stderr of git merge:`); - logInfo(stderr); - break; - } - case 'diverged': { - logProgress(i18n.t('Log.LocalStateDivergeRebase')); - const { exitCode } = await GitProcess.exec(['rebase', `origin/${defaultBranchName}`], wikiFolderPath); - if (exitCode === 0 && !(await getGitRepositoryState(wikiFolderPath, logInfo, logProgress)) && (await getSyncState(wikiFolderPath, logInfo)) === 'ahead') { - logProgress(i18n.t('Log.RebaseSucceed')); - } else { - await continueRebase(wikiFolderPath, username, email, logInfo, logProgress); - logProgress(i18n.t('Log.RebaseConflictNeedsResolve')); - } - await GitProcess.exec(['push', 'origin', branchMapping], wikiFolderPath); - break; - } - default: { - logProgress(i18n.t('Log.SyncFailedSystemError')); - } - } - await credentialOff(wikiFolderPath); - logProgress(i18n.t('Log.PerformLastCheckBeforeSynchronizationFinish')); - await assumeSync(wikiFolderPath, logInfo, logProgress); - logProgress(i18n.t('Log.SynchronizationFinish')); -} -async function getRemoteUrl(wikiFolderPath: any) { - const { stdout: remoteStdout } = await GitProcess.exec(['remote'], wikiFolderPath); - const remotes = compact(remoteStdout.split('\n')); - const githubRemote = remotes.find((remote: any) => remote === 'origin') || remotes[0] || ''; - if (githubRemote) { - const { stdout: remoteUrlStdout } = await GitProcess.exec(['remote', 'get-url', githubRemote], wikiFolderPath); - return remoteUrlStdout.replace('.git', ''); - } - return ''; -} -async function clone(githubRepoUrl: any, repoFolderPath: any, userInfo: any) { - const logProgress = (message: any) => logger.notice(message, { handler: 'createWikiProgress', function: 'clone' }); - const logInfo = (message: any) => logger.info(message, { function: 'clone' }); - logProgress(i18n.t('Log.PrepareCloneOnlineWiki')); - logProgress(i18n.t('Log.StartGitInitialization')); - const { login: username, accessToken } = userInfo; - logInfo( - i18n.t('Log.UsingUrlAndUsername', { - githubRepoUrl, - username, - accessToken: truncate(accessToken, { - length: 24, - }), - }), - ); - await GitProcess.exec(['init'], repoFolderPath); - logProgress(i18n.t('Log.StartConfiguringGithubRemoteRepository')); - await credentialOn(repoFolderPath, githubRepoUrl, userInfo); - logProgress(i18n.t('Log.StartFetchingFromGithubRemote')); - const defaultBranchName = await getDefaultBranchName(repoFolderPath); - const { stderr, exitCode } = await GitProcess.exec(['pull', 'origin', `${defaultBranchName}:${defaultBranchName}`], repoFolderPath); - await credentialOff(repoFolderPath); - if (exitCode !== 0) { - logInfo(stderr); - const CONFIG_FAILED_MESSAGE = i18n.t('Log.GitRepositoryConfigurateFailed'); - logProgress(CONFIG_FAILED_MESSAGE); - throw new Error(CONFIG_FAILED_MESSAGE); - } else { - logProgress(i18n.t('Log.GitRepositoryConfigurationFinished')); - } -} -export { initWikiGit, commitAndSync, getRemoteUrl, clone }; diff --git a/src/services/libs/i18n/i18next-electron-fs-backend.ts b/src/services/libs/i18n/i18next-electron-fs-backend.ts index 57216ce6..d8d3ef7f 100644 --- a/src/services/libs/i18n/i18next-electron-fs-backend.ts +++ b/src/services/libs/i18n/i18next-electron-fs-backend.ts @@ -1,3 +1,4 @@ +/* eslint-disable unicorn/prevent-abbreviations */ import fs from 'fs-extra'; import path from 'path'; import sendToAllWindows from '../send-to-all-windows';