
1. 项目概述为什么你总在提交后两秒就后悔Git amend 这个操作我第一次用是在入职第三天——刚把一个带 typo 的 commit 推到远程仓库还没来得及喝完第一口咖啡就发现git push origin main后的终端输出里赫然写着typo in README.md: recieve → receive。那一刻不是慌是生理性的手抖。因为我知道如果直接再提一个“fix typo” commit这个补丁会像补丁摞补丁的旧毛衣一样留在历史里而团队里那个坚持“每个 commit 必须原子、可回溯、可解释”的 senior engineer已经在我工位旁晃悠了两次。Git amend 不是魔法它是 Git 设计哲学里最务实的一环允许你在 commit 尚未离开本地仓库时对它的快照snapshot和元数据author/committer/timestamp/message做一次“无痕修正”。它不改历史——它只是把“上一个 commit”这个指针悄悄挪到一个新生成的、内容更准确的 commit 上。关键在于这个操作只影响你本地的 HEAD 指针不触碰任何已推送的远程记录所以它安全、轻量、不可逆但可控。很多人把它当成“撤回上一次提交”的快捷键这是危险的误解。amend 的本质不是撤销而是覆盖式重写——它用一个新的 commit 完全替代旧的 commit连哈希值都彻底改变。这意味着如果你已经git push过再git commit --amend就必须强制推送git push --force-with-lease而这会破坏协作流程。所以真正要掌握的不是命令怎么敲而是判断“现在还能不能 amend”这个时间窗口。这个窗口的边界就是git push执行前的最后一毫秒。我见过太多人因为没看清git status里那行不起眼的Your branch is ahead of origin/main by 1 commit.就贸然 amend结果推不上去又不敢 force最后只能硬着头皮加个空 commit 来“掩盖”反而让历史更混乱。它适合谁不是初学者的玩具也不是老手的炫技工具。它是每天要处理 5 个 PR、对提交质量有洁癖、且习惯在git add后用git diff --staged再扫一眼的人的日常呼吸。你不需要记住所有参数但必须刻进肌肉记忆的是git commit --amend -m 新的完整消息和git commit --amend --no-edit这两条指令的适用场景。前者用于重写整个提交信息后者用于只修改暂存区内容而不动消息——这个区别决定了你是在“编辑文档”还是在“重写历史”。2. 核心原理拆解Git 是如何“假装没发生过”的要真正用好 amend必须掀开 Git 的包装纸看看底层发生了什么。Git 的核心模型是快照snapshot而非差异diff。每次 commitGit 并不记录“从上一个版本改了哪几行”而是对工作目录当前所有文件的状态拍一张完整的、压缩过的快照并把这个快照的唯一指纹SHA-1 或 SHA-256 哈希值存下来。同时这个 commit 对象还包含三个关键元数据指向父 commit 的指针parent、作者信息author、提交者信息committer以及提交消息message。当你执行git commit --amend时Git 在后台做了三件确定性极强的事2.1 快照重建不是修改是重拍Git 首先检查当前暂存区staging area/index的内容。无论你之前git add过什么或者用git add -p精确选中了哪些 hunk此刻暂存区的状态就是新 commit 快照的全部来源。Git 会基于这个暂存区重新计算所有文件的树对象tree object和 blob 对象blob object生成一组全新的哈希值。旧 commit 的快照数据并不会被立即删除但它会变成“悬空对象”dangling object就像被遗弃在仓库角落的旧底片。Git 的垃圾回收机制git gc会在默认 30 天后自动清理它们但在那之前你甚至可以用git show old-hash把它捞回来——前提是你还记得那个旧哈希。提示想立刻看到被 amend 掉的旧 commit 内容执行git reflog找到commit (amend): ...上一行的HEAD{1}然后git show HEAD{1}。这是你的后悔药但别依赖它。2.2 元数据继承与覆盖谁在说谎新 commit 的元数据并非凭空生成。Git 默认会继承旧 commit 的大部分元数据但有一个关键例外committer 时间戳会被更新为当前时间而 author 时间戳保持不变。这是 Git 区分“谁写了代码”和“谁最终把它打包进历史”的精妙设计。比如你昨天写了代码author time yesterday今天才git add并git commitcommitter time today。当你--amend时author time 依然锚定在昨天而 committer time 变成今天的 amend 时间。这保证了贡献归属的准确性。至于提交消息message--amend默认会把你拉进$EDITOR通常是 vim 或 nano让你编辑。如果你加了--no-edit参数Git 就完全跳过这一步直接复用旧消息。这里有个极易被忽略的细节--amend本身不会改变 author 信息。也就是说如果你git commit --amend --authorNew Name newemail.com你是在显式覆盖 author 字段而不是在“修改”它。这个覆盖是永久的且会体现在后续所有基于此 commit 的分支上。2.3 引用指针的原子移动HEAD 的一次优雅转身Git 仓库里HEAD是一个指向当前所在分支如main的符号引用而main本身则是一个指向最新 commit 哈希的指针。git commit --amend的最后一步就是让main这个指针从原来那个旧 commit 的哈希原子性地atomically切换到新生成的 commit 哈希上。这个过程是瞬间完成的没有中间状态。你可以用git log --oneline --graph清晰地看到这个变化原来那条线上的一个节点消失了被一个新节点无缝取代而它的父节点parent关系保持不变。注意--amend只能修改HEAD指向的那个 commit。如果你想修改更早的 commit比如倒数第二个那就必须用git rebase -i那是另一个维度的操作和 amend 有本质区别。amend 是单点修正rebase 是区间重写。3. 实操全流程从“手抖”到“肌肉记忆”的七种典型场景光懂原理不够真实世界里的 amend 是带着具体气味和温度的。下面是我过去三年在不同项目中高频使用的七种场景每一种都配上了真实的终端命令流、我的思考路径和踩过的坑。请务必注意所有示例都假设你尚未执行git push。3.1 场景一消息写错一个字但代码完全正确最常见触发条件git commit -m Fix bug in login flow回车后立刻发现login拼成了logn。标准操作git commit --amend -m Fix bug in login flow为什么这么干这是 amend 的“教科书式”用法。消息是纯文本元数据修改它不涉及任何文件内容变更风险最低。-m参数直接覆盖省去打开编辑器的步骤。实操心得我给自己定了个铁律——只要git commit命令敲完回车手指就立刻移向方向键↑看一眼上一条命令是否拼写正确。这比事后 amend 更高效。另外永远不要用git commit --amend而不加-m或--no-edit否则你会卡在 vim 里对着一个空白编辑器发呆怀疑人生。3.2 场景二漏加了一个关键文件比如 .env.example触发条件git commit -m Add new API client后突然想起忘了git add .env.example。标准操作git add .env.example git commit --amend --no-edit为什么这么干--no-edit是关键。它告诉 Git“快照内容我改了因为新加了文件但消息一字不改别烦我”。如果你用-m就得再打一遍消息纯属浪费时间。避坑指南.env.example这类文件我习惯在项目根目录放一个git add -f .env.example的 alias比如gafe因为.env*通常在.gitignore里。-fforce参数能绕过 ignore 规则。但--amend本身不关心 ignore它只认暂存区所以git add -f后再--amend是完美组合。3.3 场景三代码改多了想拆分成两个原子 commit触发条件你在一个 commit 里既修复了登录 Bug又重构了用户模块的测试消息是Fix login and refactor tests。但 PR Reviewer 明确要求“功能修复和重构必须分开”。标准操作# 1. 先撤回这个大 commit但保留工作区和暂存区 git reset --soft HEAD~1 # 2. 此时 git status 会显示所有改动都在 Changes to be committed 状态 # 3. 用 git add -p 精确选择登录修复的 hunk git add -p # 4. 提交第一部分 git commit -m Fix login flow: handle empty password case # 5. 剩下的测试重构还在暂存区直接提交第二部分 git commit -m Refactor user module tests: use shared fixtures为什么不用 amend因为 amend 只能生成一个新 commit无法“分裂”。reset --soft是更底层的控制它把 HEAD 指针挪回去但把所有改动都保留在暂存区给你完全的重新组织权。这是比 amend 更强大的“后悔药”但门槛也更高。3.4 场景四Author 信息错了比如用了公司邮箱该用个人邮箱触发条件git config --global user.email workcompany.com是全局设置但这个开源项目要求用personalgmail.com。标准操作git commit --amend --authorYour Name personalgmail.com为什么这么干--author参数是唯一能修改 author 字段的方式。它会覆盖 author name 和 email但不会动 committer 信息。执行后git log --prettyformat:%an %ae | %cn %ce会清晰显示 author 和 committer 的分离。血泪教训我在一个 Apache 项目的 PR 里因为 author email 不符合 CLAContributor License Agreement要求被 bot 自动拒绝了三次。后来发现git commit --amend --author...后必须git push --force-with-lease才能把修正后的 author 推上去。但 force push 有风险所以现在我的做法是在git commit前先用git config --local user.email xxx为当前仓库单独设置 email一劳永逸。3.5 场景五想加一个 Signed-off-by 行为合规或社区要求触发条件项目要求每个 commit message 末尾必须有Signed-off-by: Your Name email以满足 DCODeveloper Certificate of Origin。标准操作git commit --amend -s为什么这么干-s或--signoff是 Git 内置参数它会自动在当前 commit message 的末尾追加一行Signed-off-by: ...格式严格遵循user.name和user.email配置。它比手动编辑 message 更可靠因为不会拼错字段名。深度技巧-s不是简单地“加一行”它会智能处理 message 结构。如果原消息是feat(api): add rate limiting Add Redis-backed rate limiter for /v1/users endpoint.那么git commit --amend -s后它会变成feat(api): add rate limiting Add Redis-backed rate limiter for /v1/users endpoint. Signed-off-by: Your Name email注意它确保了Signed-off-by行和正文之间有空行这是 DCO 解析器的要求。手动加很容易漏掉这个空行。3.6 场景六误提交了敏感信息如 API Key但尚未 push触发条件git add . git commit -m WIP: integrate Stripe结果发现config.js里硬编码了STRIPE_SECRET_KEYsk_test_...。标准操作# 1. 从暂存区移除敏感文件但保留在工作区 git reset HEAD config.js # 2. 从工作区删除敏感内容用编辑器删掉 key # 3. 重新 add 修正后的文件 git add config.js # 4. amend覆盖旧 commit git commit --amend --no-edit为什么这是黄金流程git reset HEAD file是关键。它把文件从暂存区“拿下来”但不碰工作区这样你就能在编辑器里安全地删掉密钥而不会丢失其他修改。如果直接git checkout -- config.js会把整个文件的工作区改动都丢掉得不偿失。终极保险对于所有含密钥的项目我强制自己在.gitattributes里加一行config.js diffsecrets再在.git/config里配置diff.secrets.textconvcat并配合git secrets工具做 pre-commit hook。这样git commit时如果检测到密钥会直接报错阻止根本不会有 amend 的机会。3.7 场景七想修改多个连续的 commit比如前三个触发条件你连续git commit了三次但发现第一个 commit 的消息太模糊第二个漏了测试第三个 author 错了。标准操作这不是 amend 的战场是git rebase -i的领域。git rebase -i HEAD~3 # 在编辑器里把第一行的 pick 改成 reword第二行改成 edit第三行改成 fixup # 保存退出后Git 会停在第二个 commit让你 git add 测试文件然后 git commit --amend --no-edit # 然后 git rebase --continue为什么必须区分--amend是单点手术刀rebase -i是整容级的面部重塑。试图用git commit --amend去改倒数第二个 commit只会得到一个错误fatal: No commit to amend。因为HEAD指向的是最后一个不是倒数第二个。理解这个边界是避免在终端里陷入无限git log循环的关键。4. 工具链与高级技巧让 amend 从“手动”走向“自动化”当 amend 成为你每日开发流的一部分下一步就是把它嵌入到你的工具链里让它从一个需要思考的命令变成一个无需思考的反射动作。这不仅能减少错误更能释放认知带宽让你专注在真正的逻辑上。4.1 配置 Git 别名把 5 个单词缩成 2 个字母Git 的alias功能是提升效率的核武器。我把最常用的 amend 操作都封装成了短别名放在~/.gitconfig的[alias]区块下[alias] # ca commit amend, with no-edit, perfect for adding files ca commit --amend --no-edit # cam commit amend, with message, for quick edits cam !f() { git commit --amend -m \$*\; }; f # cas commit amend, with signoff, for DCO compliance cas commit --amend -s # cai commit amend, interactive, for full control over message cai commit --amend # caw commit amend, with all staged changes, but keep old message caw !f() { git add -A git commit --amend --no-edit; }; f使用效果漏加文件git ca—— 一个命令搞定。快速改消息git cam Fix auth redirect loop—— 比git commit --amend -m ...少敲 12 个字符。开源项目 PRgit cas—— 一键签名永不忘记。原理与安全这些别名之所以安全是因为它们都基于--amend的核心语义没有引入任何副作用。caw这个别名尤其强大它等价于git add -A git commit --amend --no-edit意思是“把工作区所有变化都加进来然后 amend 当前 commit”。我常用它来快速整合零散的调试日志或 console.log避免污染主逻辑。4.2 集成 VS Code在编辑器里点一下就 amendVS Code 的 Git 插件内置提供了图形化 amend 操作。但默认设置下它有个致命缺陷点击“Amend Commit”按钮后它会弹出一个编辑器让你改 message但不会自动把暂存区的新文件加进来。这导致你点了 amend结果发现新文件根本没进去白忙活。解决方案修改 VS Code 设置settings.json{ git.enableSmartCommit: true, git.smartCommitChanges: all, git.postCommitCommand: push }其中git.smartCommitChanges:all是关键。它让 VS Code 在执行 amend 前自动执行git add -A把所有工作区变化都暂存。这样你只需点一下按钮输入新消息或留空用--no-edit就完成了从代码修改到 commit 修正的闭环。我的工作流写完代码 →CtrlShiftP→ 输入Git: Amend Commit→ 在弹出的输入框里敲新消息 → 回车。整个过程不到 3 秒比切到终端敲命令还快。而且VS Code 的 Git 面板会实时高亮显示哪些文件被暂存、哪些是未暂存视觉反馈极佳。4.3 Pre-commit Hook在 amend 前就拦截问题Pre-commit hook 是 Git 的守门员它在git commit包括--amend被执行前运行一段脚本。如果脚本返回非零值commit 就会被中止。这是防止低级错误的终极防线。我为--amend场景定制了一个轻量级 hook放在.git/hooks/pre-commit#!/bin/bash # 检查是否是 amend 操作 if git rev-parse --verify HEAD /dev/null 21; then # 获取上一个 commit 的消息 OLD_MSG$(git log -1 --pretty%B HEAD^) # 检查新消息是否为空amend 时容易手滑按了回车 NEW_MSG$(git log -1 --pretty%B HEAD 2/dev/null || echo ) if [ -z $NEW_MSG ] [ -n $OLD_MSG ]; then echo ERROR: Commit message is empty! Please provide a meaningful message. echo Hint: Use git commit --amend -m \your message\ or edit the message. exit 1 fi fi # 检查是否误提交了 .env 文件即使被 ignore也可能被 -f 强制加了 if git status --porcelain | grep -q ^A.*\.env; then echo ERROR: You are about to commit .env file! This is dangerous. echo Please remove it with git reset HEAD .env and try again. exit 1 fi效果当你git commit --amend后忘了写消息直接回车hook 会立刻报错阻止一个空消息的 commit 诞生。同样如果你不小心git add -f .env它也会在最后一刻拉住你。这个 hook 不会拖慢你的速度执行时间 10ms但能帮你避开 80% 的低级失误。4.4 Git Worktree Amend在多任务间无缝切换Worktree 是 Git 2.5 引入的神器它允许你在同一个仓库里为不同的分支创建独立的工作目录。这和--amend结合能解决一个经典难题你正在 feature/login 分支上开发但突然需要紧急修复 main 分支的一个线上 Bug修完还得立刻回到 login 分支且不能让两个分支的暂存区互相污染。标准流程# 1. 在 main 分支上创建一个独立 worktree git worktree add ../hotfix-main main # 2. cd 进去修 Bugcommit cd ../hotfix-main # ... fix the bug ... git add . git commit -m hotfix(main): prevent 500 on missing user id # 3. 现在你想 amend 这个 hotfix commit但不想影响你原来的 feature/login 工作区 git commit --amend -m hotfix(main): prevent 500 on missing user id (add null check) # 4. 推送 git push origin main # 5. 完事切回原来的终端cd 回你的 feature/login 目录继续开发一切如常为什么这是降维打击传统的做法是git stash但 stash 有局限——它只保存工作区和暂存区不保存未跟踪文件且git stash pop有时会冲突。Worktree 是物理隔离feature/login目录里的任何改动对hotfix-main目录里的git commit --amend没有任何影响。它们共享同一个.git目录但拥有完全独立的HEAD、暂存区和工作区。这是--amend在复杂协作场景下的最高阶用法。5. 协作红线与灾难恢复当 amend 走错一步如何全身而退git commit --amend最大的魅力是它的轻量和安全但最大的风险恰恰也源于这种“轻量感”。一旦你越过了那条看不见的协作红线——git push之后的 amend——它就从一个便利工具瞬间变成一颗定时炸弹。下面这些是我亲身经历、或从同事那里听来的“血案”以及对应的、经过实战验证的恢复方案。5.1 灾难一已 push 后 amend再 push 被拒最常见现场还原你git push origin main了然后发现消息写错了立刻git commit --amend -m Fix login: handle empty password再git push origin main终端炸出To github.com:user/repo.git ! [rejected] main - main (non-fast-forward) error: failed to push some refs to github.com:user/repo.git hint: Updates were rejected because the tip of your current branch is behind hint: its remote counterpart. Integrate the remote changes (e.g. hint: git pull ...) before pushing again.错误应对千万别做git push --force这是最野蛮的做法它会无视所有远程保护强行覆盖。如果别人在这期间也git push了他们的 commit 会直接消失引发严重协作事故。git pull git pushgit pull会触发git merge生成一个 merge commit把你的 amend 和别人的 commit 混在一起历史变得一团糟。正确恢复三步走立刻停止所有操作确认现状git status # 查看当前分支状态 git log --oneline -n 5 # 看看本地 HEAD 和 origin/main 的差异 git ls-remote origin main # 获取远程 main 的最新 commit hash用--force-with-lease安全覆盖git push --force-with-lease origin main--force-with-lease是--force的安全版。它会先检查远程main分支的当前 hash 是否和你本地origin/main的 hash 一致。如果一致说明没人动过远程分支它才允许 force如果不一致比如别人 push 了它会失败并报错保护你和他人。这是 GitHub 和 GitLab 默认推荐的强制推送方式。通知协作者在 Slack 或邮件里发一条简短消息“Hi team, I just amended and force-pushed the latest commit onmainto fix the message. If you have local changes based on the old commit, pleasegit pull --rebaseto update.” 主动告知是专业素养的体现。5.2 灾难二amend 后发现改错了想找回旧 commit现场还原git commit --amend -m Fix login后你意识到其实旧消息Login fix: add validation更准确新消息反而误导了 Reviewer。恢复方案两种按需选择方案 A用 reflog 找回最快推荐git reflog # 找到类似 HEAD{1}: commit (amend): Login fix: add validation 的行 git reset --hard HEAD{1} # 一步回到 amend 前的状态reflog记录了 HEAD 指针的所有移动历史包括--amend。HEAD{1}就是上一次 HEAD 的位置也就是 amend 前的那个 commit。reset --hard会把工作区、暂存区、HEAD 全部重置到那个点干净利落。方案 B用 dangling object 恢复万能但稍慢git fsck --lost-found # 找出所有悬空对象 # 输出类似dangling commit abc123... git show abc123 # 确认这就是你要的旧 commit git cherry-pick abc123 # 把它作为一个新 commit 加到当前 HEAD 后面fsckfile system check是 Git 的底层诊断工具它能扫描出所有未被任何引用指向的对象。cherry-pick则是把一个 commit 的更改应用到当前分支上。这种方式不改变历史而是“复制”一个旧 commit适合你不想重置当前工作区的场景。5.3 灾难三多人协作中有人 force-push 了 amend你本地 pull 后出现“奇怪”的 merge commit现场还原同事git commit --amend git push --force-with-lease origin main了。你本地git pull origin main结果git log里多了一个 merge commit把你的本地 commit 和同事的 force-pushed commit 连在了一起历史图谱变成了“之”字形。根本原因git pull默认是git fetch git merge。当你fetch时Git 发现远程main的 hash 和你本地origin/main的 hash 不同因为被 force 了它就会认为这是一个“分叉”于是执行merge来“缝合”生成一个 merge commit。正确应对永远用 rebase# 1. 先 fetch只下载新对象不合并 git fetch origin # 2. 用 rebase 把你的本地 commit “重放”到新的远程 main 之上 git rebase origin/main # 3. 如果有冲突解决后 git add git rebase --continue # 4. 最后 push git push origin maingit pull --rebase是等效的快捷命令。它告诉 Git“别 merge把我本地的 commit一个个地、顺序地应用到最新的origin/main上面去”。这样你的历史会是一条干净的直线没有多余的 merge commit。我早已把git config --global pull.rebase true设为全局让所有git pull都默认走 rebase。5.4 灾难四CI/CD 流水线因 amend 失败GitHub Actions/GitLab CI现场还原你的 PR 基于一个旧 commit你git commit --amend修正了消息git push --force-with-lease结果 CI 流水线挂了日志里报错Error: Could not find commit xxxxx in repository。原因分析CI 系统如 GitHub Actions在触发时会根据GITHUB_SHA环境变量去检出那个特定的 commit。当你 force-push 后那个旧的xxxxxcommit 在远程仓库里已经不存在了被新 commit 替代CI 就找不到它自然失败。解决方案平台相关GitHub Actions在 workflow 文件.github/workflows/ci.yml里把on: [push, pull_request]改为on: push: branches: [main] pull_request: types: [opened, synchronize, reopened] # synchronize 就是 force-push 触发的关键是synchronize事件它专门用来响应 PR 的 force-push 更新。这样每次你 amend 并 force-pushCI 都会重新触发检出最新的 commit。GitLab CI在.gitlab-ci.yml里确保workflow: rules中包含了changes规则或者直接信任CI_COMMIT_SHA因为 GitLab 在 force-push 后会自动更新这个变量。终极建议对于任何需要 CI 验证的 PR永远不要在 PR 创建后git commit --amend并 force-push。正确的流程是PR 创建后所有修改都用新的、独立的 commit 来追加git commit -m chore: address review comments等 Reviewer 批准后再由 Maintainer 在 Merge 时选择 “Squash and merge”把所有 commits 合并成一个干净的 commit。这样历史清晰CI 稳定协作顺畅。6. 经验总结从“会用”到“精通”的五个心法写到这里你已经掌握了git commit --amend的所有技术细节、实操场景和灾难应对。但真正的精通不在于你知道多少命令而在于你形成了怎样的思维习惯和决策框架。这是我用三年时间在无数个深夜 debug、无数次 PR 被拒、无数次和同事激烈争论后沉淀下来的五个心法。它们不写在任何官方文档里却是我每天都在践行的准则。6.1 心法一Amend 是“本地暂存区”的快照修正不是“工作区”的撤销这是最根本的认知锚点。很多人混淆git add、git commit和git commit --amend的作用域。git add操作的是暂存区indexgit commit是把暂存区拍成快照--amend是用一个新的暂存区快照去替换上一个 commit。工作区working directory里的文件从来不是 amend 的直接操作对象。所以当你想用 amend 来“撤销”一个工作区的修改时你找错了工具——你应该用git checkout -- file或git restore file。而 amend 的正确姿势永远是先git add你想包含的改动再--amend。这个顺序就是 Git 工作流的 DNA。6.2 心法二永远在git push前用git status和git log --oneline -n 3做最后的“视觉确认”我见过太多人因为一个git push命令敲得太快而让一个本可以 amend 修正的错误变成了一个需要revert或rebase的麻烦。我的固定流程是在敲git push前必须执行两个命令git status它会告诉你Your branch is ahead of origin/main by 1 commit.这就是 amend 的最后窗口。如果看到这行而你又觉得 commit 有问题立刻git commit --amend。git log --oneline -n 3它会列出最近三条 commit 的简短哈希和消息。快速扫一眼确认消息是否准确、哈希是否是你预期的。这个习惯让我在过去一年里零次因消息错误而需要 force-push。6.3 心法三把--amend当作一个“提交前的必经质检环节”而不是“提交后的后悔药”高手和新手的区别往往就在这一念之间。新手把 amend 当作“救火队”只有出事了才调用高手把 amend 当作“流水线上的质检站”在每一次git commit之后、git push之前都主动执行一次git commit --amend --no-edit配合git add -A别名。为什么因为--no-edit本身不改变任何东西但它会强制 Git 重新计算一次快照并把 HEAD 指针“轻轻拨动”一下。这个动作会触发所有 pre-commit hook会暴露所有被忽略的.env文件会校验所有 DCO 签名