Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: git manager #48

Merged
merged 19 commits into from
Jul 22, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module.exports = getESLintConfig('react-ts', {
'@iceworks/best-practices/recommend-polyfill': 0,
'import/order': 1,
'no-param-reassign': 0,
'@typescript-eslint/no-require-imports': 0
'@typescript-eslint/no-require-imports': 0,
'no-await-in-loop': 0
},
});
3 changes: 3 additions & 0 deletions main/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ export const TAOBAO_NODE_MIRROR = 'https://npm.taobao.org/mirrors/node';

export const PROFILE_FILES = ['.bash_profile', '.bashrc', '.zshrc'];
export const DEFAULT_PROFILE_FILE = '.bash_profile';

// git
export const GLOBAL_GITCONFIG_PATH = path.join(process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'], '.gitconfig');
181 changes: 181 additions & 0 deletions main/git/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import * as path from 'path';
import ini = require('ini');
import * as fse from 'fs-extra';
import { GLOBAL_GITCONFIG_PATH } from '../constants';
import log from '../utils/log';
import {
getSSHPublicKey,
updateSSHConfig,
getSSHConfigs,
SSHDir,
rsaFileSuffix,
removeSSHConfig,
} from './ssh';

const HOME_DIR = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'];
const IGNORE_CONFIG_KEYS = ['gitDir', 'hostName'];

export async function getGlobalGitConfig() {
const globalGitConfig = await parseGitConfig(GLOBAL_GITCONFIG_PATH);
log.info('get-global-git-config', globalGitConfig);
return globalGitConfig;
}

export async function updateGlobalGitConfig(gitConfig: object) {
log.info('update-global-git-config', gitConfig);
await writeGitConfig(GLOBAL_GITCONFIG_PATH, gitConfig);
}

export async function getUserGitConfigs() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议注释下,说明怎么取 步骤是什么 可能的问题有什么

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

const globalGitConfig = await parseGitConfig(GLOBAL_GITCONFIG_PATH);
const globalGitConfigKeys = Object.keys(globalGitConfig);
const userGitConfigs = [];

for (const key of globalGitConfigKeys) {
if (/^includeIf "gitdir:/.test(key)) {
const gitConfigPath = globalGitConfig[key].path || '';
let userGitConfigPath = gitConfigPath.replace('~', HOME_DIR);
if (!path.isAbsolute(userGitConfigPath)) {
// .gitconfig-gitlab -> /Users/xx/.gitconfig-gitlab
userGitConfigPath = path.join(HOME_DIR, userGitConfigPath);
}
const userGitConfigPathExists = await fse.pathExists(userGitConfigPath);
if (!userGitConfigPathExists) {
continue;
}

const userGitConfig = await parseGitConfig(userGitConfigPath);
// e.g.: gitlab
const userGitConfigName = path.basename(userGitConfigPath).replace('.gitconfig-', '');
let userGitDir = '';
const userGitDirMatchRes = key.match(/^includeIf "gitdir:(.*)"/);
if (userGitDirMatchRes) {
// e.g.: /Users/workspace/gitlab/
userGitDir = userGitDirMatchRes[1];
}

let SSHPublicKey = '';
let hostName = '';
const privateKeyPath = path.join(SSHDir, `${userGitConfigName}${rsaFileSuffix}`);
const SSHConfigSections = await getSSHConfigs();
// eslint-disable-next-line no-labels
fyangstudio marked this conversation as resolved.
Show resolved Hide resolved
loopLabel:
for (const section of SSHConfigSections) {
const { config = [], value: HostName } = section;
for (const { param, value } of config) {
if (param === 'IdentityFile' && value.replace('~', HOME_DIR) === privateKeyPath) {
hostName = HostName;
SSHPublicKey = await getSSHPublicKey(privateKeyPath);
// eslint-disable-next-line no-labels
break loopLabel;
}
}
}

userGitConfigs.push({
...userGitConfig,
configName: userGitConfigName,
gitDir: userGitDir,
gitConfigPath: userGitConfigPath,
hostName,
SSHPublicKey,
});
}
}

log.info('get-user-git-configs', userGitConfigs);
return userGitConfigs;
}

export async function addUserGitConfig(configName: string, gitDir: string) {
const gitConfigPath = `${path.join(HOME_DIR, `.gitconfig-${configName}`)}`;
if (fse.pathExistsSync(gitConfigPath)) {
const err = new Error(`${configName} Git 配置已存在,请使用其他配置名称`);
err.name = 'add-user-git-config';
log.error(err);
throw err;
}

const globalGitConfig = await parseGitConfig(GLOBAL_GITCONFIG_PATH);
const includeIfKey = `includeIf "gitdir:${gitDir}"`;
if (globalGitConfig[includeIfKey]) {
const err = new Error(`目录 ${gitDir} 已被设置,请使用其他目录`);
err.name = 'add-user-git-config';
log.error(err);
throw err;
}

globalGitConfig[includeIfKey] = {
path: gitConfigPath,
};

// append git config to ~/.gitconfig
await writeGitConfig(GLOBAL_GITCONFIG_PATH, globalGitConfig);

// create ~/.gitconfig-gitlab
await fse.createFile(gitConfigPath);

log.info('add-user-git-config', includeIfKey, globalGitConfig[includeIfKey]);
}

