刚换了新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像你本地的记账本,每次修改文件后,选中需要记录的变化,在账本上记一笔,这一笔就是一次commit;GitHub像云端寄存箱,当你把账本里记录好的内容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形式的git@github.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装一份属于自己的Git
Homebrew是Mac生态里使用最广泛的软件包管理工具,它的官方说法是“The Missing Package Manager for macOS”。如果你还没有,可以在终端粘贴官方安装脚本,不过国内网络环境偶尔会碰到下载脚本慢的情况,多试几次或换个网络环境一般就能解决。
装好Homebrew之后,安装Git只需一行:
brew install gitApple Silicon芯片的Mac上,Homebrew默认安装在/opt/homebrew,Intel芯片的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如果你只想看单个配置项,可以加--get:git config --global --get user.name。这一步不做,后面的commit虽然也能成功,但GitHub上几乎看不出这个提交是谁做的,质量大打折扣。
3. 配置SSH Key:让GitHub直接识别你的Mac
3.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 key,Title可以写“MacBook”,Key Type选Authentication Key,把刚才复制的内容粘贴进去,保存。
这里有个常见的复制失误:有人复制后内容变成了两行,或者开头多个空格,GitHub保存时会报格式错误。稳妥的做法是复制完粘贴到文本编辑器里看一眼,确认是一行连续字符串后再提交。
3.3 验证密钥是否生效
配置完之后,在终端运行:
ssh -T git@github.com如果看到类似一句话,要求你确认主机的指纹,输入yes回车。之后如果出现:
Hi 你的用户名! You've 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格式,也就是git@github.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_Store - Node项目:
node_modules/ - Java项目:
target/、*.class - Xcode项目:
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 git@github.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失败,报错通常是:
git@github.com: Permission denied (publickey).看到publickey字样,基本可以断定问题出在密钥环节。按这个顺序排查:
- 确认你用的是SSH地址,而不是HTTPS地址。
- 确认公钥已经粘贴到GitHub的SSH and GPG keys页面,并且粘贴时没有多空格、少行。
- 执行
ssh -T git@github.com,看看是否提示认证成功。如果报Permission denied,说明GitHub后台没有匹配到你的公钥。 - 检查你是否加载了正确的私钥。有时
.ssh目录下有多把密钥,默认用的不是你想用的那把,可以执行ssh-add -l查看已加载列表。
如果确认公钥没问题但是ssh-add列表为空,可以手动把私钥加到ssh-agent:
eval "$(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 git@github.com:用户名/仓库名.git如果你确定之前的关联毫无用处,直接删掉再添加:
git remote rm origin git remote add origin git@github.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把远程内容合进来,再push:
git 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登录账号是不是仓库所属账号,或者这个仓库确实被加入了团队。如果仓库属于一个组织,比如git@github.com:some-org/project.git,你的账号必须在这个组织里并被授予访问权限。
5.5 网络、大文件和.gitignore相关的隐藏坑
网络层面,偶尔会遇到push时连接超时,比如Operation timed out。这种情况优先确认网络是否稳定,以及能否正常访问GitHub页面。如果只是临时波动,等一会儿重试通常就恢复了。不要盲目修改DNS或系统host文件,很多时候越改越乱。
大文件方面,GitHub对单个文件有100MB限制,超过50MB时可能已经有警告。如果项目里不小心放了大文件,push会被拒,报错会直接指出是哪个文件超限。处理办法是把这个文件从Git索引里移除,并加入.gitignore:
git 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”到“能自由管理项目版本”之间,收获最大的一步。