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 all 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 @@ -8,6 +8,7 @@ export const PACKAGE_JSON_FILE_NAME = 'package.json';
export const TOOLKIT_DIR = path.join(process.env.HOME, '.toolkit');
export const TOOLKIT_TMP_DIR = path.join(TOOLKIT_DIR, 'tmp');
export const TOOLKIT_PACKAGES_DIR = path.join(TOOLKIT_DIR, 'packages');
export const TOOLKIT_USER_GIT_CONFIG_DIR = path.join(TOOLKIT_DIR, 'git');

export const DEFAULT_LOCAL_PACKAGE_INFO: ILocalPackageInfo = {
localVersion: null,
Expand Down Expand Up @@ -35,3 +36,5 @@ export const NPM_REGISTRY = 'https://registry.npmjs.org/';
export const TAOBAO_NPM_REGISTRY = 'https://registry.npm.taobao.org';
export const ALI_NPM_REGISTRY = 'https://registry.npm.alibaba-inc.com/';
export const TAOBAO_NODE_MIRROR = 'https://npm.taobao.org/mirrors/node';
// git
export const GLOBAL_GITCONFIG_PATH = path.join(process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'], '.gitconfig');
187 changes: 187 additions & 0 deletions main/git/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import * as path from 'path';
import ini = require('ini');
import * as fse from 'fs-extra';
import * as globby from 'globby';
import { IAddUserConfig, IUserConfig } from '../types/git';
import { GLOBAL_GITCONFIG_PATH, TOOLKIT_USER_GIT_CONFIG_DIR } from '../constants';
import log from '../utils/log';
import {
updateSSHConfig,
getSSHConfig,
removeSSHConfig,
addSSHConfig,
} from './ssh';

const USER_GIT_CONFIG_FILENAME_PREFIX = '.gitconfig-';
const IGNORE_CONFIG_KEYS = ['gitDir'];

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 getExistedUserGitConfigNames() {
const filenames = await getUserGitConfigFilenames();
return filenames.map((filename: string) => filename.replace(USER_GIT_CONFIG_FILENAME_PREFIX, ''));
}

/**
* get user git config list
*/
export async function getUserGitConfigs(): Promise<IUserConfig[]> {
const gitConfigFilenames = await getUserGitConfigFilenames();
const userGitDirs = await getUserGitDirs();

const userGitConfigs = [];
for (const gitConfigFilename of gitConfigFilenames) {
const configPath = path.join(TOOLKIT_USER_GIT_CONFIG_DIR, gitConfigFilename);
if (!fse.pathExistsSync(configPath)) {
continue;
}
const gitConfig = await parseGitConfig(configPath);
const filename = path.basename(configPath);
const configName = filename.replace(USER_GIT_CONFIG_FILENAME_PREFIX, '');
const { SSHPublicKey } = await getSSHConfig(configName);
userGitConfigs.push({
...gitConfig,
SSHPublicKey,
configName,
gitDirs: userGitDirs[configPath] || [],
});
}

return userGitConfigs;
}

export async function addUserGitConfig(userGitConfig: IAddUserConfig) {
const { configName, user: { name: userName, hostName } } = userGitConfig;
const gitConfigPath = getGitConfigPath(configName);

checkUserGitConfigExists(configName, gitConfigPath);

await fse.createFile(gitConfigPath);
// do not save the configName to the gitconfig file
delete userGitConfig.configName;
await writeGitConfig(gitConfigPath, userGitConfig);
await addSSHConfig({ hostName, configName, userName });
}

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

IGNORE_CONFIG_KEYS.forEach((key) => {
delete gitConfig[key];
});
// save to ~/.toolkit/git/.gitconfig-${configName}
const gitConfigPath = `${path.join(TOOLKIT_USER_GIT_CONFIG_DIR, `${USER_GIT_CONFIG_FILENAME_PREFIX}${configName}`)}`;
await writeGitConfig(gitConfigPath, gitConfig);

log.info('update-user-git-config', configName, gitConfig);
}

async function getUserGitDirs() {
const globalGitConfig = await getGlobalGitConfig();

const userGitDirs = {};

const configKeys = Object.keys(globalGitConfig);

for (const configKey of configKeys) {
const { path: gitConfigPath } = globalGitConfig[configKey];
if (!gitConfigPath) {
continue;
}
if (!userGitDirs[gitConfigPath]) {
userGitDirs[gitConfigPath] = [];
}
const gitDir = configKey.replace(/includeIf "gitdir:(.*)"/, (match, p1) => p1);
userGitDirs[gitConfigPath].push(gitDir);
}

return userGitDirs;
}

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

const originIncludeIfKey = `includeIf "gitdir:${originGitDir}"`;
const currentIncludeIfKey = `includeIf "gitdir:${currentGitDir}"`;

delete globalGitConfig[originIncludeIfKey];
const gitConfigPath = getGitConfigPath(configName);
globalGitConfig[currentIncludeIfKey] = { path: gitConfigPath };
await writeGitConfig(GLOBAL_GITCONFIG_PATH, globalGitConfig);

log.info('update-user-git-dir: ', currentIncludeIfKey, globalGitConfig[currentIncludeIfKey]);
}

