-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- 新增周报生成命令和配置项 - 添加周报生成服务,支持Git和SVN提交记录采集 - 实现周报WebView界面,展示工作内容和工时统计 - 支持自动计算合理工时分配 - 新增周报相关类型定义和常量配置 🔧 chore(config): 更新package.json配置 - 添加生成周报命令配置 - 新增周报模板和自动保存选项 - 配置周报工时统计相关参数 🔧 chore(command): 扩展命令管理器 - 在命令管理器中注册周报生成命令 - 添加周报命令常量定义
- Loading branch information
1 parent
29e89ce
commit b557c7a
Showing
8 changed files
with
522 additions
and
1 deletion.
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,45 @@ | ||
import * as vscode from "vscode"; | ||
import { BaseCommand } from "./BaseCommand"; | ||
import { NotificationHandler } from "../utils/NotificationHandler"; | ||
import { ProgressHandler } from "../utils/ProgressHandler"; | ||
import { LocalizationManager } from "../utils/LocalizationManager"; | ||
import { WeeklyReportPanel } from "../webview/WeeklyReportPanel"; | ||
import { SCMFactory } from "../scm/SCMProvider"; | ||
|
||
export class GenerateWeeklyReportCommand extends BaseCommand { | ||
async validateConfig(): Promise<boolean> { | ||
const scmProvider = await SCMFactory.detectSCM(); | ||
if (!scmProvider) { | ||
const locManager = LocalizationManager.getInstance(); | ||
await NotificationHandler.error( | ||
locManager.getMessage("scm.not.detected") | ||
); | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
async execute(): Promise<void> { | ||
try { | ||
if (!(await this.validateConfig())) { | ||
return; | ||
} | ||
|
||
await ProgressHandler.withProgress( | ||
LocalizationManager.getInstance().getMessage("weeklyReport.generating"), | ||
async () => { | ||
WeeklyReportPanel.createOrShow(this.context.extensionUri); | ||
} | ||
); | ||
} catch (error) { | ||
if (error instanceof Error) { | ||
await NotificationHandler.error( | ||
LocalizationManager.getInstance().format( | ||
"weeklyReport.generation.failed", | ||
error.message | ||
) | ||
); | ||
} | ||
} | ||
} | ||
} |
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,233 @@ | ||
import * as vscode from "vscode"; | ||
import { exec } from "child_process"; | ||
import { promisify } from "util"; | ||
import * as fs from "fs"; | ||
import * as path from "path"; | ||
import * as os from "os"; | ||
import { WorkItem, Repository } from "../types/weeklyReport"; | ||
import { SCMFactory } from "../scm/SCMProvider"; | ||
|
||
const execAsync = promisify(exec); | ||
const readFileAsync = promisify(fs.readFile); | ||
const readdirAsync = promisify(fs.readdir); | ||
|
||
export class WeeklyReportService { | ||
private readonly WORK_DAYS = 5; // 固定为5个工作日 | ||
private readonly HOURS_PER_DAY = 8; // 每天8小时 | ||
private allLogs: string[] = []; | ||
|
||
constructor() {} | ||
|
||
async generate(): Promise<WorkItem[]> { | ||
const scmProvider = await SCMFactory.detectSCM(); | ||
if (!scmProvider) { | ||
throw new Error("No SCM provider detected"); | ||
} | ||
|
||
// 获取作者信息 | ||
const author = await this.getAuthor(scmProvider.type); | ||
if (!author) { | ||
throw new Error("Unable to detect author information"); | ||
} | ||
|
||
const repositories = await this.findRepositories(); | ||
await this.collectLogs(repositories, author); | ||
return this.processLogs(); | ||
} | ||
|
||
private async getSvnAuthor(): Promise<string | undefined> { | ||
try { | ||
const svnAuthPath = path.join( | ||
os.homedir(), | ||
".subversion", | ||
"auth", | ||
"svn.simple" | ||
); | ||
const files = await readdirAsync(svnAuthPath); | ||
|
||
// 读取第一个认证文件 | ||
if (files.length > 0) { | ||
const authFile = path.join(svnAuthPath, files[0]); | ||
const content = await readFileAsync(authFile, "utf-8"); | ||
|
||
// 使用正则表达式匹配用户名 | ||
const usernameMatch = content.match(/username="([^"]+)"/); | ||
if (usernameMatch && usernameMatch[1]) { | ||
return usernameMatch[1]; | ||
} | ||
} | ||
|
||
// 如果无法从配置文件获取,尝试从 svn info 获取 | ||
const { stdout } = await execAsync("svn info --show-item author"); | ||
return stdout.trim(); | ||
} catch (error) { | ||
console.error(`Error getting SVN author: ${error}`); | ||
return undefined; | ||
} | ||
} | ||
|
||
private async getAuthor(type: "git" | "svn"): Promise<string | undefined> { | ||
try { | ||
if (type === "git") { | ||
const { stdout } = await execAsync("git config user.name"); | ||
return stdout.trim(); | ||
} else { | ||
return await this.getSvnAuthor(); | ||
} | ||
} catch (error) { | ||
console.error(`Error getting author: ${error}`); | ||
return undefined; | ||
} | ||
} | ||
|
||
private async collectLogs(repositories: Repository[], author: string) { | ||
for (const repo of repositories) { | ||
if (repo.type === "git") { | ||
await this.collectGitLogs(repo.path, author); | ||
} else { | ||
await this.collectSvnLogs(repo.path, author); | ||
} | ||
} | ||
} | ||
|
||
private getLastWeekDates(): { start: Date; end: Date } { | ||
const today = new Date(); | ||
const currentDay = today.getDay(); | ||
|
||
// 计算上周一的日期 | ||
const lastMonday = new Date(today); | ||
lastMonday.setDate(today.getDate() - currentDay - 7 + 1); | ||
lastMonday.setHours(0, 0, 0, 0); | ||
|
||
// 计算上周五的日期 | ||
const lastFriday = new Date(lastMonday); | ||
lastFriday.setDate(lastMonday.getDate() + 4); | ||
lastFriday.setHours(23, 59, 59, 999); | ||
|
||
return { start: lastMonday, end: lastFriday }; | ||
} | ||
|
||
private async collectGitLogs(repoPath: string, author: string) { | ||
const { start, end } = this.getLastWeekDates(); | ||
const startDate = start.toISOString(); | ||
const endDate = end.toISOString(); | ||
|
||
const command = `git log --after="${startDate}" --before="${endDate}" --author="${author}" --pretty=format:"%s"`; | ||
try { | ||
const { stdout } = await execAsync(command, { cwd: repoPath }); | ||
if (stdout.trim()) { | ||
this.allLogs = this.allLogs.concat(stdout.trim().split("\n")); | ||
} | ||
} catch (error) { | ||
console.error(`Error collecting Git logs: ${error}`); | ||
} | ||
} | ||
|
||
private async findRepositories(): Promise<Repository[]> { | ||
const repositories: Repository[] = []; | ||
const workspaceFolders = vscode.workspace.workspaceFolders; | ||
|
||
if (!workspaceFolders) { | ||
return repositories; | ||
} | ||
|
||
for (const folder of workspaceFolders) { | ||
try { | ||
// 检查是否是 Git 仓库 | ||
const { stdout: gitOutput } = await execAsync( | ||
"git rev-parse --git-dir", | ||
{ | ||
cwd: folder.uri.fsPath, | ||
} | ||
); | ||
if (gitOutput) { | ||
repositories.push({ | ||
type: "git", | ||
path: folder.uri.fsPath, | ||
}); | ||
continue; | ||
} | ||
} catch {} | ||
|
||
try { | ||
// 检查是否是 SVN 仓库 | ||
const { stdout: svnOutput } = await execAsync("svn info", { | ||
cwd: folder.uri.fsPath, | ||
}); | ||
if (svnOutput) { | ||
repositories.push({ | ||
type: "svn", | ||
path: folder.uri.fsPath, | ||
}); | ||
} | ||
} catch {} | ||
} | ||
|
||
return repositories; | ||
} | ||
|
||
private async collectSvnLogs(repoPath: string, author: string) { | ||
const { start, end } = this.getLastWeekDates(); | ||
try { | ||
const command = `svn log -r {${start.toISOString()}}:{${end.toISOString()}} --search="${author}" --xml`; | ||
const { stdout } = await execAsync(command, { cwd: repoPath }); | ||
const matches = stdout.matchAll(/<msg>([\s\S]*?)<\/msg>/g); | ||
for (const match of matches) { | ||
if (match[1] && match[1].trim()) { | ||
this.allLogs.push(match[1].trim()); | ||
} | ||
} | ||
} catch (error) { | ||
console.error(`Error collecting SVN logs: ${error}`); | ||
} | ||
} | ||
|
||
private processLogs(): WorkItem[] { | ||
const uniqueLogs = [...new Set(this.allLogs)]; | ||
const workItems: WorkItem[] = []; | ||
const totalHours = this.WORK_DAYS * this.HOURS_PER_DAY; | ||
const hoursPerLog = totalHours / uniqueLogs.length; | ||
|
||
uniqueLogs.forEach((log, index) => { | ||
let timeSpent = hoursPerLog; | ||
if (index === uniqueLogs.length - 1) { | ||
const totalAllocated = workItems.reduce( | ||
(sum, item) => sum + parseFloat(item.time), | ||
0 | ||
); | ||
const remaining = totalHours - totalAllocated; | ||
if (remaining > 0) { | ||
timeSpent = remaining; | ||
} | ||
} | ||
|
||
workItems.push({ | ||
content: log, | ||
time: `${timeSpent.toFixed(1)}h`, | ||
description: this.generateDescription(log), | ||
}); | ||
}); | ||
|
||
return workItems; | ||
} | ||
|
||
private generateDescription(log: string): string { | ||
// 移除常见的提交前缀,如 feat:, fix: 等 | ||
const cleanLog = log.replace( | ||
/^(feat|fix|docs|style|refactor|test|chore|perf):\s*/i, | ||
"" | ||
); | ||
|
||
// 移除 emoji | ||
const noEmoji = cleanLog | ||
.replace(/:[a-z_]+:|�[\u{1F300}-\u{1F6FF}]/gu, "") | ||
.trim(); | ||
|
||
// 如果内容过短,添加更多描述 | ||
if (noEmoji.length < 20) { | ||
return `完成${noEmoji}相关功能的开发和调试工作`; | ||
} | ||
|
||
return noEmoji; | ||
} | ||
} |
Oops, something went wrong.