-
Notifications
You must be signed in to change notification settings - Fork 73
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
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
b46b09c
feat: support get global config
luhc228 41a419f
feat: add get user git config
luhc228 d8a13ab
feat: support edit delete user git config
luhc228 535c0a9
fix: edit user git config error
luhc228 1ac87d5
feat: support select folder
luhc228 8d920b3
feat: add desc
luhc228 a6ea698
feat: add sshkey display
luhc228 b62d4dc
feat: support update and delete ssh config
luhc228 1d7b480
fix: style
luhc228 a1ea70a
fix: param name
luhc228 2eb90b7
fix: ipc name
luhc228 6a4fdf2
fix: comment
luhc228 8084e22
chore: remove comment
luhc228 c836481
Merge branch 'release-next' into feat/git
luhc228 f7807f4
refactor: add git config
luhc228 a0f8878
refactor: git manager
luhc228 46f88c4
feat: add tips
luhc228 35ed852
feat: add type
luhc228 9d397f1
fix: conflict
luhc228 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() { | ||
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './config'; | ||
export * from './ssh'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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++) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 直接用 array findIndex? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, '/'); | ||
}); | ||
}; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
建议注释下,说明怎么取 步骤是什么 可能的问题有什么
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done