mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2025-12-06 02:30:47 -08:00
406 lines
16 KiB
JavaScript
406 lines
16 KiB
JavaScript
/* eslint-disable sonarjs/no-duplicate-string */
|
|
/* eslint-disable unicorn/consistent-function-scoping */
|
|
/* eslint-disable no-await-in-loop */
|
|
const fs = require('fs-extra');
|
|
const path = require('path');
|
|
const { compact, truncate, trim } = require('lodash');
|
|
const { GitProcess } = require('dugite');
|
|
const { logger } = require('./log');
|
|
const i18n = require('./i18n');
|
|
|
|
const getGitUrlWithCredential = (rawUrl, username, accessToken) =>
|
|
trim(`${rawUrl}.git`.replace('https://github.com/', `https://${username}:${accessToken}@github.com/`));
|
|
const getGitUrlWithOutCredential = urlWithCredential => 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, githubRepoUrl, userInfo) {
|
|
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) {
|
|
const githubRepoUrl = await getRemoteUrl(wikiFolderPath);
|
|
const gitUrlWithOutCredential = getGitUrlWithOutCredential(githubRepoUrl);
|
|
await GitProcess.exec(['remote', 'set-url', 'origin', gitUrlWithOutCredential], wikiFolderPath);
|
|
}
|
|
|
|
/**
|
|
* Git add and commit all file
|
|
* @param {string} wikiFolderPath
|
|
* @param {string} username
|
|
* @param {string} email
|
|
* @param {?string} message
|
|
*/
|
|
async function commitFiles(wikiFolderPath, username, email, message = 'Initialize with TiddlyGit-Desktop') {
|
|
await GitProcess.exec(['add', '.'], wikiFolderPath);
|
|
return 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, githubRepoUrl, userInfo, isMainWiki) {
|
|
const logProgress = message => logger.notice(message, { handler: 'createWikiProgress', function: 'initWikiGit' });
|
|
const logInfo = message => logger.info(message, { function: 'initWikiGit' });
|
|
|
|
logProgress(i18n.t('Log.StartLocalGitInitialization'));
|
|
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 { stderr: pushStdError, exitCode: pushExitCode } = await GitProcess.exec(
|
|
['push', 'origin', 'master:master', '--force'],
|
|
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) {
|
|
const { stdout } = await GitProcess.exec(['status', '--porcelain'], wikiFolderPath);
|
|
const matchResult = stdout.match(/^(\?\?|[ACMR] |[ ACMR][DM])*/gm);
|
|
return matchResult.some(match => !!match);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
async function getSyncState(wikiFolderPath, logInfo) {
|
|
const { stdout } = await GitProcess.exec(
|
|
['rev-list', '--count', '--left-right', 'origin/master...HEAD'],
|
|
wikiFolderPath,
|
|
);
|
|
logInfo('Checking sync state with upstream');
|
|
logInfo('stdout:', stdout, '(stdout end)');
|
|
if (stdout === '') return 'noUpstream';
|
|
if (stdout.match(/0\t0/)) return 'equal';
|
|
if (stdout.match(/0\t\d+/)) return 'ahead';
|
|
if (stdout.match(/\d+\t0/)) return 'behind';
|
|
return 'diverged';
|
|
}
|
|
|
|
async function assumeSync(wikiFolderPath, logInfo, logProgress) {
|
|
if ((await getSyncState(wikiFolderPath, logInfo)) === 'equal') return;
|
|
|
|
const SYNC_ERROR_MESSAGE = i18n.t('Log.SynchronizationFailed');
|
|
logProgress(SYNC_ERROR_MESSAGE);
|
|
throw new Error(SYNC_ERROR_MESSAGE);
|
|
}
|
|
|
|
/**
|
|
* echo the git dir
|
|
* @param {string} wikiFolderPath repo path
|
|
*/
|
|
async function getGitDirectory(wikiFolderPath, logInfo, logProgress) {
|
|
const { stdout, stderr } = await GitProcess.exec(
|
|
['rev-parse', '--is-inside-work-tree', wikiFolderPath],
|
|
wikiFolderPath,
|
|
);
|
|
if (stderr) 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) {
|
|
return path.resolve(`${gitPath1}/${gitPath2}`);
|
|
}
|
|
}
|
|
const CONFIG_FAILED_MESSAGE = i18n.t('Log.NotAGitRepository');
|
|
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
|
|
*/
|
|
async function getGitRepositoryState(wikiFolderPath, logInfo, logProgress) {
|
|
const gitDirectory = await getGitDirectory(wikiFolderPath, logInfo, logProgress);
|
|
if (!gitDirectory) return 'NOGIT';
|
|
let result = '';
|
|
if ((await fs.lstat(path.join(gitDirectory, 'rebase-merge', 'interactive')).catch(() => {}))?.isFile()) {
|
|
result += 'REBASE-i';
|
|
} else if ((await fs.lstat(path.join(gitDirectory, 'rebase-merge')).catch(() => {}))?.isDirectory()) {
|
|
result += 'REBASE-m';
|
|
} else {
|
|
if ((await fs.lstat(path.join(gitDirectory, 'rebase-apply')).catch(() => {}))?.isDirectory()) {
|
|
result += 'AM/REBASE';
|
|
}
|
|
if ((await fs.lstat(path.join(gitDirectory, 'MERGE_HEAD')).catch(() => {}))?.isFile()) {
|
|
result += 'MERGING';
|
|
}
|
|
if ((await fs.lstat(path.join(gitDirectory, 'CHERRY_PICK_HEAD')).catch(() => {}))?.isFile()) {
|
|
result += 'CHERRY-PICKING';
|
|
}
|
|
if ((await fs.lstat(path.join(gitDirectory, 'BISECT_LOG')).catch(() => {}))?.isFile()) {
|
|
result += 'BISECTING';
|
|
}
|
|
}
|
|
|
|
if (
|
|
(await GitProcess.exec(['rev-parse', '--is-inside-git-dir', wikiFolderPath], wikiFolderPath)).stdout.startsWith(
|
|
'true',
|
|
)
|
|
) {
|
|
if (
|
|
(await GitProcess.exec(['rev-parse', '--is-bare-repository', wikiFolderPath], wikiFolderPath)).stdout.startsWith(
|
|
'true',
|
|
)
|
|
) {
|
|
result += '|BARE';
|
|
} else {
|
|
result += '|GIT_DIR';
|
|
}
|
|
} else if (
|
|
(await GitProcess.exec(['rev-parse', '--is-inside-work-tree', wikiFolderPath], wikiFolderPath)).stdout.startsWith(
|
|
'true',
|
|
)
|
|
) {
|
|
const { exitCode } = await GitProcess.exec(['diff', '--no-ext-diff', '--quiet', '--exit-code'], wikiFolderPath);
|
|
if (exitCode === 0) {
|
|
result += '|DIRTY';
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
async function continueRebase(wikiFolderPath, username, email, logInfo, logProgress) {
|
|
let hasNotCommittedConflict = true;
|
|
let rebaseContinueExitCode = 0;
|
|
let rebaseContinueStdError = '';
|
|
let repositoryState = await getGitRepositoryState(wikiFolderPath, logInfo, logProgress);
|
|
// prevent infin loop, if there is some bug that I miss
|
|
let loopCount = 0;
|
|
while (hasNotCommittedConflict) {
|
|
loopCount += 1;
|
|
if (loopCount > 1000) {
|
|
const CANT_SYNC_MESSAGE = i18n.t('Log.CantSynchronizeAndSyncScriptIsInDeadLoop');
|
|
logProgress(CANT_SYNC_MESSAGE);
|
|
throw new Error(CANT_SYNC_MESSAGE);
|
|
}
|
|
const { exitCode: commitExitCode, stderr: commitStdError } = await commitFiles(
|
|
wikiFolderPath,
|
|
username,
|
|
email,
|
|
'Conflict files committed with TiddlyGit-Desktop',
|
|
);
|
|
const rebaseContinueResult = await GitProcess.exec(['rebase', '--continue'], wikiFolderPath);
|
|
// get info for logging
|
|
rebaseContinueExitCode = rebaseContinueResult.exitCode;
|
|
rebaseContinueStdError = rebaseContinueResult.stderr;
|
|
const rebaseContinueStdOut = rebaseContinueResult.stdout;
|
|
repositoryState = await getGitRepositoryState(wikiFolderPath, logInfo, logProgress);
|
|
// if git add . + git commit failed or git rebase --continue failed
|
|
if (commitExitCode !== 0 || rebaseContinueExitCode !== 0) {
|
|
logInfo(`rebaseContinueStdError when ${repositoryState}`);
|
|
logInfo(rebaseContinueStdError);
|
|
logInfo(`commitStdError when ${repositoryState}`);
|
|
logInfo(commitStdError);
|
|
const CANT_SYNC_MESSAGE = i18n.t('Log.CantSyncInSpecialGitStateAutoFixFailed');
|
|
logProgress(CANT_SYNC_MESSAGE);
|
|
throw new Error(`${repositoryState} ${CANT_SYNC_MESSAGE}`);
|
|
}
|
|
hasNotCommittedConflict =
|
|
rebaseContinueStdError.startsWith('CONFLICT') || rebaseContinueStdOut.startsWith('CONFLICT');
|
|
}
|
|
|
|
logProgress(i18n.t('Log.CantSyncInSpecialGitStateAutoFixSucceed'));
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} wikiFolderPath
|
|
* @param {string} githubRepoUrl
|
|
* @param {{ login: string, email: string, accessToken: string }} userInfo
|
|
* @param {({ type: string, payload: { message: string, handler: string }}) => void} loggerToMainThread Send message to .log file or send to GUI or sent to notification based on type, see wiki-worker-manager.js for details
|
|
*/
|
|
async function commitAndSync(wikiFolderPath, githubRepoUrl, userInfo, loggerToMainThread) {
|
|
/** functions to send data to main thread */
|
|
const logProgress = message => logger.notice(message, { handler: 'wikiSyncProgress', function: 'commitAndSync' });
|
|
const logInfo = message => logger.info(message, { function: 'commitAndSync' });
|
|
|
|
const { login: username, email } = userInfo;
|
|
const commitMessage = 'Wiki updated with TiddlyGit-Desktop';
|
|
const branchMapping = 'master:master';
|
|
|
|
// 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', 'master'], wikiFolderPath);
|
|
|
|
//
|
|
switch (await getSyncState(wikiFolderPath, logInfo)) {
|
|
case 'noUpstream': {
|
|
logProgress(i18n.t('Log.CantSyncGitNotInitialized'));
|
|
return;
|
|
}
|
|
case 'equal': {
|
|
logProgress(i18n.t('Log.NoNeedToSync'));
|
|
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/master'],
|
|
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/master'], 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.RebaseConfliceNeedsResolve'));
|
|
}
|
|
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) {
|
|
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) {
|
|
const { stdout: remoteUrlStdout } = await GitProcess.exec(['remote', 'get-url', githubRemote], wikiFolderPath);
|
|
return remoteUrlStdout.replace('.git', '');
|
|
}
|
|
return '';
|
|
}
|
|
|
|
async function clone(githubRepoUrl, repoFolderPath, userInfo) {
|
|
const logProgress = message => logger.notice(message, { handler: 'createWikiProgress', function: 'clone' });
|
|
const logInfo = message => logger.info(message, { function: 'clone' });
|
|
logProgress(i18n.t('Log.PrepareCloneOnlineWiki'));
|
|
logProgress(i18n.t('Log.InitialGitInitialization'));
|
|
const { login: username, accessToken } = userInfo;
|
|
logInfo(
|
|
`Using gitUrl ${githubRepoUrl} with username ${username} and 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 { stderr, exitCode } = await GitProcess.exec(['pull', 'origin', 'master:master'], 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'));
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
initWikiGit,
|
|
commitAndSync,
|
|
getRemoteUrl,
|
|
clone,
|
|
};
|