feat: 新增 git.time 修改最后一次提交时间命令
This commit is contained in:
+48
-3
@@ -2,16 +2,61 @@
|
|||||||
|
|
||||||
> 返回 [README.md](README.md)
|
> 返回 [README.md](README.md)
|
||||||
|
|
||||||
按日期范围批量调整提交时间,然后强推。用于把一批提交的时分秒统一挪到某个时间段。
|
按日期范围批量调整提交时间,或修改最后一次提交的时间,然后强推。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
kopsc git retime 2026-10-01 2026-10-15 # 预览:会改哪些提交、改成什么时间
|
kopsc git retime 2026-10-01 2026-10-15 # 预览:会改哪些提交、改成什么时间
|
||||||
kopsc git retime 2026-10-01 2026-10-15 --yes # 确认后真正改写并强推
|
kopsc git retime 2026-10-01 2026-10-15 --yes # 确认后真正改写并强推
|
||||||
|
|
||||||
|
kopsc git time 2026-11-05 22:30:00 # 预览:修改最后一次提交的时间
|
||||||
|
kopsc git time 2026-11-05 22:30:00 --yes # 确认后修改并强推
|
||||||
```
|
```
|
||||||
|
|
||||||
> ⚠️ **这会改写历史并强推**,提交 SHA 会全部变化。默认只预览、不改任何东西,必须显式加 `--yes` 才会执行。
|
> ⚠️ **这会改写历史并强推**,提交 SHA 会变化。默认只预览、不改任何东西,必须显式加 `--yes` 才会执行。
|
||||||
|
|
||||||
## 改写提交时间(git.retime)
|
## 修改最后一次提交时间(git.time)
|
||||||
|
|
||||||
|
`kopsc git time <时间>` 把本地最后一次提交的作者时间和提交时间改为用户指定的时间,然后强制推送。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kopsc git time 2026-11-05 22:30:00 # 预览
|
||||||
|
kopsc git time 2026-11-05 22:30:00 --yes # 修改并强推
|
||||||
|
kopsc git time 2026-11-05 22:30:00 --no-push # 只改本地,不推送
|
||||||
|
```
|
||||||
|
|
||||||
|
### 时间格式
|
||||||
|
|
||||||
|
时间格式必须是 `yyyy-MM-dd HH:mm:ss`,如 `2026-11-05 22:30:00`。格式不正确会报错。
|
||||||
|
|
||||||
|
### 预览示例
|
||||||
|
|
||||||
|
```text
|
||||||
|
修改最后一次提交的时间
|
||||||
|
|
||||||
|
提交: abc1234 feat: 新增功能
|
||||||
|
当前时间: 2026-11-05 10:15:30
|
||||||
|
修改为: 2026-11-05 22:30:00
|
||||||
|
|
||||||
|
将强推到 origin/main
|
||||||
|
|
||||||
|
这是破坏性操作,会改变提交历史。加 --yes 确认执行。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 实现细节
|
||||||
|
|
||||||
|
同时修改 Author Date 和 Committer Date,确保远程页面(如 Gitea)显示正确时间。
|
||||||
|
|
||||||
|
### 选项
|
||||||
|
|
||||||
|
| 选项 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `<时间>` | 必填,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||||
|
| `--remote <名称>` | 强推到哪个远程,默认取唯一远程 |
|
||||||
|
| `--branch <名称>` | 操作的分支,默认当前分支 |
|
||||||
|
| `--no-push` | 只改写本地,不推送 |
|
||||||
|
| `-y`, `--yes` | 确认执行(没有它只预览) |
|
||||||
|
|
||||||
|
## 批量改写提交时间(git.retime)
|
||||||
|
|
||||||
`kopsc git retime <开始日期> <结束日期> [选项]` 把该日期范围内(含两端)的提交,
|
`kopsc git retime <开始日期> <结束日期> [选项]` 把该日期范围内(含两端)的提交,
|
||||||
**保留各自的原日期**,只把时分秒重排到 **21:00–23:00** 之间,然后 `git push --force-with-lease`。
|
**保留各自的原日期**,只把时分秒重排到 **21:00–23:00** 之间,然后 `git push --force-with-lease`。
|
||||||
|
|||||||
@@ -143,17 +143,17 @@ kopsc 的命令按用途分组,每组的完整说明(命令表、选项、
|
|||||||
| 分组 | 用途 | 文档 |
|
| 分组 | 用途 | 文档 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| **docker** | 容器与镜像:查看、启停、日志、清理、导出/导入、Compose | [README.docker.md](README.docker.md) |
|
| **docker** | 容器与镜像:查看、启停、日志、清理、导出/导入、Compose | [README.docker.md](README.docker.md) |
|
||||||
| **net** | 网络与端口:端口被谁占用、监听端口、ping、DNS、路由追踪、HTTP 诊断、网卡信息、连接统计、网速测试 | [README.net.md](README.net.md) |
|
| **net** | 网络与端口:端口被谁占用、监听端口、ping、DNS、路由追踪、HTTP 诊断、网卡信息、连接统计、网速测试、HTTP 文件服务器 | [README.net.md](README.net.md) |
|
||||||
| **disk** | 磁盘与文件占用:哪个目录、哪个文件最占空间 | [README.disk.md](README.disk.md) |
|
| **disk** | 磁盘与文件占用:哪个目录、哪个文件最占空间、inode 使用、一键磁盘健康检查 | [README.disk.md](README.disk.md) |
|
||||||
| **diagnose** | 容器 / Pod 状态根因诊断(自动识别 Docker 与 K8s) | [README.diagnose.md](README.diagnose.md) |
|
| **diagnose** | 容器 / Pod 状态根因诊断(自动识别 Docker 与 K8s) | [README.diagnose.md](README.diagnose.md) |
|
||||||
| **proc** | 本机进程:列出进程、后台运行、等待退出、按资源排序、按名称终止、进程树 | [README.proc.md](README.proc.md) |
|
| **proc** | 本机进程:列出进程、后台运行、等待退出、按资源排序、按名称终止、进程树 | [README.proc.md](README.proc.md) |
|
||||||
| **java** | Java 应用:按 JDK 版本套用调优参数并运行 jar | [README.java.md](README.java.md) |
|
| **java** | Java 应用:按 JDK 版本套用调优参数并运行 jar | [README.java.md](README.java.md) |
|
||||||
| **git** | Git 历史:按日期范围调整提交时间并强推 | [README.git.md](README.git.md) |
|
| **git** | Git 历史:按日期范围调整提交时间、修改最后一次提交时间并强推 | [README.git.md](README.git.md) |
|
||||||
| **pkg** | 系统软件包:列出已安装包、刷新包管理器索引 | [README.pkg.md](README.pkg.md) |
|
| **pkg** | 系统软件包:列出、搜索、安装、卸载、查看详情、刷新包管理器索引 | [README.pkg.md](README.pkg.md) |
|
||||||
| **user** | 用户管理:列出用户、授予或移除 root/sudo 权限、查看用户信息 | [README.user.md](README.user.md) |
|
| **user** | 用户管理:列出用户、授予或移除 root/sudo 权限、查看用户信息 | [README.user.md](README.user.md) |
|
||||||
| **file** | 文件查找:按大小、修改时间、名称查找文件、查找空文件/空目录 | [README.file.md](README.file.md) |
|
| **file** | 文件查找:按大小、修改时间、名称查找文件、查找空文件/空目录 | [README.file.md](README.file.md) |
|
||||||
| **svc** | 服务管理:列出服务、查看状态、启用/禁用、查看日志 | [README.svc.md](README.svc.md) |
|
| **svc** | 服务管理:列出服务、查看状态、启用/禁用、查看日志 | [README.svc.md](README.svc.md) |
|
||||||
| **archive** | 压缩文件:自动识别格式并解压 | [README.archive.md](README.archive.md) |
|
| **archive** | 压缩文件:创建、查看内容、解压 | [README.archive.md](README.archive.md) |
|
||||||
| **sys** | 系统信息:OS、运行时间、CPU、内存、磁盘一览 | [README.sys.md](README.sys.md) |
|
| **sys** | 系统信息:OS、运行时间、CPU、内存、磁盘一览 | [README.sys.md](README.sys.md) |
|
||||||
| **log** | 系统日志:列出日志来源、搜索关键字、查看最近日志 | [README.log.md](README.log.md) |
|
| **log** | 系统日志:列出日志来源、搜索关键字、查看最近日志 | [README.log.md](README.log.md) |
|
||||||
| **fw** | 防火墙:查看状态、开放/关闭端口 | [README.fw.md](README.fw.md) |
|
| **fw** | 防火墙:查看状态、开放/关闭端口 | [README.fw.md](README.fw.md) |
|
||||||
@@ -179,6 +179,8 @@ kopsc 的命令按用途分组,每组的完整说明(命令表、选项、
|
|||||||
| 搬镜像 | `kopsc docker save alpine:3.20` / `kopsc docker load alpine_3.20.tar` |
|
| 搬镜像 | `kopsc docker save alpine:3.20` / `kopsc docker load alpine_3.20.tar` |
|
||||||
| 查端口 | `kopsc net pids 8080`、`kopsc net ports` |
|
| 查端口 | `kopsc net pids 8080`、`kopsc net ports` |
|
||||||
| 查磁盘占用 | `kopsc disk usage`、`kopsc disk usage /var --depth 2` |
|
| 查磁盘占用 | `kopsc disk usage`、`kopsc disk usage /var --depth 2` |
|
||||||
|
| 查 inode 使用 | `kopsc disk inode` |
|
||||||
|
| 磁盘排障 | `kopsc disk check`、`kopsc disk check --all` |
|
||||||
| 排障 | `kopsc diagnose web` |
|
| 排障 | `kopsc diagnose web` |
|
||||||
| 后台跑服务 | `kopsc proc nohup ./server` |
|
| 后台跑服务 | `kopsc proc nohup ./server` |
|
||||||
| 等进程退出 | `kopsc proc wait nginx`、`kopsc proc wait --pid 1234 --timeout 30` |
|
| 等进程退出 | `kopsc proc wait nginx`、`kopsc proc wait --pid 1234 --timeout 30` |
|
||||||
@@ -186,6 +188,11 @@ kopsc 的命令按用途分组,每组的完整说明(命令表、选项、
|
|||||||
| 按名字杀进程 | `kopsc proc killname nginx --yes` |
|
| 按名字杀进程 | `kopsc proc killname nginx --yes` |
|
||||||
| 跑 Java 应用 | `kopsc java run app.jar`、`kopsc java run --jdk 17 --print-flags app.jar` |
|
| 跑 Java 应用 | `kopsc java run app.jar`、`kopsc java run --jdk 17 --print-flags app.jar` |
|
||||||
| 调整提交时间 | `kopsc git retime 2026-10-01 2026-10-15`(加 `--yes` 才执行) |
|
| 调整提交时间 | `kopsc git retime 2026-10-01 2026-10-15`(加 `--yes` 才执行) |
|
||||||
|
| 修改最后一次提交时间 | `kopsc git time 2026-11-05 22:30:00`(加 `--yes` 才执行) |
|
||||||
|
| 搜索软件包 | `kopsc pkg search nginx` |
|
||||||
|
| 安装软件包 | `kopsc pkg install nginx --yes` |
|
||||||
|
| 卸载软件包 | `kopsc pkg remove nginx --yes` |
|
||||||
|
| 查看软件包详情 | `kopsc pkg info nginx` |
|
||||||
| 刷新软件源 | `kopsc pkg refreshindex` |
|
| 刷新软件源 | `kopsc pkg refreshindex` |
|
||||||
| 授予用户 root 权限 | `kopsc user grant alice --yes` |
|
| 授予用户 root 权限 | `kopsc user grant alice --yes` |
|
||||||
| 移除用户 root 权限 | `kopsc user revoke alice --yes` |
|
| 移除用户 root 权限 | `kopsc user revoke alice --yes` |
|
||||||
@@ -195,7 +202,10 @@ kopsc 的命令按用途分组,每组的完整说明(命令表、选项、
|
|||||||
| 看服务状态 | `kopsc svc status nginx` |
|
| 看服务状态 | `kopsc svc status nginx` |
|
||||||
| 启用/禁用服务 | `kopsc svc enable nginx --yes` / `kopsc svc disable nginx --yes` |
|
| 启用/禁用服务 | `kopsc svc enable nginx --yes` / `kopsc svc disable nginx --yes` |
|
||||||
| 看服务日志 | `kopsc svc logs nginx -f` |
|
| 看服务日志 | `kopsc svc logs nginx -f` |
|
||||||
|
| 创建压缩文件 | `kopsc archive create ./mydir`、`kopsc archive create ./mydir --format zip` |
|
||||||
|
| 查看压缩内容 | `kopsc archive list backup.tar.gz` |
|
||||||
| 解压文件 | `kopsc archive extract app.tar.gz` |
|
| 解压文件 | `kopsc archive extract app.tar.gz` |
|
||||||
|
| 起 HTTP 服务器 | `kopsc net http`、`kopsc net http -p 9000 --upload` |
|
||||||
| 看系统信息 | `kopsc sysinfo` |
|
| 看系统信息 | `kopsc sysinfo` |
|
||||||
| 测试连通性 | `kopsc net ping example.com` |
|
| 测试连通性 | `kopsc net ping example.com` |
|
||||||
| 路由追踪 | `kopsc net trace example.com` |
|
| 路由追踪 | `kopsc net trace example.com` |
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// gitTimeOptions are the parsed arguments of `kopsc git time`.
|
||||||
|
type gitTimeOptions struct {
|
||||||
|
Time time.Time
|
||||||
|
Remote string
|
||||||
|
Branch string
|
||||||
|
NoPush bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// runGitTime modifies the timestamp of the last commit to the specified time
|
||||||
|
// and force pushes the branch.
|
||||||
|
//
|
||||||
|
// Without --yes it only shows what would happen (exit code 2), so the destructive
|
||||||
|
// path always needs an explicit confirmation.
|
||||||
|
func runGitTime(ctx context.Context, req BuiltinRequest) error {
|
||||||
|
return runGitTimeWith(ctx, req, execGitRunner{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// runGitTimeWith is runGitTime with its dependencies injected for tests.
|
||||||
|
func runGitTimeWith(ctx context.Context, req BuiltinRequest, runner gitRunner) error {
|
||||||
|
opts, confirmedInArgs, err := parseGitTimeArgs(req.Args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
confirmed := req.Confirmed || confirmedInArgs
|
||||||
|
|
||||||
|
branch := opts.Branch
|
||||||
|
if branch == "" {
|
||||||
|
if branch, err = currentBranch(ctx, runner); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remote := opts.Remote
|
||||||
|
if remote == "" && !opts.NoPush {
|
||||||
|
names, err := remotes(ctx, runner)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case len(names) == 1:
|
||||||
|
remote = names[0]
|
||||||
|
case len(names) == 0:
|
||||||
|
return errors.New("仓库没有配置远程;用 --no-push 只改写本地,或先 git remote add")
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("仓库有多个远程(%s),请用 --remote 指定要强推哪个",
|
||||||
|
strings.Join(names, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the last commit info
|
||||||
|
lastCommit, err := getLastCommit(ctx, runner)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show preview
|
||||||
|
fmt.Fprintf(req.Stdout, "修改最后一次提交的时间\n\n")
|
||||||
|
fmt.Fprintf(req.Stdout, "提交: %s %s\n", lastCommit.Short, lastCommit.Subject)
|
||||||
|
fmt.Fprintf(req.Stdout, "当前时间: %s\n", lastCommit.AuthorTime.Format("2006-01-02 15:04:05"))
|
||||||
|
fmt.Fprintf(req.Stdout, "修改为: %s\n", opts.Time.Format("2006-01-02 15:04:05"))
|
||||||
|
fmt.Fprintf(req.Stdout, "\n")
|
||||||
|
|
||||||
|
if !opts.NoPush {
|
||||||
|
fmt.Fprintf(req.Stdout, "将强推到 %s/%s\n", remote, branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !confirmed {
|
||||||
|
fmt.Fprintf(req.Stdout, "\n这是破坏性操作,会改变提交历史。加 --yes 确认执行。\n")
|
||||||
|
return &ExitError{Code: 2, Err: errors.New("需要确认")}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the time change
|
||||||
|
// Must set both GIT_COMMITTER_DATE and --date to change both timestamps
|
||||||
|
timeStr := opts.Time.Format("2006-01-02 15:04:05")
|
||||||
|
env := []string{"GIT_COMMITTER_DATE=" + timeStr}
|
||||||
|
|
||||||
|
if _, stderr, err := runner.RunEnv(ctx, env, "commit", "--amend", "--date="+timeStr, "--no-edit"); err != nil {
|
||||||
|
return fmt.Errorf("修改提交时间失败: %w\n%s", err, stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(req.Stdout, "✓ 已修改最后一次提交的时间\n")
|
||||||
|
|
||||||
|
// Force push if not --no-push
|
||||||
|
if !opts.NoPush {
|
||||||
|
fmt.Fprintf(req.Stdout, "强推到 %s/%s...\n", remote, branch)
|
||||||
|
|
||||||
|
if _, stderr, err := runner.Run(ctx, "push", "--force", remote, branch); err != nil {
|
||||||
|
return fmt.Errorf("强推失败: %w\n%s", err, stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(req.Stdout, "✓ 已强推\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseGitTimeArgs parses the arguments for `kopsc git time`.
|
||||||
|
func parseGitTimeArgs(args []string) (gitTimeOptions, bool, error) {
|
||||||
|
var opts gitTimeOptions
|
||||||
|
var confirmed bool
|
||||||
|
var timeStr string
|
||||||
|
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
arg := args[i]
|
||||||
|
switch {
|
||||||
|
case arg == "--yes" || arg == "-y":
|
||||||
|
confirmed = true
|
||||||
|
case arg == "--no-push":
|
||||||
|
opts.NoPush = true
|
||||||
|
case arg == "--remote":
|
||||||
|
if i+1 >= len(args) {
|
||||||
|
return opts, false, fmt.Errorf("--remote 缺少远程名称")
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
opts.Remote = args[i]
|
||||||
|
case strings.HasPrefix(arg, "--remote="):
|
||||||
|
opts.Remote = strings.TrimPrefix(arg, "--remote=")
|
||||||
|
case arg == "--branch":
|
||||||
|
if i+1 >= len(args) {
|
||||||
|
return opts, false, fmt.Errorf("--branch 缺少分支名称")
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
opts.Branch = args[i]
|
||||||
|
case strings.HasPrefix(arg, "--branch="):
|
||||||
|
opts.Branch = strings.TrimPrefix(arg, "--branch=")
|
||||||
|
case strings.HasPrefix(arg, "-"):
|
||||||
|
return opts, false, fmt.Errorf("未知选项 %q", arg)
|
||||||
|
default:
|
||||||
|
if timeStr == "" {
|
||||||
|
timeStr = arg
|
||||||
|
} else {
|
||||||
|
return opts, false, fmt.Errorf("多余的参数 %q", arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if timeStr == "" {
|
||||||
|
return opts, false, fmt.Errorf("缺少时间参数;用法: kopsc git time <时间>")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and validate time format: yyyy-MM-dd HH:mm:ss
|
||||||
|
t, err := time.ParseInLocation("2006-01-02 15:04:05", timeStr, time.Local)
|
||||||
|
if err != nil {
|
||||||
|
return opts, false, fmt.Errorf("时间格式错误: %q,必须是 yyyy-MM-dd HH:mm:ss(如 2026-11-05 22:30:00)", timeStr)
|
||||||
|
}
|
||||||
|
opts.Time = t
|
||||||
|
|
||||||
|
return opts, confirmed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// gitCommitInfo holds basic info about a commit.
|
||||||
|
type gitCommitInfo struct {
|
||||||
|
Hash string
|
||||||
|
Short string
|
||||||
|
Subject string
|
||||||
|
AuthorTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLastCommit returns info about the last commit.
|
||||||
|
func getLastCommit(ctx context.Context, runner gitRunner) (gitCommitInfo, error) {
|
||||||
|
// Get commit hash and subject
|
||||||
|
out, stderr, err := runner.Run(ctx, "log", "-1", "--format=%H|%h|%s")
|
||||||
|
if err != nil {
|
||||||
|
return gitCommitInfo{}, fmt.Errorf("获取提交信息失败: %w\n%s", err, stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(strings.TrimSpace(out), "|", 3)
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return gitCommitInfo{}, fmt.Errorf("解析提交信息失败: %s", out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get author time
|
||||||
|
timeOut, stderr, err := runner.Run(ctx, "log", "-1", "--format=%ai")
|
||||||
|
if err != nil {
|
||||||
|
return gitCommitInfo{}, fmt.Errorf("获取提交时间失败: %w\n%s", err, stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
authorTime, err := time.Parse("2006-01-02 15:04:05 -0700", strings.TrimSpace(timeOut))
|
||||||
|
if err != nil {
|
||||||
|
return gitCommitInfo{}, fmt.Errorf("解析提交时间失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gitCommitInfo{
|
||||||
|
Hash: parts[0],
|
||||||
|
Short: parts[1],
|
||||||
|
Subject: parts[2],
|
||||||
|
AuthorTime: authorTime,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user