diff --git a/.task/checksum/generate-bindings--BUILD_FLAGS--buildvcs-false--gcflags-all---l-- b/.task/checksum/generate-bindings--BUILD_FLAGS--buildvcs-false--gcflags-all---l--
index 6b88588..85d7703 100644
--- a/.task/checksum/generate-bindings--BUILD_FLAGS--buildvcs-false--gcflags-all---l--
+++ b/.task/checksum/generate-bindings--BUILD_FLAGS--buildvcs-false--gcflags-all---l--
@@ -1 +1 @@
-c6a7af2915380768678ecf3032701af1
+b3473eff2f1694108cc3ae68b4ae04f0
diff --git a/bin/gitpilot.exe b/bin/gitpilot.exe
index 7750361..310787c 100644
Binary files a/bin/gitpilot.exe and b/bin/gitpilot.exe differ
diff --git a/frontend/app/components/ContentArea.vue b/frontend/app/components/ContentArea.vue
index 74cfdfd..91586e4 100644
--- a/frontend/app/components/ContentArea.vue
+++ b/frontend/app/components/ContentArea.vue
@@ -19,6 +19,7 @@ import {
UserOutlined,
RollbackOutlined,
ExclamationCircleOutlined,
+ CloseCircleOutlined,
} from '@ant-design/icons-vue'
import { Modal } from 'ant-design-vue'
import { AppService } from '../../bindings/github.com/zhuy1228/GitPilot/internal/app'
@@ -614,6 +615,33 @@ async function resetToCommit(commit, mode = 'hard') {
})
}
+async function revertCommit(commit) {
+ if (!props.project?.path || !commit) return
+ Modal.confirm({
+ title: '确认撤回提交',
+ icon: h(ExclamationCircleOutlined),
+ content: h('div', [
+ h('p', '将创建一个新提交来撤回以下提交的更改:'),
+ h('p', { style: 'font-family: monospace; color: #89b4fa;' }, `${commit.shortHash} - ${commit.message}`),
+ h('p', { style: 'color: var(--text-muted); font-size: 12px; margin-top: 8px;' }, '此操作会生成一个新的反向提交,不会丢失历史记录。'),
+ ]),
+ okText: '确认撤回',
+ cancelText: '取消',
+ okButtonProps: { danger: true },
+ async onOk() {
+ try {
+ await AppService.RevertCommit(props.project.path, commit.hash)
+ await loadStatus()
+ selectedCommit.value = null
+ commitDiff.value = ''
+ } catch (e) {
+ console.error('撤回提交失败:', e)
+ Modal.error({ title: '撤回失败', content: String(e) })
+ }
+ },
+ })
+}
+
function formatTime(ts) {
if (!ts) return ''
const d = new Date(ts * 1000)
@@ -842,41 +870,56 @@ function formatTime(ts) {
v-for="log in commitLogs"
:key="log.hash"
class="commit-item"
- :class="{ active: selectedCommit?.hash === log.hash }"
+ :class="{ active: selectedCommit?.hash === log.hash, unpushed: !log.pushed }"
@click="selectCommit(log)"
>
+
{{ log.message }}
-
+
+
-
+
-
- resetToCommit(log, key)">
-
- 🔴 硬回滚
- 丢弃所有更改
-
-
- 🟡 混合回滚
- 保留到工作区
-
-
- 🟢 软回滚
- 保留到暂存区
-
-
-
-
+
+
+
+
+
+
+ resetToCommit(log, key)">
+
+ 🔴 硬回滚
+ 丢弃所有更改
+
+
+ 🟡 混合回滚
+ 保留到工作区
+
+
+ 🟢 软回滚
+ 保留到暂存区
+
+
+
+
+
{{ log.shortHash }}
+ 未推送
{{ log.author }}
{{ formatTime(log.timestamp) }}
@@ -1379,22 +1422,52 @@ function formatTime(ts) {
min-width: 0;
}
-.commit-reset-btn {
- visibility: hidden;
- color: var(--text-muted) !important;
+.commit-action-btns {
+ display: flex;
+ align-items: center;
+ gap: 0;
flex-shrink: 0;
+ visibility: hidden;
+}
+
+.commit-action-btn {
+ color: var(--text-muted) !important;
width: 24px;
height: 24px;
}
-.commit-reset-btn:hover {
+.commit-action-btn:hover {
color: #f38ba8 !important;
}
-.commit-item:hover .commit-reset-btn {
+.commit-item:hover .commit-action-btns {
visibility: visible;
}
+/* 未推送标识 */
+.commit-unpushed-dot {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: #a6e3a1;
+ flex-shrink: 0;
+ margin-right: 2px;
+}
+
+.commit-push-tag {
+ font-size: 10px;
+ padding: 0 5px;
+ border-radius: 3px;
+ background: rgba(166, 227, 161, 0.15);
+ color: #a6e3a1;
+ line-height: 16px;
+ flex-shrink: 0;
+}
+
+.commit-item.unpushed {
+ border-left: 2px solid #a6e3a1;
+}
+
.commit-msg {
font-size: 13px;
color: var(--text-primary);
diff --git a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/appservice.js b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/appservice.js
index b0e96e2..95bd4c5 100644
--- a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/appservice.js
+++ b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/appservice.js
@@ -282,6 +282,16 @@ export function ResetProject(path, hash, mode) {
return $Call.ByID(2061148684, path, hash, mode);
}
+/**
+ * RevertCommit 撤回指定提交(生成一个反向提交)
+ * @param {string} path
+ * @param {string} hash
+ * @returns {$CancellablePromise}
+ */
+export function RevertCommit(path, hash) {
+ return $Call.ByID(1334788539, path, hash);
+}
+
/**
* SelectDirectory 打开系统文件夹选择器,返回选中的路径
* @returns {$CancellablePromise}
diff --git a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/models.js b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/models.js
index 7370398..a8fc047 100644
--- a/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/models.js
+++ b/frontend/bindings/github.com/zhuy1228/GitPilot/internal/app/models.js
@@ -133,6 +133,13 @@ export class CommitLog {
*/
this["message"] = "";
}
+ if (!("pushed" in $$source)) {
+ /**
+ * @member
+ * @type {boolean}
+ */
+ this["pushed"] = false;
+ }
Object.assign(this, $$source);
}
diff --git a/internal/app/service.go b/internal/app/service.go
index ac1e929..c7c17a0 100644
--- a/internal/app/service.go
+++ b/internal/app/service.go
@@ -569,6 +569,7 @@ type CommitLog struct {
Email string `json:"email"`
Timestamp int64 `json:"timestamp"`
Message string `json:"message"`
+ Pushed bool `json:"pushed"`
}
// BranchInfo 分支信息
@@ -589,7 +590,39 @@ func (s *AppService) GetCommitLog(path string, count int) ([]CommitLog, error) {
if err != nil {
return nil, fmt.Errorf("获取提交历史失败: %w", err)
}
- return parseCommitLog(out), nil
+ logs := parseCommitLog(out)
+
+ // 获取当前分支名,查询未推送的提交
+ branch, brErr := s.gitClient.Branch(path)
+ if brErr == nil {
+ branch = strings.TrimSpace(branch)
+ unpushedOut, upErr := s.gitClient.UnpushedCommits(path, branch)
+ unpushedSet := make(map[string]bool)
+ if upErr == nil {
+ for _, h := range strings.Split(strings.TrimSpace(unpushedOut), "\n") {
+ if h != "" {
+ unpushedSet[h] = true
+ }
+ }
+ }
+ for i := range logs {
+ logs[i].Pushed = !unpushedSet[logs[i].Hash]
+ }
+ }
+
+ return logs, nil
+}
+
+// RevertCommit 撤回指定提交(生成一个反向提交)
+func (s *AppService) RevertCommit(path, hash string) error {
+ if _, err := os.Stat(path); os.IsNotExist(err) {
+ return fmt.Errorf("项目路径不存在: %s", path)
+ }
+ if strings.TrimSpace(hash) == "" {
+ return fmt.Errorf("提交哈希不能为空")
+ }
+ _, err := s.gitClient.RevertCommit(path, strings.TrimSpace(hash))
+ return err
}
func parseCommitLog(output string) []CommitLog {
diff --git a/internal/git/client.go b/internal/git/client.go
index cceb773..a47a9d2 100644
--- a/internal/git/client.go
+++ b/internal/git/client.go
@@ -119,6 +119,16 @@ func (g *GitClient) Log(path string, count int) (string, error) {
)
}
+// UnpushedCommits 获取当前分支上未推送到远程的提交哈希列表
+func (g *GitClient) UnpushedCommits(path, branch string) (string, error) {
+ return g.Run(path, "rev-list", "origin/"+branch+"..HEAD")
+}
+
+// RevertCommit 撤回指定提交(创建一个反向提交)
+func (g *GitClient) RevertCommit(path, hash string) (string, error) {
+ return g.Run(path, "revert", "--no-edit", hash)
+}
+
// BranchList 获取所有本地分支
func (g *GitClient) BranchList(path string) (string, error) {
return g.Run(path, "branch", "--format=%(refname:short)\t%(HEAD)")