export async function updateUserGitConfig(gitConfig: any, configName: string, gitConfigPath: string) {
const { hostName = '', user = {} } = gitConfig;
const { name: userName = '' } = user;
await updateSSHConfig(configName, hostName, userName);

IGNORE_CONFIG_KEYS.forEach((key) => {
delete gitConfig[key];
});

await writeGitConfig(gitConfigPath, gitConfig);
log.info('update-user-git-config', gitConfigPath, gitConfig);
}

export async function updateUserGitDir(
originGitDir: string,
currentGitDir: string,
) {
const globalGitConfig = await parseGitConfig(GLOBAL_GITCONFIG_PATH);

const originIncludeIfKey = `includeIf "gitdir:${originGitDir}"`;
const currentIncludeIfKey = `includeIf "gitdir:${currentGitDir}"`;
const includeIfValue = globalGitConfig[originIncludeIfKey];

delete globalGitConfig[originIncludeIfKey];
globalGitConfig[currentIncludeIfKey] = includeIfValue;

await writeGitConfig(GLOBAL_GITCONFIG_PATH, globalGitConfig);

log.info('updateUserGitDir: ', currentIncludeIfKey, globalGitConfig[currentIncludeIfKey]);
}

export async function removeUserGitConfig(configName: string, gitDir: string, gitConfigPath: string) {
// Remove SSH config section
await removeSSHConfig(configName);

const globalGitConfig = await parseGitConfig(GLOBAL_GITCONFIG_PATH);
const globalGitConfigKeys = Object.keys(globalGitConfig);
for (const key of globalGitConfigKeys) {
if (key === `includeIf "gitdir:${gitDir}"`) {
log.info('remove-user-git-config', globalGitConfig[key]);
delete globalGitConfig[key];
break;
}
}

// update gitConfig to ~/.gitconfig
await writeGitConfig(GLOBAL_GITCONFIG_PATH, globalGitConfig);

// remove the gitconfig file
await fse.remove(gitConfigPath);
}

async function parseGitConfig(gitConfigPath: string) {
const gitConfigContent = await fse.readFile(gitConfigPath, 'utf-8');
return ini.parse(gitConfigContent);
}

async function writeGitConfig(gitConfigPath: string, config: object) {
await fse.writeFile(gitConfigPath, ini.stringify(config, { whitespace: true }));
log.info('write-git-config', config);
}
2 changes: 2 additions & 0 deletions main/git/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './config';
export * from './ssh';
161 changes: 161 additions & 0 deletions main/git/ssh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import * as util from 'util';
import * as path from 'path';
import * as fse from 'fs-extra';
import SSHKeyGen = require('ssh-keygen');
import SSHConfig = require('ssh-config');
import log from '../utils/log';

const SSHKeyGenAsync = util.promisify(SSHKeyGen);

const HOME_DIR = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'];
export const SSHDir = path.join(HOME_DIR, '.ssh');
export const rsaFileSuffix = '_id_rsa';
// ~/.ssh/config
const SSHConfigPath = path.join(SSHDir, 'config');

/**
* generate SSH public key and private key
* @param userEmail current user email of this git config
* @param configName SSH config name
*/
export async function generateSSHKey(userEmail: string, configName: string) {
const location = path.join(SSHDir, `${configName}${rsaFileSuffix}`);

await SSHKeyGenAsync({
comment: userEmail,
location,
read: true,
});
}

export async function getSSHPublicKey(SSHPrivateKeyPath: string) {
const SSHPublicKeyPath = `${SSHPrivateKeyPath}.pub`;
const SSHPublicKeyFileExists = await fse.pathExists(SSHPublicKeyPath);
if (!SSHPublicKeyFileExists) {
fyangstudio marked this conversation as resolved.
Show resolved Hide resolved
return '';
}
return await fse.readFile(SSHPublicKeyPath, 'utf-8');
}

export async function getSSHConfigs() {
const SSHConfigExists = await fse.pathExists(SSHConfigPath);
let SSHConfigSections;
if (!SSHConfigExists) {
SSHConfigSections = [];
} else {
const SSHConfigContent = await fse.readFile(SSHConfigPath, 'utf-8');
SSHConfigSections = SSHConfig.parse(SSHConfigContent);
}

log.info('get-SSH-configs', SSHConfigSections);
return SSHConfigSections;
}

