feat: implement git-sync using dugite

https://github.com/simonthum/git-sync/blob/master/git-sync
This commit is contained in:
tiddlygit-test 2020-07-18 17:54:47 +08:00
parent 53a80e8621
commit efdf4fa370
9 changed files with 6413 additions and 87 deletions

View file

@ -12,6 +12,7 @@
}
],
"unicorn/no-reduce": [0],
"sonarjs/cognitive-complexity": ["error", 30],
"react/jsx-props-no-spreading": [0],
"import/no-extraneous-dependencies": [2, { "devDependencies": true }],
"react/static-property-placement": [0],

6113
flow-typed/npm/lodash_v4.x.x.js vendored Normal file

File diff suppressed because it is too large Load diff

6
package-lock.json generated
View file

@ -12833,9 +12833,9 @@
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npm.taobao.org/lodash/download/lodash-4.17.15.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.15.tgz",
"integrity": "sha1-tEf2ZwoEVbv+7dETku/zMOoJdUg="
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
},
"lodash._reinterpolate": {
"version": "3.0.0",

View file

@ -43,6 +43,7 @@
"is-url": "1.2.4",
"isomorphic-git": "^1.7.1",
"jimp": "0.14.0",
"lodash": "4.17.19",
"menubar": "9.0.1",
"node-fetch": "2.6.0",
"prop-types": "15.7.2",

View file

@ -1,98 +1,304 @@
const git = require('isomorphic-git');
const http = require('isomorphic-git/http/node');
const fs = require('fs');
/* eslint-disable no-await-in-loop */
const fs = require('fs-extra');
const path = require('path');
const { compact } = require('lodash');
const { GitProcess } = require('dugite');
const { wikiCreationProgress } = require('./wiki/progress-message');
function processUserInfo(userInfo) {
const { login: name, email, accessToken } = userInfo;
const author = {
name,
email,
};
const committer = {
name: 'tiddly-git',
email: 'tiddlygit@gmail.com',
};
const onAuth = () => ({
username: name,
password: accessToken,
});
return {
author,
committer,
onAuth,
};
}
async function commitFiles(wikiFolderPath, author, message = 'Initialize with TiddlyGit-Desktop') {
await git.add({ fs, dir: wikiFolderPath, filepath: '.' });
await git.commit({
fs,
dir: wikiFolderPath,
author,
message,
});
/**
* 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);
}
async function initWikiGit(wikiFolderPath, githubRepoUrl, userInfo) {
wikiCreationProgress('开始初始化本地Git仓库');
const gitUrl = `${githubRepoUrl}.git`;
const { author, onAuth } = processUserInfo(userInfo);
await git.init({ fs, dir: wikiFolderPath });
await commitFiles(wikiFolderPath, author);
const { login: username, email, accessToken } = userInfo;
const gitUrl = `${githubRepoUrl}.git`.replace(
'https://github.com/',
`https://${username}:${accessToken}@github.com/`,
);
console.info(`Using gitUrl ${gitUrl}`);
await GitProcess.exec(['init'], wikiFolderPath);
await commitFiles(wikiFolderPath, username, email);
wikiCreationProgress('仓库初始化完毕开始配置Github远端仓库');
await git.addRemote({
fs,
dir: wikiFolderPath,
remote: 'origin',
url: gitUrl,
});
await GitProcess.exec(['remote', 'add', 'origin', gitUrl], wikiFolderPath);
wikiCreationProgress('正在将Wiki所在的本地Git备份到Github远端仓库');
await git.push({
fs,
http,
dir: wikiFolderPath,
remote: 'origin',
ref: 'master',
force: true,
onAuth,
});
wikiCreationProgress('Git仓库配置完毕');
const { stderr: pushStdError, exitCode: pushExitCode } = await GitProcess.exec(
['push', 'origin', 'master', '--force'],
wikiFolderPath,
);
if (pushExitCode !== 0) {
console.info('pushStdError', pushStdError);
throw new Error('Git仓库配置失败详见错误日志');
} else {
wikiCreationProgress('Git仓库配置完毕');
}
}
/**
* 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']);
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) {
const { stdout } = await GitProcess.exec(
['rev-list', '--count', '--left-right', 'origin/master...HEAD'],
wikiFolderPath,
);
console.info('Checking sync state with upstream');
console.info('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) {
if ((await getSyncState(wikiFolderPath)) === 'equal') return;
throw new Error(
'同步失败!你需要用 Github Desktop 等工具检查当前 Git 仓库的状态。失败可能是网络原因导致的,如果的确如此,可在调整网络后重试。',
);
}
/**
* echo the git dir
* @param {string} wikiFolderPath repo path
*/
async function getGitDirectory(wikiFolderPath) {
const { stdout, stderr } = await GitProcess.exec(
['rev-parse', '--is-inside-work-tree', wikiFolderPath],
wikiFolderPath,
);
if (stderr) console.info(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}`);
}
}
throw new Error(`${wikiFolderPath} 不是一个 git 仓库`);
}
/**
* get various repo state in string format
* @param {string} wikiFolderPath repo path to check
* @returns {string} gitState
*/
async function getGitRepositoryState(wikiFolderPath) {
const gitDirectory = await getGitDirectory(wikiFolderPath);
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) {
let hasNotCommittedConflict = true;
let rebaseContinueExitCode = 0;
let rebaseContinueStdError = '';
let repositoryState = await getGitRepositoryState(wikiFolderPath);
// prevent infin loop, if there is some bug that I miss
let loopCount = 0;
while (hasNotCommittedConflict) {
loopCount += 1;
if (loopCount > 1000) {
throw new Error('无法同步,而且同步脚本陷入死循环');
}
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);
// if git add . + git commit failed or git rebase --continue failed
if (commitExitCode !== 0 || rebaseContinueExitCode !== 0) {
console.info(`rebaseContinueStdError when ${repositoryState}`);
console.info(rebaseContinueStdError);
console.info(`commitStdError when ${repositoryState}`);
console.info(commitStdError);
throw new Error(
`无法同步,这个文件夹处在 ${repositoryState} 状态,不能直接进行同步,已尝试自动修复,但还是出现错误,请先解决所有冲突(例如使用 VSCode 打开),如果还不行,请用 Git 工具解决问题`,
);
}
hasNotCommittedConflict =
rebaseContinueStdError.startsWith('CONFLICT') || rebaseContinueStdOut.startsWith('CONFLICT');
}
console.log(`这个文件夹处在 ${repositoryState} 状态,不能直接进行同步,但已自动修复`);
}
async function commitAndSync(wikiFolderPath, githubRepoUrl, userInfo) {
const { author, onAuth } = processUserInfo(userInfo);
console.log(`Sync to cloud for ${wikiFolderPath} under ${JSON.stringify(author)}`);
await commitFiles(wikiFolderPath, author, 'Wiki updated with TiddlyGit-Desktop');
await git.pull({
fs,
http,
author,
onAuth,
dir: wikiFolderPath,
ref: 'master',
// singleBranch: true,
});
await git.push({
fs,
http,
dir: wikiFolderPath,
remote: 'origin',
ref: 'master',
force: true,
onAuth,
});
console.log(`${wikiFolderPath} Sync completed`);
const { login: username, email } = userInfo;
const commitMessage = 'Wiki updated with TiddlyGit-Desktop';
const branchMapping = 'master:master';
// preflight check
const repoStartingState = await getGitRepositoryState(wikiFolderPath);
if (!repoStartingState || repoStartingState === '|DIRTY') {
console.log(`准备同步 ${wikiFolderPath} ,使用的作者信息为 ${username} <${email}>`);
} else if (repoStartingState === 'NOGIT') {
throw new Error(`无法同步,这个文件夹没有初始化为 Git 仓库`);
} else {
// we may be in middle of a rebase, try fix that
await continueRebase(wikiFolderPath, username, email);
}
if (await haveLocalChanges(wikiFolderPath)) {
console.log(`有需要提交(commit)的内容,正在自动提交,使用的提交信息为 「${commitMessage}`);
const { exitCode: commitExitCode, stderr: commitStdError } = await commitFiles(
wikiFolderPath,
username,
email,
commitMessage,
);
if (commitExitCode !== 0) {
console.info('commit failed');
console.info(commitStdError);
}
}
console.log('正在拉取云端数据,以便比对');
await GitProcess.exec(['fetch', 'origin', 'master'], wikiFolderPath);
//
switch (await getSyncState(wikiFolderPath)) {
case 'noUpstream': {
console.log('同步失败,当前目录可能不是一个初始化好的 git 仓库');
return;
}
case 'equal': {
console.log('无需同步,本地状态和云端一致');
return;
}
case 'ahead': {
console.log('本地状态超前于云端,开始上传');
const { exitCode, stderr } = await GitProcess.exec(['push', 'origin', branchMapping], wikiFolderPath);
if (exitCode === 0) break;
console.log(`git push 的返回值是 ${exitCode},这通常意味着有网络问题`);
console.info('stderr of git push:');
console.info(stderr);
break;
}
case 'behind': {
console.log('本地状态落后于云端,开始合并云端数据');
const { exitCode, stderr } = await GitProcess.exec(
['merge', '--ff', '--ff-only', 'origin/master'],
wikiFolderPath,
);
if (exitCode === 0) break;
console.log(`git merge 的返回值是 ${exitCode},详见错误日志`);
console.info('stderr of git merge:');
console.info(stderr);
break;
}
case 'diverged': {
console.log('本地状态与云端有分歧,开始变基(Rebase)');
const { exitCode, stderr } = await GitProcess.exec(['rebase', 'origin/master'], wikiFolderPath);
if (
exitCode === 0 &&
!(await getGitRepositoryState(wikiFolderPath)) &&
(await getSyncState(wikiFolderPath)) === 'ahead'
) {
console.log('变基(Rebase)成功,开始上传');
} else {
await continueRebase(wikiFolderPath, username, email);
console.log(`变基(Rebase)时发现冲突,需要解决冲突`);
}
await GitProcess.exec(['push', 'origin', branchMapping], wikiFolderPath);
await assumeSync(wikiFolderPath);
break;
}
default: {
console.log('同步失败,同步系统可能出现问题');
}
}
await assumeSync(wikiFolderPath);
console.log(`${wikiFolderPath} 同步完成`);
}
async function getRemoteUrl(wikiFolderPath) {
try {
const remotes = await git.listRemotes({ fs, dir: wikiFolderPath });
const githubRemote = remotes.find(remote => remote.remote === 'origin') || remotes[0] || { url: '' };
return githubRemote.url.replace('.git', '');
} catch {
return '';
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 '';
}
module.exports = {

View file

@ -13,6 +13,7 @@ const {
setWorkspaces,
setWorkspacePicture,
} = require('./workspaces');
const sendToAllWindows = require('./send-to-all-windows');
const {
addView,

View file

@ -69,6 +69,7 @@ const loadListeners = () => {
try {
await createSubWiki(newFolderPath, folderName, mainWikiToLink, onlyLink);
} catch (error) {
console.info(error);
return String(error);
}
});
@ -76,6 +77,7 @@ const loadListeners = () => {
try {
await ensureWikiExist(wikiPath, shouldBeMainWiki);
} catch (error) {
console.info(error);
return String(error);
}
});
@ -90,6 +92,8 @@ const loadListeners = () => {
try {
await initWikiGit(wikiFolderPath, githubRepoUrl, userInfo);
} catch (error) {
console.info(error);
removeWiki(wikiFolderPath);
return String(error);
}
});

View file

@ -75,7 +75,7 @@ function DoneButton({
<CloseButton
variant="contained"
color="secondary"
disabled={!existedFolderLocation || !githubWikiUrl}
disabled={!existedFolderLocation || !githubWikiUrl || progressBarOpen}
onClick={async () => {
updateForm(workspaceFormData);
const creationError = await ensureWikiExist(existedFolderLocation, true);
@ -110,7 +110,7 @@ function DoneButton({
<CloseButton
variant="contained"
color="secondary"
disabled={!existedFolderLocation || !mainWikiToLink || !githubWikiUrl}
disabled={!existedFolderLocation || !mainWikiToLink || !githubWikiUrl || progressBarOpen}
onClick={async () => {
const wikiFolderName = basename(existedFolderLocation);
const parentFolderLocation = dirname(existedFolderLocation);

View file

@ -79,7 +79,7 @@ function NewWikiDoneButton({
<CloseButton
variant="contained"
color="secondary"
disabled={!parentFolderLocation || !githubWikiUrl}
disabled={!parentFolderLocation || !githubWikiUrl || progressBarOpen}
onClick={async () => {
updateForm(workspaceFormData);
let creationError = await requestCopyWikiTemplate(parentFolderLocation, wikiFolderName);
@ -118,7 +118,7 @@ function NewWikiDoneButton({
<CloseButton
variant="contained"
color="secondary"
disabled={!parentFolderLocation || !mainWikiToLink || !githubWikiUrl}
disabled={!parentFolderLocation || !mainWikiToLink || !githubWikiUrl || progressBarOpen}
onClick={async () => {
updateForm(workspaceFormData);
let creationError = await requestCreateSubWiki(parentFolderLocation, wikiFolderName, mainWikiToLink);