很多人在本地把代码写好了,准备上传到 GitHub 或者码云 Gitee 仓库时,都会在某个环节突然卡住:有的卡在 push 时提示 Authentication failed,有的卡在 remote origin already exists,还有的在网页上看到了一个 README 文件之后怎么推都推不上去。我当初学 Git 时也把这些报错挨个踩了一遍,后来才意识到,所谓"用 git 将本地文件上传到 GitHub 或者码云 Gitee 仓库",本质上没有多复杂,真正让新手困惑的是它背后那套"本地仓库-暂存区-远程仓库"的模型,以及命令在不同版本下的行为差异。
这篇文章我会从工作流讲起,把从零上传的完整操作拆成每个步骤,再补充 HTTPS 和 SSH 两种连接方式、高频报错的排查思路,最后讲一下同一份代码同时维护 GitHub 和 Gitee 的进阶做法。适合刚装好 Git 准备上传第一个项目的人,对已经能正常推送的人也会有一点排错参考价值。
1. 动手不传错:先把 Git 的本地工作区和远程仓库模型理清楚
1.1 工作区、暂存区、本地仓库不是一回事
很多人以为"上传文件"就是选中文件往云上一扔,Git 不是这么工作的。在 Git 的世界里,你的项目目录至少分成三层:工作区是你能看见、能编辑的普通文件夹;暂存区是下一次提交要记录哪些文件变化的清单;本地仓库是 .git 目录里的对象数据库,commit 之后才算真正写入历史。
可以打一个不太严谨但很好用的比方:整理行李去长途旅行。git add 是把你觉得要带的衣服挑出来放进箱子(暂存区),git commit 是拉上拉链、在行李清单上签上时间和行李编号(生成一个快照),git push 则是把行李送到车站托运(同步到远程)。如果没执行 commit,箱子还是空的;如果只执行了 commit 没 push,东西还在家里,远程仓库什么也不会看到。
命令行里最好用 git status 随时确认状态。它会把"Changes not staged for commit""Changes to be committed"这类状态直接列出来,一般不会产生歧义。
1.2 远程仓库只是个"裸仓库",页面上的文件只是展示层
GitHub、Gitee 这类平台上的仓库,本质上只保存 Git 对象和引用,并不像本地那样有一个可编辑的工作区。它通过网页端把 master/main 分支的最新内容渲染出来给你看,让你以为"这就像网盘里的文件夹"。理解这一点对排查问题很有用:当你在网页端新建仓库时勾选了"添加 README"或".gitignore",远程就产生了一次独立于你本地的 commit。如果本地是 git init 出来的全新仓库,两边没有任何共同历史,push 时 Git 会默认拒绝合并两个不相关的历史——这就是"创建仓库时勾了 README 导致推不上去"这个经典坑的根源。
另一个连带细节:Git 不会追踪空目录。如果你有一个 docs/ 目录想保留结构,但又没放任何文件,推上去之后该目录会消失。常规做法是放一个空的占位文件,比如 .gitkeep。很多项目会为 assets 目录这么做,真实项目里经常见到。
1.3 这套流程覆盖的场景和边界
本文讲的"上传方法"是标准整仓上传,也就是把本地整个项目目录纳入 Git 管理后,关联到一个远程仓库地址。如果你的实际需求是"只把一个大目录里某个子文件夹上传",建议直接在那个子文件夹里执行 git init,不要试图靠 .gitignore 把一个普通目录变成半个仓库,维护成本会高很多。
同样,如果项目里已经有超过 100MB 的单个文件,GitHub 在 push 时会直接拒绝(超过 50MB 会有提示),这类场景需要引入 Git LFS 或换用其他存储思路,不属于普通 push 的范畴。理解了这些边界,下面进入实操准备阶段就不会被后面的报错带偏。
2. 上传前必做的三件套:装好 Git、配置身份、建好空仓库
2.1 安装 Git 和第一行验证命令
不同系统装 Git 的方式不大一样,Linux 下常见的是 sudo apt install git,macOS 可以用官方安装器或通过包管理器装,Windows 上装完推荐直接打开 Git Bash 而不是 CMD,因为后面很多命令行语法在 Git Bash 里更自然。装好之后,打开终端输入:
git --version能看到类似 git version 2.39.1 的输出,就说明基本环境没问题。这一步往往被忽略,但实际遇到"command not found"或环境变量混乱时,先确认版本能让后续排查省很多时间。
2.2 配置 user.name 和 user.email:为什么省不掉
提交记录必须有作者信息,Git 默认会用你系统里已有的用户名去猜,猜出来的结果通常不对。所以上传前先做一次全局配置:
git config --global user.name "yourname" git config --global user.email "you@example.com"注意两点。第一,这里的邮箱最好和你注册 GitHub/Gitee 账号的邮箱一致,否则提交虽然能进仓库,个人主页的 contribution 统计不会把这次提交算到你名下。第二,--global 表示对所有仓库生效;如果不同项目想用不同身份,项目内执行 git config 不带 --global 即可覆盖。等以后出问题时用 git config --list 检查,能看到配置来源。
2.3 在 GitHub/Gitee 创建空仓库的关键选项
网页端新建仓库时,会看到一堆初始化选项:README、.gitignore、License。我的建议是:凡是明确要走本文这套命令行推送流程的人,创建时这三个选项都不要勾选,就把仓库保持成完全空的状态。原因前面说过——只要勾选了任何一个,远程就有了一个本地不存在的初始提交,第一次 push 大概率会遇到被拒的报错。
创建完成后,页面会显示两种可用的仓库地址:HTTPS 形式是 https://github.com/yourname/demo-repo.git,SSH 形式是 git@github.com:yourname/demo-repo.git。Gitee 的地址结构类似,只是域名换成 gitee.com。这两个地址要复制好,后面 remote add 和 push 都要用到。
提示:如果实在已经勾选了 README,后面可以先用 git pull --rebase origin main --allow-unrelated-histories 把两边历史合起来再推送。能救回来,但比直接空仓库麻烦不少。
3. 本地文件进远程仓库的五步操作全解
3.1 git init 之前,先把 .gitignore 想明白
在项目目录里打开终端,执行:
cd /path/to/your/project git initgit init 会在当前目录生成一个 .git 子目录,从这一刻起目录才被 Git 接管。但更聪明的做法是在 git add 之前就写好 .gitignore。我见过不少同学把依赖目录、构建产物、IDE 配置一股脑传上去,仓库瞬间几千个文件,代码审查根本没法审。
一个前端项目典型的 .gitignore 内容大致长这样:
node_modules/ dist/ build/ *.log .idea/ .DS_Store .env不同语言项目写法不同,原则就是三句话:可再生的依赖目录不进仓库(node_modules、target、vendor、venv);本地环境和密钥不进仓库(.env、.pem);缓存和临时文件不进仓库(*.log、.cache)。万一你已经把不该传的传上去了,后面唯一稳妥的办法是先从历史里清除,那又是一段麻烦事,所以尽量在一开始就规避。
3.2 git add 和 git commit:提交信息比很多人想的更重要
先把文件放进暂存区:
git add .git add . 会把当前目录下所有没被 .gitignore 忽略的文件都暂存。如果只想提交特定文件,可以写成 git add src/index.js。执行完 git status 看一下,会列出绿色字体的"将提交的更改",这是暂存区在工作区之上的直观体现。
然后生成快照:
git commit -m "feat: 完成初始项目结构"提交信息我会建议用一种简单的约定式风格,前缀加类型词,比如 feat: 表示新功能、fix: 表示修复、docs: 表示文档。日后回看改动时,能一眼看出每个提交在干嘛,比"update"和"111"这种信息强得多。第一次 commit 可以简单写"init project",问题不大,关键是别为空。
3.3 关联远程仓库:remote 名字只是一个指针
拿到复制好的仓库地址后,执行:
git remote add origin https://github.com/yourname/demo-repo.git这里的 origin 只是一个默认约定名,你可以换成任何名字,比如 gitee、backup。它的本质是给远程地址起一个本地别名,让后续命令不用写完整 URL。验证是否关联成功:
git remote -v应该能看到 origin 对应的 fetch 和 push 地址。如果提示 fatal: remote origin already exists,说明项目已经关联过远程,这时不要急着再 add。要么用 git remote remove origin 再重新 add,要么更柔和一点,用 git remote set-url origin <新地址> 只改 URL,保留 remote 配置里的其他状态。
3.4 分支名与首次推送:main、master 的混乱源头
首次推送前要确认一件事:本地当前分支的名字。Git 新版默认可能是 main,也可能是 master,取决于你的 git init 初始化和全局配置。GitHub 的新仓库默认分支是 main,Gitee 的新仓库默认分支是 master。两边不一致就会引发"看起来代码在本地,但推送后不知道被推到哪了"的困惑。
最直接的做法是把本地分支统一命名成自己想要的,比如:
git branch -M main-M 是强制改名,即使当前分支还叫 master 也会改过来。然后执行首次推送:
git push -u origin main这里 -u 的作用是建立"上游跟踪关系",让本地 main 分支和远程 origin/main 分支关联起来。有了这个关系,以后在这个分支直接敲 git push 就可以了,不用每次都写完整参数。
如果远程仓库在 Gitee,而且你希望远程分支叫 master,推送时可以显式做映射:
git push -u origin main:master冒号左边是本地分支名,右边是远程分支名。这个技巧在推送默认分支名不一致时会非常管用。
3.5 完整命令块:给想直接抄作业的人
把上面五步合并成一个完整流程,这里不再做解释,跟读一遍就能理解整条链路:
cd /path/to/your/project git init git add . git commit -m "init project" git remote add origin https://github.com/yourname/demo-repo.git git branch -M main git push -u origin main这一段适合"先把事情跑通,再慢慢理解原理"的人。跑通之后,你会发现各种图形化的 Git 工具,本质上也是在做这串命令里的事,只是把步骤打包成了按钮。我建议第一次上传全程用命令行,因为只有能看懂每个操作发生了什么,遇到报错时才不会对着一堆按钮无从下手。
4. HTTPS 还是 SSH?两条路都实测之后,我的选择和建议
4.1 两种连接方式的核心差异
Git 操作远程仓库,最常见的是 HTTPS 和 SSH 两种通道。它们最后都能把代码推上去,但在配置方式、认证原理和使用体验上差别不小。
| 对比项 | HTTPS | SSH |
|---|---|---|
| 认证方式 | 用户名加访问令牌 | 私钥与公钥配对 |
| 首次配置 | 复制地址直接 push,按提示输凭据 | 需要生成密钥并添加到平台 |
| 更换电脑 | 重新登录即可,较简单 | 新机器要重新配私钥和公钥 |
| 推送频率低/一次性 | 合适 | 没必要 |
| 长期主力开发 | 可用但有时会反复要求认证 | 更顺滑,推荐 |
| 常见失败点 | 密码和 Token 混淆 | 公钥未添加、私钥未加载 |
如果只是偶尔上传一次,HTTPS 其实是最快的路径;如果每天要在多个仓库之间来回推送,SSH 配好之后省心很多。两种方式可以同时存在,同一个仓库在 remote URL 上随时可以切换。
4.2 HTTPS 方式下的关键认知:密码不是密码
现在 GitHub 和 Gitee 都已经不允许用明文账号密码直接完成 push,HTTPS 方式要求你使用访问令牌(Personal Access Token)。GitHub 的令牌在账号的 Developer settings 里生成,Gitee 的令牌在安全设置里的私人令牌中生成,生成时记得勾选仓库相关的权限范围。
实际 push 时,用户名还是你的账号名,密码框里输入的是令牌字符串。很多"Authentication failed"报错,就是因为他把平时登录平台的密码当成了 push 密码。
另外一个经验是不要把令牌以明文拼进命令里。比如写 git push https://username:token@github.com/... 这种方式虽然能通,但 shell history、终端记录里都会留下完整令牌,泄露风险极大。正确做法是让 Git 的凭据管理器记住令牌,之后使用时会自动提供。
4.3 SSH 密钥生成与双平台配置
SSH 方式的认证基础是一对密钥。在终端执行:
ssh-keygen -t ed25519 -C "you@example.com"一路回车即可,生成的默认位置在 ~/.ssh/id_ed25519。查看公钥内容:
cat ~/.ssh/id_ed25519.pub把输出的整段内容分别加到 GitHub 的 SSH keys 设置页和 Gitee 的 SSH 公钥设置页。两个平台都添加同一个公钥完全没问题,因为公钥在匹配时会带上厂商自己的域名,互不干扰。
添加之后验证是否配通:
ssh -T git@github.com ssh -T git@gitee.comGitHub 会返回类似 "Hi yourname! You've successfully authenticated" 的话术,Gitee 也类似,只是提示语会写明 GITEE.COM。看到这句话,就说明密钥通道已经打通,可以把 remote 地址改成 SSH 形式了:
git remote set-url origin git@github.com:yourname/demo-repo.git之后 push 就不再需要输令牌。
4.4 我自己的选型习惯
个人经验是:主力项目尽量用 SSH,一次性脚本、临时测试仓库直接用 HTTPS 和处理凭据,不额外折腾密钥。理由很现实,SSH 的配置是一次性的,配好之后长时间稳定;HTTPS 在碰到凭据管理器缓存混乱时偶尔抽风。如果是在共享电脑上操作,SSH 私钥放别人机器上要格外谨慎,HTTPS 方式反而能更快清掉凭据,不给别人留后门。每张密钥就是一把房子的钥匙,管理习惯直接决定后面的安全边界。
5. 推送失败的高频报错和完整排查链路
5.1 报错原文与原因对照表
以下是我和身边同事在真实项目里高频遇到的报错,整理成一张速查表。表格的意义在于,先确定报错属于"认证问题""历史问题"还是"地址问题",再着手解决,而不是对着错误信息瞎试命令。
| 报错信息关键词 | 本质原因 | 对应解决方向 |
|---|---|---|
| fatal: not a git repository | 当前目录不是 Git 仓库 | 检查 pwd,执行 git init 或进入正确目录 |
| fatal: remote origin already exists | 远程已关联过 | git remote remove origin 或 set-url 覆盖 |
| remote: Repository not found | 仓库地址写错或没有权限 | 核对用户名/仓库名/后缀.git,检查访问权限 |
| Authentication failed | 凭据不对,密码框误填账号密码 | 改用 Token;清理系统凭据管理器的旧记录 |
| Permission denied (publickey) | SSH 公钥未配置,或私钥未加载 | 上传公钥;ssh-add 加载私钥;ssh -T 验证 |
| ! [rejected] (fetch first) / non-fast-forward | 远程有本地没有的提交 | 先 pull --rebase,解决冲突后重新 push |
| failed to push some refs | 常伴随上一个报错 | 检查分支名是否对齐,再走 rebase 流程 |
5.2 一个完整的排查案例:新仓库勾了 README 导致推不上去
某开发者在 GitHub 新建仓库时顺手勾选了"Add a README file",本地 git init 后完成了 add 和 commit,接着执行 git push -u origin main,结果被拒绝。报错里有 rejected (non-fast-forward) 字样,还提到了远程存在当前分支没有的工作。
当时我的排查顺序是这样走的:先 git remote -v 确认关联的仓库地址正确;再打开网页端仓库页面,发现远程有且仅有一次"Initial commit"——那份自动生成的 README;然后 git log 看本地唯一的提交,两者确实没有任何共同祖先。根因很清楚:远程有历史、本地也有历史,但两条历史互不认识,Git 默认不允许强制覆盖。
解决办法是让两边先合并,用 --allow-unrelated-histories 允许合并不相关的历史:
git pull --rebase origin main --allow-unrelated-histories本地原来已经写了一版 README,自动生成的 README 也在这条历史里,rebase 时大概率会产生冲突。处理方式和其他冲突一样:打开冲突文件,把两边内容整理成想要的样子,然后 git add 该文件,再 git rebase --continue。等 rebase 完成,本地历史和远程就有了公共基点,此时再执行 git push -u origin main 就能正常推送。
这个案例非常有代表性,因为"新仓库勾选初始化文件"在网页端是默认推荐动作,很多人都是这么入坑的。所以我的建议还是:命令行上传时,创建仓库保持空状态最省事。
5.3 另一个案例:HTTPS 认证失败,问题其实出在残留凭据
有一次同事推代码时连续收到 Authentication failed,原因是系统凭据管理器里存着他很久以前输入的旧密码。虽然新生成的令牌已经复制到剪贴板,但凭据管理器会优先提供旧记录,Git 根本没把新的"用户名/令牌"组合当回事。
处理方法是打开操作系统的凭据管理器,找到 GitHub 或 Gitee 相关的条目删除,再重新 push,输入正确的令牌。macOS 则要去钥匙串访问里搜索对应域名删除。这类问题非常隐蔽,报错信息一点也不会提示"缓存里有旧密码",只能靠经验判断。
5.4 一套通用的排查顺序
遇到过几次报错之后,我总结出一个属于自己的排查顺序,分享出来给大家参考:
- 先确认自己在哪个目录:pwd && git status,排除"命令打错地方"这种低级错误。
- 再确认远程关联:git remote -v,看地址对不对。
- 确认当前分支:git branch,看本地分支名和想推送的分支是否一致。
- 尝试 git fetch origin,观察能不能从远程拿到数据;如果能 fetch,说明认证和网络基本没问题,问题大概率出在历史合并上。
- 最后看网页端仓库页面,确认远程的默认分支和提交情况。
这套顺序本质上是把"本地状态、远程关连、网络认证、历史关系"四层逐一排查。每次报错都要先读完整错误信息,Git 的报错其实写得很克制,但信息量足够,挨个看下来基本能定位问题。
6. 一份代码同时维护 GitHub 和 Gitee:双远程实战
6.1 为什么需要两个远程仓库
很多人会在 GitHub 和 Gitee 各建一个仓库放同一份代码。原因不难理解:GitHub 生态完善、Gitee 在部分网络环境下访问速度更快;两边各留一份也等于多一处备份。Git 本身不限制一个本地仓库关联多少个远程,remote 名称只是一串别名,所以这个需求完全可以靠配置解决,不需要复制两份项目分别维护。
双远程的常见组织方式有三种。第一种是两个独立的 remote,分别叫 origin 和 gitee,推送时明确要推给谁;第二种是一个 remote 下挂多个 push 地址,执行一次 push 同时推到两边;第三种是不同分支对应不同远程,比如 main 推 GitHub,develop 推 Gitee。对绝大多数人来说,前两种足够日常使用。
6.2 双 remote 的配置与推送
假设已经把 GitHub 仓库关联成了 origin,再来关联 Gitee:
git remote add gitee https://gitee.com/yourname/demo-repo.git git remote -v第二条命令会列出两个 remote:origin 指向 github.com,gitee 指向 gitee.com。推送时分别执行:
git push -u origin main git push -u gitee master注意分支名映射问题,Gitee 仓库默认分支是 master,本地 main 推上去后远程仓库页面里的分支可能叫 main,不一定符合预期。可以用显式映射把本地 main 推到远程 master:
git push -u gitee main:master这条推送执行后,Gitee 网页端的默认分支如果还是 master,展示内容就会和本地 main 对应起来。
6.3 一个 remote 多个 push 地址:一条命令推两家
如果想省掉第二条 push 命令,可以用 Git 的 set-url 特性,给同一个 remote 挂多个 push 地址:
git remote set-url --add origin https://gitee.com/yourname/demo-repo.git之后 git remote -v 会看到 origin 下有两个 push 地址。执行一次 git push origin main,Git 会依次把提交推送到两个平台。
不过这个玩法有个坑:只要其中一个地址失败,整条命令会返回非零退出码,容易让人误以为"应该都成功了"或者反之。我在实际项目里遇到过 GitHub 推送成功、Gitee 因网络问题失败,但命令输出没有逐个平台明确列出的情况,排查起来要多花几分钟。所以我现在更推荐的做法是显式指定远程分别推送,虽然多敲一条命令,但输出清晰,哪一步失败一眼就能看出来。
6.4 多账号场景下的 SSH 配置
前面讲的 SSH 配置默认使用 ~/.ssh/id_ed25519 这一把私钥。如果工作中同时使用个人账号和团队账号,而在同一平台又不能把一个公钥绑定到多个账号,就需要配置 SSH 别名。
在 ~/.ssh/config 里写类似这样的内容:
Host github-work HostName github.com User git IdentityFile ~/.ssh/id_ed25519_work Host github-personal HostName github.com User git IdentityFile ~/.ssh/id_ed25519_personal生成第二把密钥时使用不同文件名:
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_work -C "work@example.com"配置完成后,仓库地址里的主机名要相应改成别名,例如 git@github-work:yourteam/demo-repo.git。这样同一台机器就能用不同私钥访问不同账号下的仓库。注意 Host 和 HostName 的区别:Host 是本地别名,HostName 才是真实服务器域名。
最后分享一点自己的实操体会。我用了很久的 Git 之后发现,"上传代码到远程仓库"这件事真正花时间的,从来不是敲完那几行命令,而是建立一套对状态和报错有感知的工作习惯。比如 commit 前看一眼 git status,push 前先想清楚远程分支名,创建远程仓库时忍住不勾 README。这些微小的习惯能让整个流程稳定很多。如果你现在正卡在某个报错里,建议先不要急着乱试命令,按上面 5.4 节的顺序把几个层面过一遍,绝大多数问题都能定位到具体原因。