Skip to content
This repository was archived by the owner on Nov 12, 2025. It is now read-only.

Commit d6ceca2

Browse files
committed
feat(git): 添加获取本地 Git 仓库信息列表功能
- 新增 get_local_git_repo_info_list 函数,用于获取指定路径下的 Git 仓库信息列表 - 添加递归查找功能,可以深度搜索指定目录下的所有 Git 仓库信息 - 优化 parse_git_url 函数,增加返回仓库的 HTML 地址 - 新增 LocalGitInfoType、LocalGitInfoListType 等类型定义,用于规范 Git 仓库信息的数据结构
1 parent 7b4a1d2 commit d6ceca2

File tree

8 files changed

+218
-90
lines changed

8 files changed

+218
-90
lines changed

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@
6565
"test": "tsx test/index.ts"
6666
},
6767
"dependencies": {
68-
"axios": "npm:@candriajs/[email protected]",
68+
"@candriajs/exec": "^1.0.4",
69+
"axios": "npm:@candriajs/[email protected]",
6970
"color-convert": "^3.1.0",
7071
"dayjs": "^1.11.13",
7172
"exec": "npm:@candriajs/exec@^1.0.2",
@@ -74,8 +75,8 @@
7475
"https-proxy-agent": "^7.0.6",
7576
"jsonwebtoken": "^9.0.2",
7677
"language-colors": "^2.1.55",
77-
"lodash": "npm:@candriajs/[email protected].5",
78-
"simple-git": "^3.27.0",
78+
"lodash": "npm:@candriajs/[email protected].7",
79+
"simple-git": "npm:@candriajs/simple-git@^1.0.2",
7980
"socks-proxy-agent": "^8.0.5",
8081
"uuid": "^11.1.0"
8182
},

src/common/git.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import fs from 'node:fs'
12
import path from 'node:path'
23

34
import { simpleGit } from 'simple-git'
@@ -13,6 +14,11 @@ import {
1314
exists,
1415
parse_git_url
1516
} from '@/common/utils'
17+
import type {
18+
LocalGitInfoListOptionsType,
19+
LocalGitInfoListType,
20+
LocalGitInfoType
21+
} from '@/types'
1622

