Mac上使用Git与SSH Key将项目上传到GitHub的完整指南
刚换了新Mac、第一次正经用GitHub的同学经常卡在同一个问题上看了一堆教程也跟着敲了git add、git commit、git push结果终端里不是Permission denied就是Repository not found折腾两小时项目还是躺在本地。这篇文章就专门解决“如何在Mac上将自己的项目上传至github”这件事从环境检查、SSH密钥配置到第一次推送成功再把推送失败时最常见的报错逐个拆开尽量让你今天看完、今天就能把代码交到远程仓库。先说清楚这不是一篇只贴命令的速查手册。我会尽量把每一步背后“为什么这么做”讲透比如为什么要用SSH而不是HTTPS、为什么commit之前要先add、为什么远程有README时本地push会被拒。理解了这些后面遇到任何报错你自己也能推断出大概方向。1. 上传动作拆解Git和GitHub在这条链路里各自扮演什么角色1.1 一套本地命令、一个远程仓库很多人把Git和GitHub混为一谈其实它们是完全不同的东西。Git是一个跑在你自己电脑里的版本控制工具负责记录项目文件每次的变化GitHub则是一个托管Git仓库的网站本质上是给你提供了一个远程存储的位置方便你换电脑、换环境、甚至和别人协作时都能拿到同一份代码。你可以这样想象Git像你本地的记账本每次修改文件后选中需要记录的变化在账本上记一笔这一笔就是一次commitGitHub像云端寄存箱当你把账本里记录好的内容push上去时等于把当天所有笔记复印一份寄到云端。后续你换台电脑只要从云端拉取就能拿到完整的历史记录。上传到GitHub这套链路核心其实只有四个动作命令作用类比git init把当前目录变成Git仓库开一个新账本git add把文件放进暂存区把笔记挑出来准备抄git commit把暂存内容正式提交成一次历史记录在账本上写下一条记录git push把本地提交记录推送到GitHub把账本复印件寄到云端对还没有接触过版本控制的初学者来说最容易困惑的是“为什么commit之前要先add”。这是因为Git把工作区、暂存区、本地仓库分成了三层。文件刚修改完时躺在工作区add之后进入暂存区commit之后才被正式纳入仓库历史。这个设计的好处是你可以只提交一部分文件比如这次只提交代码、不提交配置文件分批记录非常灵活。1.2 为什么教程都推荐用SSH而不是HTTPS在GitHub建好仓库后页面会给你两个远程地址格式HTTPS形式的https://github.com/用户名/仓库名.git以及SSH形式的gitgithub.com:用户名/仓库名.git。大部分教程默认让你复制SSH地址刚开始很多人不理解明明HTTPS看起来更直观为什么要绕一圈去配SSH Key原因在于认证方式不同。HTTPS方式每次push/pull时都需要验证身份现在GitHub已经不接受单纯账号密码要求用Personal Access Token这个token是一长串随机字符串容易过期过期后又得去网页后台重新生成。SSH方式则是一次配置、长期使用你本地生成一对密钥把公钥放到GitHub后台之后所有操作都靠密钥自动握手验证不需要反复输入任何东西。SSH的验证原理是非对称加密简单说就是本地私钥像是你随身携带的印章GitHub存着的公钥像识别印章的读卡器。你每次访问时GitHub给出一道题目你的客户端用私钥签名GitHub用公钥验证签名是否匹配。私钥永远不出你的电脑所以即使GitHub服务器被入侵也没人拿到你的私钥。还有一个细节值得注意现在GitHub新建仓库默认主分支叫main但很多老教程里还写着master。本地仓库如果用git init创建git版本不同默认分支名也不一样。为了让两边保持一致第一次推送前通常需要手动把本地分支改成main这一步后面细说。2. 动手前先检查Mac的Git环境2.1 Mac自带Git的真相有个冷知识Mac并不会默认安装完整版Git但当你首次在终端运行git --version时系统往往弹出一个“需要安装命令行开发者工具”的窗口。这个窗口背后的东西叫 Xcode Command Line Tools包含编译器、Git以及其他常用命令行工具。也就是说如果你从未主动装过Git可以先试着运行git --version如果终端提示git version 2.39.5 (Apple Git-xxx)之类说明Git已经可用。如果提示command not found通常会在弹窗里选择“安装”即可等它下载完成后Git就会自动出现在/Library/Developer/CommandLineTools/usr/bin/git。不弹窗的话也可以手动执行xcode-select --install这会唤起同样的安装流程。这个方式适合图省事的用户因为CLT自带Git完全能满足日常push/pull需求。但它的Git版本更新节奏比较慢如果之后遇到某些新特性比如更友好的冲突提示你会想用一份独立安装的新版本。2.2 用Homebrew装一份属于自己的GitHomebrew是Mac生态里使用最广泛的软件包管理工具它的官方说法是“The Missing Package Manager for macOS”。如果你还没有可以在终端粘贴官方安装脚本不过国内网络环境偶尔会碰到下载脚本慢的情况多试几次或换个网络环境一般就能解决。装好Homebrew之后安装Git只需一行brew install gitApple Silicon芯片的Mac上Homebrew默认安装在/opt/homebrewIntel芯片的Mac安装在/usr/local。安装完成后用which git看一下路径如果显示/opt/homebrew/bin/git说明你正在用的是新版brew的Git如果还是旧路径可能需要把Homebrew的bin目录加入PATH环境变量。我的建议是如果你的项目只是个人学习或普通开发CLT自带Git够用如果你想追求版本最新、或者后续要折腾 git-flow 这类扩展工具直接上Homebrew版更顺心。两者可以共存输入git时实际用的是PATH里优先匹配的那个版本。2.3 两个全局配置别漏姓名和邮箱很多新手装完Git就急着提交结果commit之后去看GitHub发现提交记录里的头像是一片灰、作者名也是乱码。原因是Git提交时会把user.name和user.email写进历史记录你必须告诉Git“我是谁”。在终端执行git config --global user.name 你的名字 git config --global user.email 你注册GitHub用的邮箱注意这个邮箱不一定要和GitHub注册邮箱一致但一致的话头像会正确关联。--global代表全局生效以后这台电脑上所有仓库都用这个身份提交。检查一下配置是否正确git config --list如果你只想看单个配置项可以加--getgit config --global --get user.name。这一步不做后面的commit虽然也能成功但GitHub上几乎看不出这个提交是谁做的质量大打折扣。3. 配置SSH Key让GitHub直接识别你的Mac3.1 一次配置解决所有免密问题SSH Key是“在Mac上把项目上传到GitHub”这条路上最重要的一道关卡。没有它你每次push都要输入token而token过期后还要去后台重新生成。花了十分钟把密钥配好以后所有项目都能永久免密推送这笔时间非常值。先检查自己是否已经生成过密钥ls -la ~/.ssh如果看到id_rsa、id_ed25519或id_ecdsa这类文件说明已经有现成的密钥对。但要注意要确认这把公钥确实已经添加到了GitHub后台否则后面还是会报Permission denied。新建密钥的推荐命令是ssh-keygen -t ed25519 -C 你注册GitHub的邮箱这里推荐ed25519而不是老牌的rsa。简单解释一下背后的原因RSA算法通常要2048位甚至4096位长度来保证安全密钥体积大、验证时计算量也大Ed25519是一种现代椭圆曲线签名算法密钥更短、生成更快、安全性更高而且GitHub早已完整支持。如果你因为某些历史原因必须用RSA那至少加上-b 4096保证强度。执行后终端会问保存位置直接按回车使用默认路径~/.ssh/id_ed25519即可。接着会问要不要设置passphrase也就是使用私钥时额外输入的密码。这里我的建议是个人电脑可以直接留空否则每次push都会多一步输入容易劝退新手如果是公司共用电脑或其他人对你有一定接触权限最好还是设一个毕竟私钥一旦泄露就等于把仓库访问权交了出去。3.2 把公钥内容复制到GitHub后台生成完密钥之后你电脑里会有两个文件id_ed25519是私钥必须留在本地id_ed25519.pub是公钥需要交给GitHub。查看公钥内容cat ~/.ssh/id_ed25519.pub输出是一长串以ssh-ed25519开头、以你邮箱结尾的字符串。鼠标选中整行复制注意不要漏掉末尾的邮箱部分。然后打开GitHub网页点右上角头像进入 Settings左侧菜单找到 SSH and GPG keys点 New SSH keyTitle可以写“MacBook”Key Type选Authentication Key把刚才复制的内容粘贴进去保存。这里有个常见的复制失误有人复制后内容变成了两行或者开头多个空格GitHub保存时会报格式错误。稳妥的做法是复制完粘贴到文本编辑器里看一眼确认是一行连续字符串后再提交。3.3 验证密钥是否生效配置完之后在终端运行ssh -T gitgithub.com如果看到类似一句话要求你确认主机的指纹输入yes回车。之后如果出现Hi 你的用户名! Youve successfully authenticated, but GitHub does not provide shell access.就说明密钥已经完全生效。这句提示的意思是你只能通过SSH访问Git仓库不能像普通服务器那样登录shell这是GitHub的预期行为不用担心。需要留意的是验证时走的端口是22。如果网络环境比较特殊GitHub偶尔连不上表现为ssh: connect to host github.com port 22: Operation timed out可以先检查网络是否稳定或者重新尝试。这里不展开那些特殊加速工具正常网络下多试几次通常就能成功。4. 建仓库、初始化、第一次推送的完整链路4.1 在GitHub网页端创建仓库时的两个细节进入GitHub首页点右上角的号选择 New repository。仓库名Repository name要起得直观比如my-blog、todo-app。Visibility选Public公开还是Private私有公开的话所有人都能看到代码私有则只有你和你授权的人能访问个人学习项目我一般选Private等真正想展示时再改Public。创建页面底部有几个初始化选项Add a README file、Add .gitignore、Choose a license。新手经常会顺手勾上README这本身没问题但要注意如果远程仓库已经有了README而本地仓库还是空的第一次push时会被拒绝因为两边都各自有“根提交”Git不知道如何合并。这一点我在下一章排查报错时会再讲。创建完成后页面会显示远程仓库地址。这里必须切换到SSH格式也就是gitgithub.com:用户名/仓库名.git。新版的GitHub页面在Code按钮附近有SSH标签点一下就会显示SSH地址别复制成HTTPS地址否则后面就要走token认证了。4.2 本地目录初始化从普通文件夹变成Git仓库假设你的项目文件夹叫my-project在终端进入这个目录cd ~/path/to/my-project git initgit init成功后目录里会多出一个隐藏的.git文件夹它记录了仓库的全部历史信息。用git status查看当前状态终端会列出哪些文件未被跟踪也就是Red状态的Untracked files。在第一次add之前强烈建议顺手写一个.gitignore文件内容是告诉Git哪些文件目录不需要纳入版本控制。不同项目的忽略规则差异很大举几个常见例子Mac系统文件.DS_StoreNode项目node_modules/Java项目target/、*.classXcode项目xcuserdata/、DerivedData/用文本编辑器新建.gitignore放到项目根目录即可。这一步的价值在于不把本地生成物、依赖包、临时文件推上GitHub远程仓库会干净很多。4.3 add、commit以及把本地分支改成main接下来把所有需要纳入版本控制的文件加入暂存区git add ..代表当前目录下的所有内容但那些被.gitignore规则排除的文件不会进来。用git status再看一次文件会从Untracked变成绿色状态表示已经进入暂存区。然后提交第一条记录git commit -m first commit如果你的全局配置好了user.name和user.email这里会看到1 file changed, xx insertions()之类的输出。现在检查分支名git branchGitHub新建仓库默认分支是main如果本地显示* master需要执行git branch -M main-M表示强制重命名当前分支大写M的含义是“即使目标分支已存在也覆盖”对刚初始化还没有任何提交的本地仓库来说这个操作绝对安全。4.4 关联远程仓库并完成第一次推送本地仓库和远程仓库建立联系用的是remote命令git remote add origin gitgithub.com:用户名/仓库名.gitorigin只是一个约定俗成的名字表示“远程仓库默认别名”你完全可以叫github或server但用origin能让所有教程、同事都秒懂。验证关联是否成功git remote -v看到两条同样地址的输出分别对应fetch和push就说明关联好了。然后推送git push -u origin main-u的参数是--set-upstream意思是把本地main分支和远程origin/main分支建立跟踪关系。以后在这个分支上只要敲git push或git pull就不用再带参数。第一次推送时如果之前SSH没有验证过指纹终端会显示一个host key确认提示输入yes即可。推送成功后刷新GitHub仓库页面代码就已经出现在上面了。到这一步“将项目上传至GitHub”的主流程已经走完。5. 推送失败的实战排查从报错反推根因5.1 Permission denied (publickey)密钥环节最常翻车刚配置完SSH就遇到push失败报错通常是gitgithub.com: Permission denied (publickey).看到publickey字样基本可以断定问题出在密钥环节。按这个顺序排查确认你用的是SSH地址而不是HTTPS地址。确认公钥已经粘贴到GitHub的SSH and GPG keys页面并且粘贴时没有多空格、少行。执行ssh -T gitgithub.com看看是否提示认证成功。如果报Permission denied说明GitHub后台没有匹配到你的公钥。检查你是否加载了正确的私钥。有时.ssh目录下有多把密钥默认用的不是你想用的那把可以执行ssh-add -l查看已加载列表。如果确认公钥没问题但是ssh-add列表为空可以手动把私钥加到ssh-agenteval $(ssh-agent -s) ssh-add ~/.ssh/id_ed25519之所以出现这个情况是因为新版macOS有时不会自动把新增密钥加入ssh-agent。如果以上都无效干脆重新生成一把新密钥然后按第3章的流程从头走一遍通常能解决。5.2 fatal: remote origin already exists重复添加远程地址执行git remote add origin ...时提示error: remote origin already exists.说明这个仓库之前已经关联过远程地址可能地址已经变了。先查看一下当前到底关联了什么git remote -v如果你看到的是一个不想要的地址可以修改git remote set-url origin gitgithub.com:用户名/仓库名.git如果你确定之前的关联毫无用处直接删掉再添加git remote rm origin git remote add origin gitgithub.com:用户名/仓库名.git这个报错本身不可怕怕的是你没看现有remote就乱删。尤其是公司项目里可能同时也关联了内部GitLab删了再重新添加反而麻烦。先git remote -v永远是最稳妥的第一步。5.3 Updates were rejected远程仓库里有本地没有的提交这个报错几乎是每个勾选了“Add a README file”的新手都会遇到的! [rejected] main - main (fetch first) error: failed to push some refs to ... hint: Updates were rejected because the remote contains work that you do not have locally.原因是远程仓库有了README或license、.gitignore这些初始提交而本地仓库也有自己的commit两边都是从“无”开始各自写了历史。Git不知道如何把它们接在一起于是拒绝推送。解决方式是用pull把远程内容合进来再pushgit pull --rebase origin main git push -u origin main为什么我要用--rebase而不是直接git pull因为默认git pull遇到历史分叉时会产生一个额外的merge commit让提交历史变得像打结的毛线。对于刚做完第一次commit、只想把README合并进来的场景--rebase会把你的本地提交“垫”在远程最新提交之后历史保持一条直线。执行完rebase后如果本地有main分支处于rebase过程终端会提示你用git pull --rebase再继续。此时可能遇到另一个问题--rebase时如果同一个文件两边都改过会产生冲突。Git会标出冲突文件你需要手动打开文件把、、之间的内容整理成想要的结果然后git add 冲突文件 git rebase --continue5.4 Repository not found地址、权限、可见性三选一这句报错常见形式ERROR: Repository not found. fatal: Could not read from remote repository.三个原因最常出现仓库地址拼错了用户名和仓库名对不上。仓库是Private私有仓库而你的GitHub账号没有被授权访问。远程地址写的是另一个协议而你的SSH Key用在了不同的平台上。先git remote -v确认地址。再确认GitHub登录账号是不是仓库所属账号或者这个仓库确实被加入了团队。如果仓库属于一个组织比如gitgithub.com:some-org/project.git你的账号必须在这个组织里并被授予访问权限。5.5 网络、大文件和.gitignore相关的隐藏坑网络层面偶尔会遇到push时连接超时比如Operation timed out。这种情况优先确认网络是否稳定以及能否正常访问GitHub页面。如果只是临时波动等一会儿重试通常就恢复了。不要盲目修改DNS或系统host文件很多时候越改越乱。大文件方面GitHub对单个文件有100MB限制超过50MB时可能已经有警告。如果项目里不小心放了大文件push会被拒报错会直接指出是哪个文件超限。处理办法是把这个文件从Git索引里移除并加入.gitignoregit rm --cached 大文件名 echo 大文件名 .gitignore git add .gitignore git commit -m remove large file注意--cached的含义是“只从Git索引中移除不删除磁盘上的实际文件”如果你直接git rm本地文件也会被删掉很容易造成损失。如果项目确实需要管理大型二进制文件应该使用Git LFS。还有一个很隐蔽的坑.gitignore写好了但文件还是被跟踪进来。原因是.gitignore只对未跟踪文件生效。如果一个文件已经被git add过、甚至已经提交那你再把它写进.gitignore也没用Git已经记住它了。这时需要执行git rm -r --cached . git add . git commit -m apply new gitignore这会清空索引重新按最新规则加入所有文件。我见过不少项目因为遗漏这个操作把node_modules整个推上GitHub远程仓库几百MB拉下来极其痛苦。6. 提交不是终点日常推拉的正确姿势6.1 推送前习惯性检查三件事项目成功上去之后你会进入每天改代码、推送的日常循环。我的习惯是每次准备push之前先依次看一眼git status、git diff、git log这三条命令分别回答三个问题哪些文件改过、具体改了什么、上一次提交到哪了。git status让你确认不会误传临时文件。git diff查看未暂存的改动细节如果发现改错了可以及时修正。git log --oneline快速浏览提交历史确认目前分支处于什么位置。这三个命令都是只读操作不会改变仓库状态可以放心随意敲。6.2 从远程拉取推荐rebase保持线性历史协作项目里经常需要把远程新提交拉回本地。个人认为对于小型团队git pull --rebase是最舒服的方式。它会把你在本地的新提交临时挪开先把远程最新提交拉下来再把你的提交按顺序放回去。这样历史是线性的看git log --graph不会有乱七八糟的岔路。如果遇到冲突Git会停下来提示你修改。冲突文件里会出现 HEAD 远程的内容 你本地的内容 你的commit你把两个区域整理成最终想要的样子后执行git add和git rebase --continue。rebase过程中如果中途想放弃可以git rebase --abort回到rebase之前的状态不会留下后遗症。6.3 commit message的写法值得花时间很多人习惯写first commit、update、fix一个人短期用还行三个月后回看历史几乎等于没写。我的经验是commit message遵循一个朴素的格式类型 简短描述。比如feat: add login page、fix: resolve navbar overlap issue、docs: update readme。这样做的好处是以后翻git log --oneline每一条都像一本书的目录你一眼就知道某次改动在做什么回滚时也能精准定位。一次commit尽量只做一件事不要把“改了样式、又加了接口、还删了旧文件”混在一条提交里。真要出问题时你会感谢自己当时保持了commit颗粒度。另外如果本地仓库已经和远程建立了跟踪关系push可以直接写git push但前提是当前分支设置了upstream。如果你创建了新的本地分支还没push过第一次仍然要用git push -u origin 分支名。我个人的实操体会是Mac上这套流程跑顺之后真正日常花时间的不是Git命令本身而是每次提交前想清楚“这次改动应该属于哪一条记录”。养成先git status再git diff的习惯每一条commit都带清楚描述你的项目历史会变得非常耐读。这也是我从“只会把代码塞到GitHub”到“能自由管理项目版本”之间收获最大的一步。