export async function removeUserGitDir(gitDir: string, configName: string) {
const gitConfigPath = getGitConfigPath(configName);
const globalGitConfig = await parseGitConfig(GLOBAL_GITCONFIG_PATH);

const includeIfKey = `includeIf "gitdir:${gitDir}"`;
const includeIfValue = globalGitConfig[includeIfKey];
if (includeIfValue && includeIfValue.path === gitConfigPath) {
delete globalGitConfig[includeIfKey];
await writeGitConfig(GLOBAL_GITCONFIG_PATH, globalGitConfig);
log.info('remove-user-git-dir: ', includeIfKey, gitConfigPath);
} else {
const error = new Error(`Can not remove ${gitDir}. The ${includeIfValue} is not found.`);
log.error(error);
throw error;
}
}

export async function removeUserGitConfig(configName: string, gitDirs = []) {
await removeSSHConfig(configName);

for (const gitDir of gitDirs) {
await removeUserGitDir(gitDir, configName);
}

// remove the gitconfig file
const gitConfigPath = getGitConfigPath(configName);
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));
log.info('write-git-config', config);
}

function checkUserGitConfigExists(configName: string, gitConfigPath: string) {
if (fse.pathExistsSync(gitConfigPath)) {
const err = new Error(`${configName} config has existed,please use other config name.`);
err.name = 'add-user-git-config';
log.error(err);
throw err;
}
}

async function getUserGitConfigFilenames() {
return await globby([`${USER_GIT_CONFIG_FILENAME_PREFIX}*`], { cwd: TOOLKIT_USER_GIT_CONFIG_DIR, dot: true });
}

/**
* get absolute git config path in ~/.toolkit/git/
*/
function getGitConfigPath(configName: string) {
return path.join(TOOLKIT_USER_GIT_CONFIG_DIR, `${USER_GIT_CONFIG_FILENAME_PREFIX}${configName}`);
}
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';
170 changes: 170 additions & 0 deletions main/git/ssh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
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');
const SSHConfigPath = path.join(SSHDir, 'config');

/**
* generate SSH public key and private key
*/
export async function generateSSHKey(configName: string, userEmail: string) {
const location = path.join(SSHDir, configName);
return 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;
}

/**
* get HostName and public SSH Key from ~/.ssh
*/
export async function getSSHConfig(configName: string) {
let SSHPublicKey = '';
const privateKeyPath = path.join(SSHDir, `${configName}`);
const SSHConfigSections = await getSSHConfigs();
/* eslint-disable no-labels */
loopLabel:
for (const section of SSHConfigSections) {
const { config = [] } = section;
for (const { param, value } of config) {
if (param === 'IdentityFile' && value.replace('~', HOME_DIR) === privateKeyPath) {
SSHPublicKey = await getSSHPublicKey(privateKeyPath);
/* eslint-disable no-labels */
break loopLabel;
}
}
}

return { SSHPublicKey };
}

/**
* 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}`),
};
SSHConfigSections.append(newSSHConfigSection);

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

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

/**
* save SSH Config to ~/.ssh/config
*/
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);
throw error;
}
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}`),
};
SSHConfigSections.append(newSSHConfigSection);
await fse.writeFile(SSHConfigPath, SSHConfig.stringify(SSHConfigSections));

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

/**
* Remove SSH config section
*/
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
const removeSection = SSHConfigSections.splice(currentSSHConfigIndex, 1);
await fse.writeFile(SSHConfigPath, SSHConfig.stringify(SSHConfigSections));
// remove SSH private key and public key
const privateSSHKeyPath = path.join(SSHDir, `${configName}`);
const publicSSHKeyPath = path.join(SSHDir, `${configName}.pub`);
await fse.remove(privateSSHKeyPath);
await fse.remove(publicSSHKeyPath);

log.info('remove-SSH-config', removeSection);
}

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

const currentSSHConfigIndex = SSHConfigSections.findIndex(({ config = [] }) => {
return config.some(({ param, value }) => {
return param === 'IdentityFile' && value.replace('~', HOME_DIR) === privateKeyPath;
});
});

return currentSSHConfigIndex;
}
Loading