1723
/**
1824
* 获取 Git 版本
@@ -44,6 +50,137 @@ async function is_installed_git (): Promise<boolean> {
4450
return !!await get_git_version()
4551
}
4652

53+
/**
54+
* 获取本地 Git 仓库信息
55+
* @description 获取本地 Git 仓库信息,返回仓库名称、仓库路径、仓库地址等信息
56+
* @param local_path - 本地仓库路径
57+
* @returns Git 仓库信息
58+
* @example
59+
* ```ts
60+
* const info = await get_local_git_repo_info('D:/project/repo')
61+
* console.log(info)
62+
* -> {
63+
* name: 'repo',
64+
* path: 'D:/project/repo',
65+
* url: 'https://github.com/owner/repo.git',
66+
* html_url: 'https://github.com/owner/repo',
67+
* owner: 'owner',
68+
* repo: 'repo'
69+
* }
70+
* ```
71+
*/
72+
export async function get_local_git_repo_info (
73+
local_path: string
74+
): Promise<LocalGitInfoType | null> {
75+
try {
76+
if (!local_path) throw new Error(MissingLocalRepoPathMsg)
77+
if (!await exists(path.join(local_path)) || !await exists(path.join(local_path, '.git'))) return null
78+
const git = simpleGit(local_path)
79+
const remotes = await git.getRemotes(true)
80+
if (remotes.length > 0) {
81+
const remoteUrl = remotes[0].refs.push
82+
const { owner, repo, html_url } = parse_git_url(remoteUrl)
83+
return {
84+
name: path.basename(local_path),
85+
path: path.join(local_path),
86+
url: remoteUrl,
87+
html_url,
88+
owner,
89+
repo
90+
}
91+
}
92+
return null
93+
} catch (error) {
94+
throw new Error(`获取本地仓库信息失败: ${error}`)
95+
}
96+
}
97+
98+
/**
99+
* 获取本地仓库信息列表
100+
* @param local_path 本地仓库路径
101+
* @param options 配置项
102+
* - loop 是否递归查找
103+
* - maxDepth 递归最大深度
104+
* - dir 忽略的文件夹名称列表
105+
* @returns
106+
* @example
107+
* ```ts
108+
* // 无数据
109+
* const res = await get_local_git_repo_list('D:\\project\\GitHub', { loop: true, maxDepth: 5, dir: ['node_modules'] })
110+
* -> []
111+
* // 有数据
112+
* const res = await get_local_git_repo_list('D:\\project\\GitHub', { loop: true, maxDepth: 5, dir: ['node_modules'] })
113+
* -> [
114+
* {
115+
* "name": "GitHub",
116+
* "path": "D:\\project\\GitHub\\GitHub",
117+
* "url": "https://github.com/GitHub/GitHub.git",
118+
* "html_url": "https://github.com/GitHub/GitHub",
119+
* "owner": "GitHub",
120+
* "repo": "GitHub"
121+
* }
122+
* ]
123+
*/
124+
export async function get_local_git_repo_list (
125+
local_path: string,
126+
options: LocalGitInfoListOptionsType
127+
= {
128+
loop: false,
129+
maxDepth: 5,
130+
dir: []
131+
}
132+
): Promise<LocalGitInfoListType> {
133+
const { loop = false, maxDepth = 5, dir = [] } = options
134+
135+
const searchRepo = async (
136+
dirPath: string,
137+
currentDepth: number = 0
138+
): Promise<LocalGitInfoListType> => {
139+
try {
140+
if (!await exists(dirPath)) return []
141+
const stat = await fs.promises.stat(dirPath)
142+
if (!stat.isDirectory()) return []
143+
144+
const result: LocalGitInfoListType = []
145+
const currentRepoInfo = await get_local_git_repo_info(dirPath)
146+
147+
if (currentRepoInfo) {
148+
result.push(currentRepoInfo)
149+
}
150+
if (loop && currentDepth < maxDepth) {
151+
const dirItems = await fs.promises.readdir(dirPath, { withFileTypes: true })
152+
153+
const subResults = await Promise.all(
154+
dirItems
155+
.filter(item =>
156+
item.isDirectory() &&
157+
!dir.includes(item.name)
158+
)
159+
.map(async item => {
160+
const subPath = path.join(dirPath, item.name)
161+
return await searchRepo(subPath, currentDepth + 1)
162+
})
163+
)
164+
165+
result.push(...subResults.flat())
166+
}
167+
168+
return result
169+
} catch (error) {
170+
return []
171+
}
172+
}
173+
174+
try {
175+
if (!local_path) throw new Error(MissingLocalRepoPathMsg)
176+
if (!await is_installed_git()) throw new Error(GitClientNotInstalledMsg)
177+
178+
return await searchRepo(local_path)
179+
} catch (error) {
180+
throw new Error(`获取本地目录git仓库列表失败: ${(error as Error).message}`)
181+
}
182+
}
183+
47184
/**
48185
* 获取本地仓库的默认分支
49186
* @param local_path - 本地仓库路径

src/common/utils.ts

Lines changed: 34 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { exec as execCmd } from 'node:child_process'
21
import fs from 'node:fs'
32

3+
import { exec as execCmd, type ExecOptions, type ExecReturn, execSync as execSyncCmd } from '@candriajs/exec'
44
import convert, { type RGB } from 'color-convert'
55
import dayjs from 'dayjs'
66
import relativeTime from 'dayjs/plugin/relativeTime.js'
@@ -10,15 +10,13 @@ import LanguageColors from 'language-colors'
1010
import { basePath } from '@/root'
1111
import type {
1212
ContributionResult,
13-
ExecOptions,
14-
ExecReturn,
15-
RepoBaseParamType
13+
GitRepoType
1614
} from '@/types'
1715

1816
const localeCache = new Set<string>(['en'])
1917

2018
/**
21-
* 执行 shell 命令
19+
* 异步执行 shell 命令
2220
* @param cmd 命令
2321
* @param options 选项
2422
* @param options.log 是否打印日志 默认不打印
@@ -35,54 +33,36 @@ const localeCache = new Set<string>(['en'])
3533
* // -> 打印执行命令和结果
3634
* ```
3735
*/
38-
export function exec<T extends boolean = false> (
36+
export async function exec<T extends boolean = false> (
3937
cmd: string,
4038
options?: ExecOptions<T>
4139
): Promise<ExecReturn<T>> {
42-
const logger = console
43-
return new Promise((resolve) => {
44-
if (options?.log) {
45-
logger.info([
46-
'[exec] 执行命令:',
47-
`pwd: ${options?.cwd ?? process.cwd()}`,
48-
`cmd: ${cmd}`,
49-
`options: ${JSON.stringify(options)}`
50-
].join('\n'))
51-
}
52-
53-
execCmd(cmd, options, (error, stdout, stderr) => {
54-
if (options?.log) {
55-
const info = error as Error
56-
if (info.message) info.message = `\x1b[91m${info.message}\x1b[0m`
57-
logger.info([
58-
'[exec] 执行结果:',
59-
`stderr: ${stderr.toString()}`,
60-
`stdout: ${stdout.toString()}`,
61-
`error: ${JSON.stringify(info, null, 2)}`
62-
].join('\n'))
63-
}
64-
65-
if (options?.booleanResult) {
66-
return resolve((!error) as ExecReturn<T>)
67-
}
68-
69-
stdout = stdout.toString()
70-
stderr = stderr.toString()
71-
72-
if (options?.trim) {
73-
stdout = stdout.trim()
74-
stderr = stderr.trim()
75-
}
40+
return await execCmd(cmd, options)
41+
}
7642

77-
const value = {
78-
status: !error,
79-
error,
80-
stdout,
81-
stderr
82-
} as ExecReturn<T>
83-
resolve(value)
84-
})
85-
})
43+
/**
44+
* 同步执行 shell 命令
45+
* @param cmd 命令
46+
* @param options 选项
47+
* @param options.log 是否打印日志 默认不打印
48+
* @param options.booleanResult 是否只返回布尔值 表示命令是否成功执行 默认返回完整的结果
49+
* @example
50+
* ```ts
51+
* const { status, error, stdout, stderr } = execSync('ls -al')
52+
* // -> { status: true, error: null, stdout: '...', stderr: '...' }
53+
*
54+
* const status = execSync('ls -al', { booleanResult: true })
55+
* // -> true
56+
*
57+
* const { status, error, stdout, stderr } = execSync('ls -al', { log: true })
58+
* // -> 打印执行命令和结果
59+
* ```
60+
*/
61+
export function execSync<T extends boolean = false> (
62+
cmd: string,
63+
options?: ExecOptions<T>
64+
): ExecReturn<T> {
65+
return execSyncCmd(cmd, options)
8666
}
8767

8868
/**
@@ -202,14 +182,14 @@ export async function get_relative_time (
202182
* @example
203183
* ```ts
204184
* console.log(parse_git_url('https://github.com/user/repo.git'))
205-
* -> { owner: 'user', repo: 'repo' }
185+
* -> { owner: 'user', repo: 'repo', html_url: 'https://github.com/user/repo.git' }
206186
* console.log(parse_git_url('https://ghproxy.com/github.com/user/repo.git'))
207-
* -> { owner: 'user', repo: 'repo' }
187+
* -> { owner: 'user', repo: 'repo', html_url: 'https://github.com/user/repo.git' }
208188
* console.log(parse_git_url('https://ghproxy.com/https://github.com/user/repo.git'))
209-
* -> { owner: 'user', repo: 'repo' }
189+
* -> { owner: 'user', repo: 'repo', html_url: 'https://github.com/user/repo.git' }
210190
* ```
211191
*/
212-
export function parse_git_url (url: string): RepoBaseParamType {
192+
export function parse_git_url (url: string): GitRepoType {
213193
const proxyRegex = /^https?:\/\/[^/]+\/(?:https?:\/\/)?([^/]+\/[^/]+\/[^/]+)/
214194
const proxyMatch = url.match(proxyRegex)
215195

@@ -220,6 +200,7 @@ export function parse_git_url (url: string): RepoBaseParamType {
220200

221201
const info = GitUrlParse(url)
222202
return {
203+
html_url: info.href,
223204
owner: info.owner,
224205
repo: info.name
225206
}

src/exports/lodash.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as lodash from 'lodash'
22
export { lodash as default }
3-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
4-
// @ts-expect-error
3+
// @ts-expect-error TS2498
54
export * from 'lodash'

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
format_date,
33
get_langage_color,
4+
get_local_git_repo_list,
45
get_local_repo_default_branch,
56
get_relative_time,
67
get_remote_repo_default_branch
@@ -34,6 +35,7 @@ export {
3435
create_state_id,
3536
format_date,
3637
get_langage_color,
38+
get_local_git_repo_list,
3739
get_local_repo_default_branch,
3840
get_relative_time,
3941
get_remote_repo_default_branch

src/types/base/git.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/** 本地Git仓库信息 */
2+
export interface LocalGitInfoType {
3+
/** 文件夹名称 */
4+
name: string
5+
/** 文件夹路径 */
6+
path: string
7+
/**
8+
* git地址,原始地址可能是反代的地址
9+
* */
10+
url: string
11+
/**
12+
* 仓库地址, 这个是经过处理的,保证是不经过任何代理地址的地址
13+
* */
14+
html_url: string
15+
/** 仓库名称 */
16+
owner: string
17+
/** 仓库名称 */
18+
repo: string
19+
}
20+
21+
/** 获取本地路径的Git仓库信息列表 */
22+
export type LocalGitInfoListType = Array<LocalGitInfoType>
23+
24+
export interface LocalGitInfoListOptionsType {
25+
/** 是否递归查找 */
26+
loop?: boolean
27+
/** 递归最大深度 */
28+
maxDepth?: number
29+
/** 忽略目录 */
30+
dir?: string[]
31+
}

src/types/base/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from '@/types/base/base'
2+
export * from '@/types/base/git'
23
export * from '@/types/base/pkg'
34
export * from '@/types/base/request'
45
export * from '@/types/base/response'

0 commit comments

Comments
 (0)