@@ -12,7 +12,7 @@ interface SquashOptions {
1212 force ?: boolean
1313 filter ?: ( line : string ) => unknown
1414}
15- // chore: release
15+
1616/**
1717 * 合并本地最新的 N 个 Git 提交
1818 * @param n 需要合并的提交数量
@@ -23,11 +23,23 @@ interface SquashOptions {
2323export async function squashLastNCommits ( n : number , options ?: SquashOptions ) {
2424 validateInput ( n )
2525 checkGitRepository ( )
26- // checkWorkingDirectoryClean();
2726
2827 const commitCount = await getUnpushedCommitCount ( )
2928 const actualSquashCount = calculateActualSquashCount ( n , commitCount , options ?. force )
30- return await performSquash ( actualSquashCount , options ?. message , options ?. filter )
29+
30+ // 获取需要合并的提交的标签
31+ const commitHashes = await getCommitHashes ( actualSquashCount )
32+ const tags = await getLocalUnpushedTags ( commitHashes )
33+
34+ // 执行合并并打标签
35+ const squashSuccessful = await performSquash ( actualSquashCount , options ?. message , options ?. filter )
36+
37+ if ( squashSuccessful ) {
38+ // 为合并后的提交打上标签
39+ await addTagsToCommit ( commitHashes , tags )
40+ }
41+
42+ return squashSuccessful
3143}
3244
3345// 以下为内部工具函数
@@ -50,7 +62,6 @@ async function checkGitRepository(): Promise<void> {
5062async function getUnpushedCommitCount ( ) {
5163 try {
5264 const currentBranch = ( await x ( 'git' , [ 'rev-parse' , '--abbrev-ref' , 'HEAD' ] , execOptions ) ) . stdout . trim ( )
53-
5465 const upstreamExists = ( await x (
5566 'git' ,
5667 [ 'rev-parse' , '--abbrev-ref' , `${ currentBranch } @{u}` ] ,
@@ -64,14 +75,8 @@ async function getUnpushedCommitCount() {
6475 )
6576 }
6677
67- try {
68- const result = await x ( 'git' , [ 'rev-list' , '--count' , `${ currentBranch } @{u}..HEAD` ] , execOptions )
69-
70- return Number . parseInt ( result . stdout . trim ( ) , 10 )
71- }
72- catch {
73- throw new Error ( 'Failed to count local commits' )
74- }
78+ const result = await x ( 'git' , [ 'rev-list' , '--count' , `${ currentBranch } @{u}..HEAD` ] , execOptions )
79+ return Number . parseInt ( result . stdout . trim ( ) , 10 )
7580 }
7681 catch ( error ) {
7782 if ( error instanceof Error ) {
@@ -86,8 +91,6 @@ function calculateActualSquashCount(
8691 available : number ,
8792 force = false ,
8893) : number {
89- // if (available === 0) throw new Error('No commits to squash');
90-
9194 const actual = Math . min ( requested , available )
9295 if ( ! force && actual < requested ) {
9396 throw new Error (
@@ -104,15 +107,12 @@ async function performSquash(count: number, message?: string, filter?: SquashOpt
104107 }
105108
106109 try {
107- // 生成默认提交信息
108- const commitMessage = message
109- || `${ await getOriginalCommitMessages ( count , filter ) } `
110+ const commitMessage = message || `${ await getOriginalCommitMessages ( count , filter ) } `
110111
111112 // 回退 HEAD 但保留工作目录
112113 await x ( 'git' , [ 'reset' , '--soft' , `HEAD~${ count } ` ] , execOptions )
113114
114115 // 创建新提交
115- // 使用参数数组格式避免 shell 解析问题
116116 await x ( 'git' , [
117117 'commit' ,
118118 ...buildMessageArgs ( commitMessage ) ,
@@ -136,31 +136,20 @@ async function getOriginalCommitMessages(count: number, filter = (line: string)
136136 )
137137
138138 const msgList = res . stdout . split ( '\n' )
139-
140- const temp = msgList . filter ( item => item )
141-
142139 const msg = msgList . filter ( filter ) . join ( '\n' )
143140
144- console . log ( temp , count , JSON . stringify ( msg ) )
145141 return msg
146142}
147143
148- // 构建跨平台兼容的提交信息参数
149144function buildMessageArgs ( message : string ) : string [ ] {
150- // 方法 1:直接传递(适用于简单消息)
151- // return ['-m', message];
152-
153- // 方法 2:使用临时文件(推荐用于复杂格式)
154145 const tempFile = createTempFile ( message )
155146 return [ '-F' , tempFile ]
156147}
157148
158- // 创建临时文件处理复杂消息
159149function createTempFile ( content : string ) : string {
160150 const tempPath = path . join ( os . tmpdir ( ) , `git-commit-${ Date . now ( ) } .txt` )
161151 fs . writeFileSync ( tempPath , content , 'utf-8' )
162152
163- // 注册退出清理钩子
164153 process . once ( 'exit' , ( ) => {
165154 try {
166155 fs . unlinkSync ( tempPath )
@@ -170,6 +159,60 @@ function createTempFile(content: string): string {
170159 return tempPath
171160}
172161
162+ // 获取需要合并的提交的 Hash 值
163+ async function getCommitHashes ( count : number ) : Promise < string [ ] > {
164+ const result = await x ( 'git' , [ 'log' , '--format=%H' , `-n ${ count } ` , 'HEAD' ] , execOptions )
165+ return result . stdout . split ( '\n' ) . filter ( hash => hash )
166+ }
167+
168+ // 获取本地未推送到远程的标签
169+ async function getLocalUnpushedTags ( commits : string [ ] = [ ] ) : Promise < Record < string , string [ ] > > {
170+ const tagsForCommits : Record < string , string [ ] > = { }
171+
172+ // 获取本地所有标签
173+ const localTagsResult = await x ( 'git' , [ 'tag' , '-l' ] , execOptions )
174+ const localTags = new Set ( localTagsResult . stdout . split ( '\n' ) . filter ( tag => tag ) )
175+
176+ // 获取远程所有标签
177+ const remoteTagsResult = await x ( 'git' , [ 'ls-remote' , '--tags' , '--refs' ] , execOptions )
178+ const remoteTags = new Set ( remoteTagsResult . stdout . split ( '\n' ) . map ( line => line . split ( '\t' ) [ 1 ] ?. replace ( 'refs/tags/' , '' ) ) . filter ( tag => tag ) )
179+
180+ // 找出那些存在于本地但没有推送到远程的标签
181+ const unpushedLocalTags = [ ...localTags ] . filter ( tag => ! remoteTags . has ( tag ) )
182+
183+ // 为每个提交检查它是否有未推送的标签
184+ for ( const commit of commits ) {
185+ const result = await x ( 'git' , [ 'tag' , '--points-at' , commit ] , execOptions )
186+ const tags = result . stdout . split ( '\n' ) . filter ( tag => tag )
187+
188+ // 过滤出那些本地未推送的标签
189+ const unpushedTags = tags . filter ( tag => unpushedLocalTags . includes ( tag ) )
190+ tagsForCommits [ commit ] = unpushedTags
191+ }
192+
193+ return tagsForCommits
194+ }
195+
196+ // 为合并后的提交添加标签
197+ async function addTagsToCommit ( commits : string [ ] , tagsForCommits : Record < string , string [ ] > ) : Promise < void > {
198+ const lastCommit = commits [ commits . length - 1 ] // 获取最后一个合并提交的哈希值
199+
200+ if ( ! lastCommit )
201+ return
202+
203+ for ( const commit of commits ) {
204+ const tags = tagsForCommits [ commit ] || [ ]
205+ for ( const tag of tags ) {
206+ try {
207+ await x ( 'git' , [ 'tag' , tag , lastCommit ] , execOptions ) // 为最后的合并提交打标签
208+ }
209+ catch ( error ) {
210+ console . error ( `Failed to tag commit ${ commit } with tag ${ tag } : ${ error } ` )
211+ }
212+ }
213+ }
214+ }
215+
173216async function checkWorkingDirectoryClean ( ) : Promise < void > {
174217 try {
175218 await x ( 'git' , [ 'diff-index' , '--quiet' , 'HEAD' , '--' ] , execOptions )
0 commit comments