0. 前言
git在團隊協作中有重要作用,是有必要進行系統學習的一門工具。本篇是筆者學習git操作的備忘筆記,主要在linux上進行。
1. git歷史
很多人都知道,Linus在1991年創建了開源的Linux,從此,Linux系統不斷發展,已經成為最大的服務器系統軟件了。
Linus雖然創建了Linux,但Linux的壯大是靠全世界熱心的志愿者參與的,這么多人在世界各地為Linux編寫代碼,那Linux的代碼是如何管理的呢?
事實是,在2002年以前,世界各地的志愿者把源代碼文件通過diff的方式發給Linus,然后由Linus本人通過手工方式合并代碼!
你也許會想,為什么Linus不把Linux代碼放到版本控制系統里呢?不是有CVS、SVN這些免費的版本控制系統嗎?因為Linus堅定地反對CVS和SVN,這些集中式的版本控制系統不但速度慢,而且必須聯網才能使用。有一些商用的版本控制系統,雖然比CVS、SVN好用,但那是付費的,和Linux的開源精神不符。
不過,到了2002年,Linux系統已經發展了十年了,代碼庫之大讓Linus很難繼續通過手工方式管理了,社區的弟兄們也對這種方式表達了強烈不滿,于是Linus選擇了一個商業的版本控制系統BitKeeper,BitKeeper的東家BitMover公司出于人道主義精神,授權Linux社區免費使用這個版本控制系統。
安定團結的大好局面在2005年就被打破了,原因是Linux社區牛人聚集,不免沾染了一些梁山好漢的江湖習氣。開發Samba的Andrew試圖破解BitKeeper的協議(這么干的其實也不只他一個),被BitMover公司發現了(監控工作做得不錯!),于是BitMover公司怒了,要收回Linux社區的免費使用權。
Linus可以向BitMover公司道個歉,保證以后嚴格管教弟兄們,嗯,這是不可能的。實際情況是這樣的:
Linus花了兩周時間自己用C寫了一個分布式版本控制系統,這就是Git!一個月之內,Linux系統的源碼已經由Git管理了!牛是怎么定義的呢?大家可以體會一下。
Git迅速成為最流行的分布式版本控制系統,尤其是2008年,GitHub網站上線了,它為開源項目免費提供Git存儲,無數開源項目開始遷移至GitHub,包括jQuery,PHP,Ruby等等。
歷史就是這么偶然,如果不是當年BitMover公司威脅Linux社區,可能現在我們就沒有免費而超級好用的Git了。
2. git三大區
- 工作區(The working tree):即電腦中git倉庫所在的目錄,進行工作和修改的目錄。
- 暫存區(The staging area):暫時存放你的修改的區域。 通過
git add
命令將工作區改動的內容(包括修改的文件、新增的文件)添加到暫存區。 暫存區是你下次要提交的內容,"that stores information about what will go into your next commit"。 - 版本庫(The Git directory):是存放git倉庫中各歷史版本的區域,在倉庫所在根目錄的.git/目錄下。 當你執行commit提交時,git會將暫存區的內容復制到版本庫中,并設為最新版本。 當執行clone從遠程克隆倉庫當本地時,會將那個遠程倉庫的版本庫也克隆下來。 在版本庫中,有一個HEAD指針,它指向當前分支(通常是master)的當前版本。
3. git配置
git config --global user.name "yourname"
配置你的名字
git config --global user.email youremail
配置你的郵箱
--global
對當前用戶,--system
對所有用戶
git config --list
查看已有的配置信息
vim ~/.gitconfig
編輯當前用戶的git配置文件,vim /etc/gitconfig
所有用戶的git配置文件
vim .gitignore
倉庫根目錄下的.gitignore
文件中配置了git操作時的忽略文件
4. git基礎操作
4.1. 創建本地倉庫
git init
在當前文件夾創建一個可git管理的本地倉庫
4.2. 將工作區保存到暫存區
git add xxx
add
命令并非添加一個文件。而是,將修改或新增的文件從工作區添加到當前本地倉庫的暫存區中,表明此修改或新增的文件在下次commit提交的內容之中了。常用git add
來添加新增的文件。
git add *
將所有修改與新增的文件添加到暫存區。當你在倉庫根目錄下設置了.gitignore
文件并配置了忽略文件時,此命令會報錯,如下:
$ git add *
The following paths are ignored by one of your .gitignore files:
ignore.md
Use -f if you really want to add them.
fatal: no files added
如果你強行使用git add -f *
命令,則會添加ignore.md文件。但這樣.gitignore
文件就沒有意義了,所以不建議使用git add *
和git add -f *
。
4.3. 將暫存區提交到版本庫
git commit
將暫存區提交到本地倉庫的版本庫。若存在沒有被git add
添加到暫存區的文件,則不會被提交。
-m "xxxx"
后面跟著的為本次提交的說明。git commit
時必須使用此選項。
-a
官方解釋:"Tell the command to automatically stage files that have been modified and deleted, but new files you have not told Git about are not affected." 意思大概是:-a
將工作區中所有進行修改或刪除后的文件都提交到本地倉庫,但新增的文件并不提交。
常用commit -m"aaa" -a
直接將更改從工作區提交到版本庫,而不用經過暫存區。
4.4. 撤銷(重置)
git reset --hard
等價于 git reset --hard HEAD
即撤銷工作區和暫存區中的所有修改,重置為版本庫中HEAD指向的當前版本(未被修改的部分)。
而上一個版本就是 HEAD^
,上上一個版本就是 HEAD^^
,當然往上100個版本寫100個^比較容易數不過來,所以寫成 HEAD~100
。
或者,你也可以 git reset --hard <commit_id>
直接回溯到某個版本,<commit_id>
指版本號(通過 git log 查看),版本號沒必要寫全,前幾位就可以了,Git會自動去找。
git reset <file>
或git reset HEAD <file>
撤銷git add
到暫存區的修改。
git checkout -- <file>
撤銷工作區的修改。
4.5. 刪除
git rm <file>
如果你已經把某個新增文件提交到版本庫了,然后你想刪除這個文件,那就用該命令將文件刪去,再提交。使用該命令后,git的狀態是:
$ git rm test.md
rm 'test.md'
$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
deleted: test.md
說明 git rm
會直接更新到暫存區。
刪除文件其實還可以,直接刪除,如下:
$ ls
README.md ignore2.md test2.md
$ rm test2.md
$ ls
README.md ignore2.md
$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
deleted: test2.md
no changes added to commit (use "git add" and/or "git commit -a")
此時沒有直接更新到暫存區,可以通過 git add/rm <file>
來更新。同時,可以通過 git checkout -- <file>
來恢復這個文件。
4.6. 查看狀態
git status
顯示工作區與暫存區的狀態,比如哪些文件被修改了、哪些文件沒保存到暫存區、哪些文件沒提交等等。若存在沒被add的新文件(即未被git跟蹤的文件),則會提示Untracked
。
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: .gitignore
# deleted: ignore.md
#
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified: test.md
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# ignore2.md
git status -s
更簡單地顯示狀態變化,如:顯示 M controllers/article.go
顯示內容的每一行開頭,M表示被修改、A表示被增加、??表示未受控制
$ git status -s
A .gitignore
D ignore.md
M test.md
?? ignore2.md
4.7. 查看不同
git diff readme.txt
查看readme.txt這個文件中具體什么內容被修改了。其中:
-
git diff
查看工作區和暫存區的區別 -
git diff --cached
查看暫存區和版本庫之間的區別 -
git diff HEAD
查看工作區和版本庫之間的區別
4.8. 查看提交日志
git log
查看倉庫版本庫中(本地倉庫和遠程倉庫的版本庫是一樣的)從最近到最遠的版本信息。換而言之,即每次提交的信息。如下是一次的信息:
commit ee26988de11d36133d180663ddf7b24c4a6233e5
Author: douNine <test@dev3.airdb.io>
Date: Fri Dec 21 00:47:21 2018 +0800
modify task/sync_bbs.go
其中commit一項對應的,是git提交的版本號
git log --graph
顯示版本以及分支圖,可以說很生動形象了
git reflog
查看歷史git命令
4.9. 隱藏工作
當工作區的修改未提交,而我們又想將其隱藏起來時,就需要用到git的stash
功能。
git stash
隱藏工作區和暫存區的修改,儲存到一個地方。
$ git status
On branch master
Your branch is ahead of 'origin/master' by 4 commits.
(use "git push" to publish your local commits)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: test2.md
no changes added to commit (use "git add" and/or "git commit -a")
$ git stash
Saved working directory and index state WIP on master: f0f89d4 --no-ff merge
$ git status
On branch master
Your branch is ahead of 'origin/master' by 4 commits.
(use "git push" to publish your local commits)
nothing to commit, working tree clean
git stash list
查看儲存起來的工作現場。
$ git stash list
stash@{0}: WIP on master: f0f89d4 --no-ff merge
恢復工作現場有兩種方法:
一是用git stash apply
恢復,但是恢復后,stash內容并不刪除,你需要用git stash drop
來刪除;
另一種方式是用git stash pop
,恢復的同時把stash內容也刪了。
5. git分支
5.1. 分支的作用:
分支在實際中有什么用呢?假設你準備開發一個新功能,但是需要兩周才能完成,第一周你寫了50%的代碼,如果立刻提交,由于代碼還沒寫完,不完整的代碼庫會導致別人不能干活了。如果等代碼全部寫完再一次提交,又存在丟失每天進度的巨大風險。
現在有了分支,就不用怕了。你創建了一個屬于你自己的分支,別人看不到,還繼續在原來的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到開發完畢后,再一次性合并到原來的分支上,這樣,既安全,又不影響別人工作。
總而言之,就是你可以在另外的git分支上干自己的事,而不影響主分支。做完還可以更新到主分支上。
注:git中主分支只有一條,叫 master
。一條分支就是一條時間線,每個時間節點代表著每個版本。master
是主分支的名字,同時也是指向主分支最新節點的指針,而HEAD
一開始指向的就是 master
指針。
5.2. 創建、切換分支
git branch dev
創建dev分支。dev分支會繼承master分支的所有內容。
git checkout dev
切換到dev分支上。
git checkout -b dev
創建dev分支,然后切換到dev分支上。git checkout命令加上-b參數表示創建并切換分支,相當于以下兩條命令:
$ git branch dev
$ git checkout dev
Switched to branch 'dev'
5.3. 查看分支
git branch
查看分支,*
后面的是當前所處分支。
$ git branch
* dev
master
git branch -a
查看遠程和本地所有的分支信息。
git log --graph --pretty=oneline --abbrev-commit
該命令常用于生動形象地顯示簡略的分支圖。顯示如下:
$ git log --graph --pretty=oneline --abbrev-commit
* 07bb2d8 (HEAD -> master, origin/master, origin/HEAD, dev) merge
|\
| * 5a0a7da commit in dev
* | 48a4216 commit in master
|/
* 6aa880e a
* 91147e8 解決分支沖突
|\
| * ea5cf82 commit in dev
* | 28809bb commit in master
|/
* 82016a7 commit in dev
* 6bf1a5d git rm
* 09f2106 bb
* 4524372 aabb
* 058e02c aaa
* f5cc038 git add -f * can add all file including the ignore file
* b5f8f30 aa
* 92c65b1 test add
* d0fe8dc test
* 3a870e8 true demo
* 09d694b find why show falsely
* d6b46e1 also test
* a757718 test.md
* 69483da Update README.md
* cfb9ede Update README.md
* 9bdf268 Merge pull request #1 from 99MyCql/readme-edits
|\
| * 3cef569 Update README.md
|/
* 677f493 Initial commit
5.4. 合并分支
在dev分支上添加一些內容,然后提交到該分支上,如下:
$ git add branch.md
$ git commit -m"commit in dev"
[dev 82016a7] commit in dev
1 file changed, 1 insertion(+)
create mode 100644 branch.md
$ git status
On branch dev
nothing to commit, working tree clean
回到主分支上,意料之中,并沒有出現剛剛修改的內容(新增了branch.md文件)。
$ git checkout dev
Switched to branch 'dev'
$ ls
README.md branch.md ignore2.md test2.md
$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
$ ls
README.md ignore2.md test2.md
此時,需要將dev分支上合并到master分支上,就能將修改從dev分支更新到master分支上了。
git merge dev
將dev分支合并到master分支。結果如下:
$ git merge dev
Updating 6bf1a5d..82016a7
Fast-forward
branch.md | 1 +
1 file changed, 1 insertion(+)
create mode 100644 branch.md
$ ls
README.md branch.md ignore2.md test2.md
5.5. 合并分支(分支管理篇)
使用 git merge dev
將dev分支合并到master分支時,會將dev中的提交都變為master中的提交。即將兩個分支融合在一起,dev中的提交將不在顯示。這種合并方式稱為:Fast-forward
。
但在實際使用中,我們希望保留dev的提交信息,這樣能清晰地了解項目每一步的進行。
$ git merge --no-ff -m "xxx" dev
該命令的作用是不采用Fast-forward
模式,保留dev分支中每次提交的信息。合并到master分支時,建立一個新的提交,即將dev中的新內容提交到master,而不是粗暴地融合。因此,該命令相當于一個提交,需要附加提交信息-m "xxx"
。該命令是項目中的合并常用命令。
--no-ff
與 Fast forward
區別:
未合并時:
A---B---C dev
/
D------------ master
git merge dev
合并:
D-A---B---C master
git merge --no-ff -m "xxx" dev
合并:
A---B---C dev
/ \
D-----------E master
5.6. 刪除分支
git branch -d dev
刪除dev分支
如果分支未合并,則需要強制刪除 git branch -D dev
5.7. 解決分支合并沖突
在dev分支上修改branch.md文件并提交。然后,切換到master分支上修改branch.md文件同樣位置,但修改內容不同,接著在master上也進行提交。
此時,在master分支上合并dev分支。顯然,兩個分支上都對同一文件同一位置做了不同修改,這樣的合并必然會產生沖突的。合并結果如下:
$ git merge dev
Auto-merging branch.md
CONFLICT (content): Merge conflict in branch.md
Automatic merge failed; fix conflicts and then commit the result.
查看branch.md文件,文件中內容如下:
<<<<<<< HEAD
in master branch:
change in master
=======
in dev branch:
add this change
>>>>>>> dev
<<<<<<< HEAD
與 =======
之間的是master分支中的內容。
=======
與 >>>>>>> dev
之間是dev分支中的內容。
對于兩部分沖突內容,我們需要 手動解決沖突 。選擇需要的留下,而將剩下的刪除即可。此處,我們選擇master中的內容。解決后文件內容如下:
in master branch:
change in master
此時再查看git狀態:
$ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.
(use "git push" to publish your local commits)
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: branch.md
no changes added to commit (use "git add" and/or "git commit -a")
按照git的建議,再 git commit -a
提交即可。
注:
1)雖然在master分支中合并了沖突,但是,在dev分支中的內容并沒有被修改。不過,合并之后,dev分支已經不重要了。
2)在沖突未解決時,很多git操作都不能使用,比如:撤銷。
5.8. 分支管理策略
在實際開發中,我們應該按照幾個基本原則進行分支管理:
首先,master分支應該是非常穩定的,也就是僅用來發布新版本,平時不能在上面干活;
那在哪干活呢?dev分支。也就是說,dev分支是不穩定的,到某個時候,比如1.0版本發布時,再把dev分支合并到master上,在master分支發布1.0版本;
你和你的小伙伴們每個人都在dev分支上干活,每個人都有自己的分支,時不時地往dev分支上合并就可以了。
軟件開發中,bug就像家常便飯一樣。有了bug就需要修復,在Git中,由于分支是如此的強大,所以,每個bug都可以通過一個新的臨時分支來修復,修復后,合并分支,然后將臨時分支刪除。
而合并時,注意要采用--no-ff
模式。
6. git遠程倉庫
6.1. 克隆到本地
git clone https://github.com/xxx/xxx
將github上xxx遠程倉庫的代碼克隆到本地,形成本地倉庫。
假如,遠程有兩個分支master和dev,本地只有一個master分支,克隆或者拉取后并不會在本地創建dev分支。
我們需要用git checkout -b dev origin/dev
命令,將遠程dev分支創建到本地。
$ git clone https://github.com/99MyCql/TestOfGit.git
Cloning into 'TestOfGit'...
remote: Enumerating objects: 53, done.
remote: Counting objects: 100% (53/53), done.
remote: Compressing objects: 100% (28/28), done.
remote: Total 84 (delta 20), reused 50 (delta 17), pack-reused 31
Unpacking objects: 100% (84/84), done.
$ cd TestOfGit/
$ ls
branch.md ignore2.md README.md test2.md
$ git branch -a
* master
remotes/origin/HEAD -> origin/master
remotes/origin/dev
remotes/origin/master
$ git checkout -b dev origin/dev
Branch dev set up to track remote branch dev from origin.
Switched to a new branch 'dev'
6.2. 查看遠程倉庫信息
git remote
查看遠程倉庫信息。
git remote -v
查看詳細信息。
$ git remote
origin
$ git remote -v
origin https://github.com/99MyCql/TestOfGit.git (fetch)
origin https://github.com/99MyCql/TestOfGit.git (push)
-v
顯示了抓取和推送的地址,如果不可推送則沒有第二個地址。
git branch -a
查看遠程和本地所有的分支信息。
$ git branch -a
* dev
master
remotes/origin/HEAD -> origin/master
remotes/origin/dev
remotes/origin/master
6.3. 添加遠程倉庫
git remote add <name> <url>
,添加一個遠程倉庫,shortname
為自定義的遠程主機名,url
為遠程倉庫的url
6.4. 刪除遠程倉庫
git remote remove <name>
6.5. 本地與遠程分支追蹤關系
在某些場合,Git會自動在本地分支與遠程分支之間,建立一種追蹤關系(tracking)。比如,在git clone的時候,所有本地分支默認與遠程主機的同名分支,建立追蹤關系,也就是說,本地的master分支自動"追蹤"origin/master分支。Git也允許手動建立追蹤關系。
$ git branch --set-upstream-to origin/dev dev
該命令指定dev分支追蹤origin/dev分支,即將本地dev分支與遠程dev分支建立追蹤關系。
當沒有建立追蹤關系時,如果在dev分支進行遠程操作又沒有指定遠程分支時,則會出錯:
$ git branch
* dev
master
$ git push
fatal: The current branch dev has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin dev
因此,得先建立本地分支與遠程分支的追蹤關系:
$ git branch --set-upstream-to origin/dev dev
Branch 'dev' set up to track remote branch 'dev' from 'origin'.
$ git push
Username for 'https://github.com': 99MyCql
Password for 'https://99MyCql@github.com':
6.6. 拉取
$ git pull <遠程主機名> <遠程分支名>:<本地分支名>
取回遠程主機某個分支的更新,再與本地的指定分支合并。
如:
$ git pull origin next:master
取回origin主機的next分支,與本地的master分支合并
$ git pull origin next
如果遠程分支是與當前分支合并,則冒號后面的部分可以省略。
$ git pull origin
本地的當前分支自動與對應的origin主機"追蹤分支"進行合并。
$ git pull
該命令表示,當前分支自動與唯一追蹤分支進行合并。
如果遠程主機刪除了某個分支,默認情況下,git pull 不會在拉取遠程分支的時候,刪除對應的本地分支。這是為了防止,由于其他人操作了遠程主機,導致git pull不知不覺刪除了本地分支。
但是,你可以改變這個行為,加上參數 -p 就會在本地刪除遠程已經刪除的分支。$ git pull -p
6.7. 推送
$ git push <遠程主機名> <本地分支名>:<遠程分支名>
注意,分支推送順序的寫法是<來源地>:<目的地>,所以git pull是<遠程分支>:<本地分支>,而git push是<本地分支>:<遠程分支>。
$ git push origin master
該命令表示,將本地的master分支推送到origin主機的master分支。如果后者不存在,則會被新建。
$ git push origin :master
如果省略本地分支名,則表示刪除指定的遠程分支,因為這等同于推送一個空的本地分支到遠程分支。
等同于:
$ git push origin --delete master
刪除origin主機的master分支。
$ git push origin
將當前分支推送到origin主機的對應存在追蹤關系的分支。
如果當前分支只有一個追蹤分支,那么主機名都可以省略
$ git push
6.6. 解決沖突
當多人協作時,經常會出現沖突。
比如,A和B同時拉取了遠程最新版到本地。A修改了項目的origin.md文件,同時提交并推送到了遠程。B也修改了該文件,但當B要推送時,便出現沖突報錯了:
$ git push
Username for 'https://github.com': 99MyCql
Password for 'https://99MyCql@github.com':
To https://github.com/99MyCql/TestOfGit.git
! [rejected] dev -> dev (fetch first)
error: failed to push some refs to 'https://github.com/99MyCql/TestOfGit.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
顯然,遠程版本庫(A推送后更新了遠程版本庫)和本地版本庫不相同,必然會出現沖突。
此時,我們需按照提示,先拉取最新的內容。
$ git pull
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://github.com/99MyCql/TestOfGit
f7a736f..69d4b68 dev -> origin/dev
Auto-merging origin.md
CONFLICT (add/add): Merge conflict in origin.md
Automatic merge failed; fix conflicts and then commit the result.
然后,跟解決合并分支沖突一樣,進行手動解決。再將解決后內容提交并推送即可。
$ vim origin.md
$ git status
On branch dev
Your branch and 'origin/dev' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" to merge the remote branch into yours)
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both added: origin.md
no changes added to commit (use "git add" and/or "git commit -a")
$ git commit -a -m"solve the conflict"
[dev 6cc66a2] solve the conflict
$ git push
Username for 'https://github.com': 99MyCql
Password for 'https://99MyCql@github.com':
Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (6/6), 567 bytes | 51.00 KiB/s, done.
Total 6 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 1 local object.
To https://github.com/99MyCql/TestOfGit.git
69d4b68..6cc66a2 dev -> dev