/**
* add SSH config to ~/.ssh/config
*/
export async function addSSHConfig(
{ hostName, userName, configName }: { configName: string; hostName: string; userName: string },
) {
const SSHConfigExists = await fse.pathExists(SSHConfigPath);
if (!SSHConfigExists) {
log.info('add-ssh-config', 'create ssh config file:', SSHConfigPath);
await fse.createFile(SSHConfigPath);
}
const SSHConfigContent = await fse.readFile(SSHConfigPath, 'utf-8');
const SSHConfigSections = SSHConfig.parse(SSHConfigContent);
const newSSHConfigSection = {
Host: hostName,
HostName: hostName,
User: userName,
PreferredAuthentications: 'publickey',
IdentityFile: path.join(SSHDir, `${configName}${rsaFileSuffix}`),
};
SSHConfigSections.append(newSSHConfigSection);

await fse.writeFile(SSHConfigPath, SSHConfig.stringify(SSHConfigSections));

log.info('add-SSH-config', newSSHConfigSection);
}

export async function updateSSHConfig(configName: string, hostName = '', userName = '') {
const SSHConfigExists = await fse.pathExists(SSHConfigPath);
if (!SSHConfigExists) {
const error = new Error(`The SSH config path: ${SSHConfigPath} does not exist.`);
error.name = 'update-ssh-config';
log.error(error);
return;
}
const SSHConfigContent = await fse.readFile(SSHConfigPath, 'utf-8');
const SSHConfigSections = SSHConfig.parse(SSHConfigContent);

const SSHConfigSectionIndex = findSSHConfigSectionIndex(SSHConfigSections, configName);

if (SSHConfigSectionIndex > -1) {
SSHConfigSections.splice(SSHConfigSectionIndex, 1);
const newSSHConfigSection = {
Host: hostName,
HostName: hostName,
User: userName,
PreferredAuthentications: 'publickey',
IdentityFile: path.join(SSHDir, `${configName}${rsaFileSuffix}`),
};

SSHConfigSections.append(newSSHConfigSection);

await fse.writeFile(SSHConfigPath, SSHConfig.stringify(SSHConfigSections));

log.info('update-SSH-config', newSSHConfigSection);
}
}

export async function removeSSHConfig(configName: string) {
const SSHConfigExists = await fse.pathExists(SSHConfigPath);
if (!SSHConfigExists) {
const error = new Error(`The SSH config path: ${SSHConfigPath} does not exist.`);
error.name = 'remove-ssh-config';
log.error(error);
throw error;
}
const SSHConfigContent = await fse.readFile(SSHConfigPath, 'utf-8');
const SSHConfigSections = SSHConfig.parse(SSHConfigContent);

const currentSSHConfigIndex = findSSHConfigSectionIndex(SSHConfigSections, configName);
if (currentSSHConfigIndex <= -1) {
return;
}
// remove SSH config
log.info('remove-SSH-config', SSHConfigSections[currentSSHConfigIndex]);
SSHConfigSections.splice(currentSSHConfigIndex, 1);
await fse.writeFile(SSHConfigPath, SSHConfig.stringify(SSHConfigSections));

// remove SSH private key and public key
const privateSSHKeyPath = path.join(SSHDir, `${configName}${rsaFileSuffix}`);
const publicSSHKeyPath = path.join(SSHDir, `${configName}${rsaFileSuffix}.pub`);
await fse.remove(privateSSHKeyPath);
await fse.remove(publicSSHKeyPath);
}

/**
* find the SSH config index in ssh config array by the configName(id)
*/
function findSSHConfigSectionIndex(SSHConfigSections: any[], configName: string) {
const privateKeyPath = path.join(SSHDir, `${configName}${rsaFileSuffix}`);

let currentSSHConfigIndex = -1;
// eslint-disable-next-line no-labels
loopLabel:
for (let index = 0; index < SSHConfigSections.length; index++) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

直接用 array findIndex?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

const section = SSHConfigSections[index];
const { config = [] } = section;
for (const { param, value } of config) {
if (param === 'IdentityFile' && value.replace('~', HOME_DIR) === privateKeyPath) {
currentSSHConfigIndex = index;
// eslint-disable-next-line no-labels
break loopLabel;
}
}
}

return currentSSHConfigIndex;
}
15 changes: 15 additions & 0 deletions main/ipc/getFolderPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as path from 'path';
import { dialog, ipcMain } from 'electron';

export default () => {
ipcMain.handle('get-folder-path', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openDirectory'],
});
if (canceled) {
return '';
}
const selectedFolderPath = filePaths[0];
return path.join(selectedFolderPath, '/');
});
};
Loading