可以在GitHub下載離線版的筆記,鏈接如下:https://github.com/FangYang970206/GitNote
1. Git和Github
許多人剛開始學習git和github的時候,分不清git和github的關系,以為它們是一樣的,我也犯過這個錯誤。那么git和github分別是什么呢?它們為什么總在一起稱呼?簡單來說,git是一套管理軟件開發版本的分布式控制系統,而GitHub是用來托管代碼的網站。用戶可以通過Git將軟件代碼上傳到Github上,全世界的程序猿可以通過GitHub查看代碼,使用Git共同開發不同版本的軟件。
版本控制主要經歷了三個時期,分別是本地版本控制,集中版本控制以及現在的分布式版本控制。下面分別來說說。
1.1 本地版本控制
本地版本控制顧名思義,就在自己的電腦上進行版本控制,通過復制項目的整個目錄以及給項目標注更改項和更改時間,在電腦本地存儲所有的版本。具體框圖如下:
1.2 集中式版本控制
集中式版本控制則是將軟件代碼存在中央服務器中,開發者可以通過遠程下載最新代碼或者提交更新,多人協作更加方便。具體框圖如下:
1.3 分布式版本控制
分布式相比於集中式,就算服務器中的所有數據全部丟失,也可以通過本地的倉庫進行恢復,因為每一次克隆,本地中都包含了軟件的所有版本以及提交歷史。具體框圖如下:
2. Git安裝
由於筆者沒有接觸過mac,所以只寫window和linux下的安裝方法。linux(基於Dedian)下:
$ sudo apt-get install git
window下:
點擊鏈接下載git,默認運行安裝即可,另外提一句,git里可以指定默認編輯器,個人比較喜歡VScode,所以比較推薦VScode啦!
3. 創建GitHub賬號
進入[官網][5]后,點擊sign up進行注冊即可,會有郵箱認證填寫經歷什么的,這個很簡單,不多說了。注冊完了可以點擊sign in進行登陸,瀏覽器記住賬號即可。 ![image_1cl8mrdtv1djr61n18ul1s4svk76m.png-92.8kB][6]4. 第一個倉庫
倉庫的英文是repository,一般簡說repo,代碼存儲在repo中,安裝完git和創建完github賬號,我們就可以創建我們自己的第一個repo啦!4.1 創建倉庫
進入自己的github主頁,點擊自己圖像旁邊的加號,然后點擊New repository,如圖所示 ![image_1cl2uuggo11381m5r1b9k9j614rr9.png-17.4kB][7]然后在填寫相關倉庫的一些信息,然后點擊creat repository
4.2 ssh key生成
git的遠程管理是基於SSH的,所以需要進行SSH的配置,這樣你才能訪問自己的倉庫。 首先,在bash(window桌面右鍵有git bash,linux則直接終端進行即可)中設置Git的`User name`和`email`(注冊名字和郵箱): ```bash $ git config --global user.name "FangYang970206" $ git config --global user.email "15270989505@163.com" ``` 然后,我們可以看看自己電腦里有沒有ssh密鑰,linux下是在`/home/.ssh`,window是在`C:\Users\Username\.ssh`,有則備份刪除,然后在終端中運行 ```bash ssh-keygen -t rsa -C "15270989505@163.com" ``` 按3個回車,密碼為空,得到了兩個文件:`id_rsa`和`id_rsa.pub`,然后打開id_rsa.pub,復制里面的內容,最后面的計算機名字不要復制,然后打開[https://github.com][9],點擊自己頭像中的`Setting`,然后選擇`SSH and GPG keys`,點擊`New SSH key`,title隨便寫,下面的key粘貼剛才復制的內容,最后點擊`Add SSH key`,成功`SSH and GPG keys`就會有SSH key的顯示,如筆者界面所示(window和ubuntu各一個) ![2.png-1767.2kB][10]4.3 git簡單工作流
點擊我們剛剛創建的倉庫,可以看到如下界面 ![3.png-2946.1kB][11] 首先我們在桌面創建一個文件夾,名字也取playground好了,我們可以安裝上圖的一個小教程來初步試試(倉庫地址不同,請使用自己的地址哦) ```bash echo "# playground" >> README.md git init git add README.md git commit -m "first commit" git remote add origin git@github.com:FangYang970206/playground.git git push -u origin master ``` 運行完上面的命令,我們重新打開我們剛才創建的倉庫,你就會發現已經有所變化,我們已經把README.md文件上傳到了我們的倉庫中。5. 使用git管理倉庫
通過上面的一個小事例,沒接觸過git可能有比較多的疑問,下面我們來一步步進行講解。5.1 上節回顧
> * 第一句`echo "# playground" >> README.md`不用多說,就是將使用echo命令將`# playground`寫入`README.md`中 > * 第二句`git init`是初始化本地倉庫,會在當前目錄產生`.git`文件夾,這是保存着所有git操作所需要的文件,是本地進行git的第一步(遠程克隆倉庫不需要這一步) > * 第三句`git add README.md`是將文件放入暫存區(stage),暫存區后面再說。 > * 第四句`git commit -m "first commit"`是記錄這次的更改,`-m `后的字符串則是更改詳情,在你的github倉庫中,你也會看到`README.md`后面跟着`"first commit"`這句話。 >* 第五句`git remote add origin git@github.com:FangYang970206/playground.git`是用來添加遠程倉庫的信息到本地,並用一個簡短的引用來表示url,命令具體是`git remote add5.2 文件跟蹤
倉庫中的文件狀態無非兩種,一種是未被git跟蹤(untracked),另一種是被git跟蹤(tracked),對於從遠程服務器中克隆出的倉庫,默認全部文件都進行進行跟蹤,而本地自己新建的倉庫,則需要通過`git add`命令將未被git跟蹤的文件變為被git跟蹤的文件。而被git跟蹤的文件有三種狀態,分別是未修改(unmodified)、未修改(modified)和暫存區(staged)。以5.1節的為例,我們先新建了README.md文件,這個文件處於未跟蹤狀態,然后初始化倉庫后,我們通過`git add`命令將未跟蹤狀態的文件轉到跟蹤狀態,並將文件加入到暫存區。然后通過`git commit`命令將暫存區狀態轉成未修改文件。下圖形象地表示了文件地狀態轉換。 ![image_1cl92fg2q1cdfvc1p2g7761c6q7t.png-57.8kB][12] 當你將文件轉成跟蹤狀態時,文件如果沒有人為移除,將一直處於跟蹤狀態,從未修改、已修改和暫存區三個狀態反復轉換,永不丟失。5.3 git常用命令
5.3.0 git add和 git commit
上面的`git add`和`git commit`是最重要的兩個命令,它是整個版本控制中最常用的兩個命令,本地版本控制流程如下: ![6.png-353.7kB][13] 下面介紹一些`git add `常用命令 ``` git add5.3.1 git status
知道文件的狀態,我們通過一些實例來學習一下。在playground倉庫新建`hello.py`,然后加入下面一行: ```python print("hello everyone") ``` `git status`命令會顯示當前倉庫的文件狀態,在終端中輸入`git status`,顯示如下內容: ```bash $ git status On branch master Your branch is up to date with 'origin/master'.Untracked files:
(use "git add
hello.py
nothing added to commit but untracked files present (use "git add" to track)
可以看到我們剛剛新建的`hello.py`處於untracked files。然后我們在終端中輸入`git add hello.py`,然后我們再次輸入`git status`,會出現下面內容:
```bash
$ git add hello.py
warning: LF will be replaced by CRLF in hello.py.
The file will have its original line endings in your working directory.
$ 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)
new file: hello.py
可以看到hello.py
的狀態變為Changes to be committed
,這意味着hello.py
進入了暫存區,我們再用git commit
提交這次更改,然后再用git status
查看狀態,結果如下:
$ git commit -m "add hello everyone"
[master 405cd1b] add hello everyone
1 file changed, 1 insertion(+)
create mode 100644 hello.py
$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
nothing to commit, working tree clean
可以看到顯示nothing to commit
,文件狀態處於未更改狀態,因為我們完成了這一次版本的提交。最后我們可以使用git push
上傳我們這次的提交到遠程github服務器上。最后可以看到github的倉庫中多了我們剛才修改的hello.py
.
5.3.2 git diff
`git status`命令的輸出可能過於模糊,如果你想知道具體修改了什么地方,可以用`git diff` 命令。它用來回答兩個問題:當前做的哪些更新還沒有暫存? 有哪些更新已經暫存起來准備好了下次提交?代碼修改運行出錯,有時候可以用`git diff`,可以看到自己或他人新加入了那些行,有助於修復bug和多人合作。 我們在`hello.py`文件再加入一行: ```python print("hello fang") ``` 然后在終端中輸入`git diff`,結果如下: ```bash $ git diff warning: LF will be replaced by CRLF in hello.py. The file will have its original line endings in your working directory. diff --git a/hello.py b/hello.py index 1ad4063..d469b07 100644 --- a/hello.py +++ b/hello.py @@ -1 +1,2 @@ print("hello everyone") +print("hello yang") ``` `+`號代表新添加的行,然后我們再加入一行: ```python print("hello fang") ``` 然后再調用`git diff`命令,結果如下: ```bash $ git diff diff --git a/hello.py b/hello.py index 97d6fb2..2595b9c 100644 --- a/hello.py +++ b/hello.py @@ -1 +1,3 @@ print("hello everyone") +print("hello yang") +print("hello fang") ``` 可以看到我們又多加了一行,我們將文件狀態轉到暫存區(使用`git add hello.py`)看看,然后運行`git diff`,可以看到是沒有任何輸出的,因為`git diff` 本身只顯示尚未暫存的改動。如果想看暫存前后的變化,則需要使用`git diff --cached`命令,則可以看到: ```bash $ git diff --cached diff --git a/hello.py b/hello.py index 97d6fb2..2595b9c 100644 --- a/hello.py +++ b/hello.py @@ -1 +1,3 @@ print("hello everyone") +print("hello yang") +print("hello fang") ``` 這里提一句,有時候我們運行`git status`,我們會看到一個文件顯示兩種狀態,比如我們上面加了一行`print("hello FY")`,然后運行`git status`,可以看到如下結果: ```bash $ git status On branch master Your branch is ahead of 'origin/master' by 1 commit. (use "git push" to publish your local commits)Changes to be committed:
(use "git reset HEAD
modified: hello.py
Changes not staged for commit:
(use "git add
(use "git checkout --
modified: hello.py
出現這種結果的原因是因為我們的文件確實存在兩種狀態,一種是我們之前加入到暫存區,一種是我們剛剛修改添加的,兩者是可以共存的。
<h4>
<a id="E33">
5.3.3 git log
</a>
</h4>
`git log`命令會列出每個提交的SHA-1 校驗和、作者的名字和電子郵件地址、提交時間以及提交說明。我們已經提交了許多次了,在bash中輸入`git log`,出現以下內容:
```bash
$ git log
commit a9b674c88eba0043f84aec4668215358f99c5572 (HEAD -> master)
Author: FangYang970206 <15270989505@163.com>
Date: Mon Aug 20 16:04:09 2018 +0800
add some info
commit 2ae267c61261b6041d16133cca56f4c8155d73fa
Author: FangYang970206 <15270989505@163.com>
Date: Mon Aug 20 10:02:57 2018 +0800
fix one error
commit 405cd1bd4b9e0d19f1698c7f7cb8f77184424040 (origin/master, origin/HEAD)
Author: FangYang970206 <15270989505@163.com>
Date: Sun Aug 19 21:49:29 2018 +0800
add hello everyone
commit edb1d60a14b25be099205a62a7c469083dc1338a
Author: FangYang970206 <15270989505@163.com>
Date: Fri Aug 17 17:41:25 2018 +0800
first commit
SHA-1校驗和(也叫hash,哈希)這里提一句,在git中,文件的存儲是通過文件內容計算哈希值(40位十六進制)進行索引的,所以不可能在git不知情的情況下修改文件,確保記錄完備,提交更改會產生哈希值記錄此次更改。
git log
其他常用命令:
git log -n #n是整數,返回最近n次提交歷史
git log -p -n #用來顯示每次提交的內容差異, -n則返回最近n次提交的差異
git log --stat #列出所有被修改過的文件、有多少文件被修改了以及被修改過的文件的哪些行被移除或是添加了。
git log --pretty=format #按照指定格式展示歷史
git log --oneline --decorate #顯示分支的指向情況,見分支的使用
git log --oneline --decorate --graph --all #輸出提交歷史、各個分支的指向以及項目的分支分叉情況。
個人覺得git log --pretty=format
非常有趣,可以很簡潔展示歷史,舉例來說,我們在終端運行git log --pretty=format:"%h %s"
,有如下結果:
$ git log --pretty=format:"%h %s"
a9b674c add some info
2ae267c fix one error
405cd1b add hello everyone
edb1d60 first commit
第一項是簡短哈希,第二項是提交說明,非常直觀。常見的format如下。
5.3.4 git rm和git mv
`git rm`命令用來刪除文件,對文件不進行版本管理。`git mv`命令可以對文件重命名和移動。 使用`git rm`: ```bash $ git rm README.md rm 'README.md' $ git status On branch master Your branch is ahead of 'origin/master' by 2 commits. (use "git push" to publish your local commits)Changes to be committed:
(use "git reset HEAD
deleted: README.md
$ git commit -m "delete README.md"
[master b976c0a] delete README.md
1 file changed, 1 deletion(-)
delete mode 100644 README.md
通過`git rm`后,可以在playground文件夾中看到README.md文件已經不見了。
常用的`git rm`命令有:
```bash
git rm -r <dirname> #遞歸刪除文件夾中的文件
git rm \*.c #刪除所有后綴.c文件
git rm -f filename #強制刪除,針對修改並放到暫存區的文件
git rm --cached README #不刪除文件,仍然放在目錄中,但不進行跟蹤
使用git mv
(先在playground文件夾新建dd文件夾):
$ git mv hello.py dd/hello.py
$ git status
On branch master
Your branch is ahead of 'origin/master' by 3 commits.
(use "git push" to publish your local commits)
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: hello.py -> dd/hello.py
$ git mv dd/hello.py dd/hello.txt
$ git status
On branch master
Your branch is ahead of 'origin/master' by 3 commits.
(use "git push" to publish your local commits)
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: hello.py -> dd/hello.txt
$ git commit -m "rename hello.py -> hello.txt"
git mv
相當於以下三條命令:
$ mv README.md README
$ git rm README.md
$ git add README
當然,能有簡潔的命令當然用簡潔的好!
5.3.5 遠程命令
這一節涉及到分支,建議跳過這一節,看完分支再過來。 `git clone`是用來克隆遠程倉庫到本地; `git remote`是用來添加遠程倉庫,並為遠程倉庫添加名字縮寫; `git push`是上傳本地倉庫到遠程倉庫中。 `git fetch`是將遠程倉庫的更新下載到本地倉庫中 `git pull`是將遠程倉庫的更新下載到本地倉庫中,並進行合並5.3.5.1 git clone
克隆可以通過https url下載或者ssh url,命令很簡單 ```bash $ git clone url ```5.3.5.2 git remote
`git remote add origin git@github.com:FangYang970206/playground.git`是用來添加遠程倉庫的信息到本地,並用一個簡短的引用來表示url,命令具體是`git remote add5.3.5.3 git push
`git push -u origin master`是將本地代碼上傳到github服務器,這句話要拆成兩部分解釋,第一部分是`git push`,是上傳命令,那上傳到那里呢?第二部分`-u origin master`則是指定上傳位置,上傳到`origin`的`master`分支,這里的`-u`是設定默認主機,也就是下次你要是也上傳到`origin`的`master`分支,就直接`git push`就可以了。 其他命令: ```bash $ git push origin5.3.5.4 git fetch
`git fetch`命令默認是將github中獲取最新的版本到本地分支,默認是獲取最新的origin/master,然后比較本地的master分支和origin/master分支的差別,進行差異合並。更一般的命令:
$ git fetch origin <remote branch>:<local branch> #分支
5.3.5.5 git pull
`git pull`命令是兩個命令的合並 ```bash $ git pull origin5.3.6 git reset和git revert
`git reset`和`git revert`命令是用來撤銷變更的,常用命令如下(關於HEAD的解釋,請看分支一節): ```bash $ git reset #取消所有暫存文件 $ git reset HEAD5.3.7 git別名
有一個小技巧可以使你的 Git 體驗更簡單、容易、熟悉:別名。可以通過 git config文件來輕松地為每一個命令設置一個別名。以下是一些實例。 ```bash $ git config --global alias.co checkout $ git config --global alias.br branch $ git config --global alias.ci commit $ git config --global alias.st status ``` 這樣,就可以用`git co`代表`git commit`,`git br`代表`git branch`等等。取消別名使可使用如下命令: ```bash $ git config --global alias.unstage 'reset HEAD --' ```5.3.8 git tag
Git 可以給歷史中的某一個提交打上標簽,以示重要。比較有代表性的是人們會使用這個功能來標記發布結點。經常可以看某些軟件庫經常發x.x.x版本。 ```bash $ git log --pretty=format:"%h %s" b976c0a delete README.md b4d7987 add some info 2ae267c fix one error 405cd1b add hello everyone edb1d60 first commit$ git tag v1 b976c0a
$ git push origin v1
通過以上命令,就可以在遠程倉庫tag下有v1版本,如圖所示(ps:我多tag了一個v2):
![image_1cld851m736l1ugok25183j1fvcaq.png-21.5kB][15]
<h4>
<a id="E39">
5.3.9 .gitignore文件
</a>
</h4>
一般我們總會有些文件無需納入Git的管理,也不希望它們總出現在未跟蹤文件列表。通常都是些自動生成的文件,比如日志文件,或者編譯過程中創建的臨時文件等。在這種情況下,我們可以創建一個名為.gitignore的文件,列出要忽略的文件模式。
文件.gitignore 的格式規范如下:
• 所有空行或者以 # 開頭的行都會被 Git 忽略。
• 可以使用標准的 glob 模式匹配。
• 匹配模式可以以(/)開頭防止遞歸。
• 匹配模式可以以(/)結尾指定目錄。
• 要忽略指定模式以外的文件或目錄,可以在模式前加上驚嘆號(!)取反。
所謂的 glob 模式是指 shell 所使用的簡化了的正則表達式。 星號(\*)匹配零個或多個任意字符;[abc] 匹配任何一個列在方括號中的字符(這個例子要么匹配一個a,要么匹配一個b,要么匹配一個c);問號(?)只匹配一個任意字符;如果在方括號中使用短划線分隔兩個字符,表示所有在這兩個字符范圍內的都可以匹配(比如 [0-9] 表示匹配所有 0 到 9 的數字)。使用兩個星號(*) 表示匹配任意中間目錄,比如a/\*\*/z可以匹配a/z, a/b/z 或 a/b/c/z等。
.gitignore 文件的例子:
```bash
# no .a files
*.a
# but do track lib.a, even though you're ignoring .a files above
!lib.a
# only ignore the TODO file in the current directory, not subdir/TODO
/TODO
# ignore all files in the build/ directory
build/
# ignore doc/notes.txt, but not doc/server/arch.txt
doc/*.txt
# ignore all .pdf files in the doc/ directory
doc/**/*.pdf
TIP: GitHub 有一個十分詳細的針對數十種項目及語言的 .gitignore 文件列表,你可以在https://github.com/github/gitignore 找到它.
5.3.10 LICENSE文件
這一小節是最后加上的,可能會與后面的章節的LICENSE沖突,這點注意一下就好。LICENSE文件是一種開源許可證,即授權條款。開源軟件並非完全沒有限制。最基本的限制,就是開源軟件強迫任何使用和修改該軟件的人承認發起人的著作權和所有參與人的貢獻。任何人擁有可以自由復制、修改、使用這些源代碼的權利,不得設置針對任何人或團體領域的限制;不得限制開源軟件的商業使用等。而許可證就是這樣一個保證這些限制的法律文件。
開源許可證有上百種,這里說說最流行的六種:GPL、BSD、MIT、Mozilla、Apache和LGPL如何做選擇,阮一峰在如何選擇開源許可證一文中給出了一張圖,直觀精確,就是下圖:
那么如何給自己的倉庫加上LICENSE呢?很簡單,點擊倉庫中的creat new file,然后寫LICENSE.md,選擇choose a license template
然后選擇MIT License,再點擊Review and Submit,最后點擊commit change就可以了。
我們就可以在自己倉庫中看到MIT協議了
6. 分支的使用
有人稱分支是git的必殺技,正是因為這一特性,git從眾多版本管理系統中脫穎而出。git鼓勵多次使用分支和合並。精通分支,將對你的版本管理十分便捷和高效。進行分支之前,先講git是如何保存數據的。 >Note: 為了節省工作量,分支中有的圖是截的書上的圖,圖中的校驗和會與實際的校驗和不同,這點注意一下就好。6.1 git保存數據方式與分支簡介
與一些版本控制軟件不同,Git保存的不是文件的變化或者差異,而是一系列不同時刻的文件快照。如下圖所示 ![8.png-1754.4kB][20] 在進行提交操作時,Git 會保存一個提交對象(commit object)。知道了 Git 保存數據的方式,我們可以很自然的想到,該提交對象會包含一個指向暫存內容快照的指針。但不僅僅是這樣,該提交對象還包含了作者的姓名和郵箱、提交時輸入的信息以及指向它的父對象的指針。首次提交產生的提交對象沒有父對象,普通提交操作產生的提交對象有一個父對象,而由多個分支合並產生的提交對象有多個父對象。
為了更加形象地說明,我們假設現在有一個工作目錄,里面包含了三個將要被暫存和提交的文件(文件需要自己動手在playground目錄新建)。暫存操作會為每一個文件計算校驗和,然后會把當前版本的文件快照保存到Git倉庫中(Git使用blob對象來保存它們),最終將校驗和加入到暫存區域等待提交:
$ git add README test.rb LICENSE
$ git commit -m 'branch note begin'
現在,Git倉庫中有五個對象(忽略dd文件夾):三個blob對象(保存着文件快照)、一個樹對象(記錄着目錄結構和blob對象索引)以及一個提交對象(包含着指向前述樹對象的指針和所有提交信息)。具體結構如下圖:
做些修改后再次提交(兩次):
$ echo "print("1")" >> README
$ git commit -am "add print("1") into README"
warning: LF will be replaced by CRLF in README.
The file will have its original line endings in your working directory.
[master 326dd0b] add print(1) into README
1 file changed, 1 insertion(+)
$ echo "print("2")" >> README
$ git commit -am "add print("2") into README"
warning: LF will be replaced by CRLF in README.
The file will have its original line endings in your working directory.
[master ebc9b45] add print(2) into README
1 file changed, 1 insertion(+)
兩次產生的提交對象會包含一個指向上次提交對象(父對象)的指針。見下圖:
Git的分支,其實本質上僅僅是指向提交對象的可變指針。Git的默認分支名字是master。在多次提交操作之后,你其實已經有一個指向最后那個提交對象的master分支。它會在每次的提交操作中自動向前移動。
- Git的“master”分支並不是一個特殊分支。它就跟其它分支完全沒有區別。之所以幾乎每一個倉庫都有 master 分支,是因為git init命令默認創建它,並且大多數人都懶得去改動它。
6.2 創建分支
使用`git branch`命令可以很簡單地創建分支,比如創建一個testing分支。 ```bash $ git branch testing ``` 這會在當前所在的提交對象上創建一個指針。如下圖所示 ![13.png-266.7kB][23] 那么,Git又是怎么知道當前在哪一個分支上呢?很簡單,它有一個名為HEAD的特殊指針。這個特殊指針就是用來指定當前所在的分支。 ![image_1clfp6pp11e0v8l71g391cok17s76k.png-8.4kB][24] 可以使用`git log`命令查看各個分支當前所指的對象。 ```bash $ git log --oneline --decorate -3 #只看倒數3個 ebc9b45 (HEAD -> master, testing) add print(2) into README 326dd0b add print(1) into README b861d60 branch note begin ``` 可以看到testing,master和現在指向master的HEAD都指向最后的提交。6.3 切換分支
創建好了分支,可以用`git checkout`命令進行切換 ```bash $ git checkout testing Switched to branch 'testing' $ git branch #可以使用git branch查看當前分支(前面帶*號) master * testing ``` 有一個簡單的命令可以快速創建新的分支並切換到新的分支: ```bash $ git checkout -b new_branch #等效於 $ git branch new_branch $ git checkout new_branch ``` 於是HEAD指針也發生了變化,如下圖 ![image_1clfp9vofjk9t6hrb81ma0fg771.png-8.1kB][25] 在testing分支進行一次提交: ```bash $ echo "print("3")" >> README$ git commit -am "add print("3") into README"
warning: LF will be replaced by CRLF in README.
The file will have its original line endings in your working directory.
[testing 8b6bbb7] add print(3) into README
1 file changed, 1 insertion(+)
git的提交記錄以及分支指向如下:
![image_1clfpdg7a10ic1l7mbvpd03e3t7e.png-9.6kB][26]
可以看到testing和HEAD都移動到前面了,而master沒有移動。現在我們切換到master分支,對master進行一次提交。
```bash
$ echo "MIT LICENSE" >> LICENSE
$ git commit -am "add MIT LICENSE"
warning: LF will be replaced by CRLF in LICENSE.
The file will have its original line endings in your working directory.
[master ce53a90] add MIT LICENSE
1 file changed, 1 insertion(+)
提交后,上圖就變成了下圖
可以使用git log
命令查看提交歷史
$ git log --oneline --decorate --graph --all -5 #只看倒數五個
* ce53a90 (HEAD -> master) add MIT LICENSE
| * 8b6bbb7 (testing) add print(3) into README
|/
* ebc9b45 add print(2) into README
* 326dd0b add print(1) into README
* b861d60 branch note begin
可以看到圖中的看到結構與上圖相同。
由於Git的分支實質上僅是包含所指對象校驗和(長度為40的SHA-1值字符串)的文件,所以它的創建和銷毀都異常高效,也就是添加或者刪除41個字節的速度,能做到這一點的原因是因為git是以文件快照的形式保存文件,所以創建分支只需創建一個新的指針指向快照即可,而其他的一些版本管理軟件往往需要將整個項目復制到另一個目錄,這就比較慢了。
6.4 為什么要使用分支?
學了上面的東西,可能會想,為什么要使用分支?可以通過現實場景的問題來回答這個問題。從個人角度,你正在開發一個網站,網站已經處於正常運行狀態,我們想在網站中加入新的功能,這時候你有兩種選擇:一是直接在當前master上進行修改和測試,二是創建一個分支。選擇一可能會產生影響正常工作的代碼,這是我們不想看到的。而選擇二創建分支可以很方便地解決這個問題,分支不會影響當前工作的分支,可以很放心地進行開發和測試,最后對原工作分支進行合並即可。
從多人協作角度上看,這個更加直接,在一條流水線上不僅效率低,而且會產生很多混亂,比如不同人代碼水平有限,有的還會編寫錯誤的代碼,使用分支可以讓軟件維護者很方便地查看不同的分支情況,選擇合適地分支進行合並。
另外,分支的創建是非常快的,只需創建一個新的指針即可,切換分支也非常地塊,這可以讓我們很靈活而不受干擾地工作。
6.5 合並分支
分支整合可以通過兩種命令:一種是基於`git merge`命令,另一種是基於`git rebase`命令。6.5.1 git merge——三方合並
`git merge`是一種保存分支結構的合並,並且是**三方合並**,通過實例來看吧。 ```bash $ git merge testing #會跳出commit記錄文件,默認退出即可 Merge made by the 'recursive' strategy. README | 1 + 1 file changed, 1 insertion(+)$ git log --oneline --decorate --graph --all -6
- f23a940 (HEAD -> master) Merge branch 'testing'
|
| * 8b6bbb7 (testing) add print(3) into README - | ce53a90 add MIT LICENSE
|/ - ebc9b45 add print(2) into README
- 326dd0b add print(1) into README
- b861d60 branch note begin
上面可以很直觀地看出提交歷史,用圖形表示如下:
![image_1clfpn074131g1o1tt9l63nbb188.png-16.1kB][28]
既然叫三方合並,是那三方呢?見下圖
![image_1clfq8dr31dgt12fjc7hmfgkkm8l.png-16.2kB][29]
上圖中淺藍色方塊就是三方,分別是當前分支,要合並的分支,以及這兩者的共同祖先(這個由git自己決定),merge合並會根據當前分支與祖先的差異和要合並的分支與祖先的差異進行共同合並。
<h4>
<a id="F52">
6.5.2 git rebase——變基
</a>
</h4>
合並還有一種方法:那就是提取某一分支(8b6bbb7)中引入的補丁和修改,然后在另一分支(ce53a90)的基礎上應用一次。在Git中,這種操作就叫做**變基**。可以使用 rebase命令將提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一樣。
```bash
$ git reset --hard ce53a90 #首先通過reset回溯到合並前的狀態
HEAD is now at ce53a90 add MIT LICENSE
$ git log --oneline --decorate --graph --all -5
* ce53a90 (HEAD -> master) add MIT LICENSE
| * 8b6bbb7 (testing) add print(3) into README
|/
* ebc9b45 add print(2) into README
* 326dd0b add print(1) into README
* b861d60 branch note begin
$ git checkout testing #切換到要進行合並的分支
$ git rebase master #使用rebase命令將testing合並到master
First, rewinding head to replay your work on top of it...
Applying: add print(3) into README
$ git log --oneline --decorate --graph --all -5
* 972909a (HEAD -> testing) add print(3) into README
* ce53a90 (master) add MIT LICENSE
* ebc9b45 add print(2) into README
* 326dd0b add print(1) into README
* b861d60 branch note begin
通過git log
命令,整個歷史可以看到沒有想merge那樣的分岔路,而是一條筆直的提交。rebase的原理是首先找到兩個分支(即當前分支 testing、變基操作的目標基底分支master)的最近共同祖先,然后對比當前分支相對於該祖先的歷次提交,提取相應的修改並存為臨時文件,然后將當前分支指向目標基底master,最后以此將之前另存為臨時文件的修改依序應用。之前分支出去的提交就沒有了延續,不會出現在提交歷史中了。可以用新的圖來表示這個過程。
無論是通過三方merge合並,還是通過rebase變基,最后的結果是一樣的,唯一不同的是提交歷史的區別,merge還會保存分支的歷史,而rebase則不會,它的提交歷史是沒有分叉的直線,相對整潔。
6.5.2.1 多重變基
現在用commit id來簡單表示校驗和,你在主分支中的C2上創建了一個特性分支server,為服務端添加了一些功能,提交了C3和C4。然后從C3上創建了特性分支client,為客戶端添加了一些功能,提交了C8和C9。最后,你回到server分支,又提交了C10。(ps:這里沒有進行代碼實踐,有興趣的朋友可以自己試試) ![image_1clgoipfepthue1crleu516mdek.png-23.3kB][31] 現在你希望將client中的修改合並到主分支並發布,但暫時並不想合並 server 中的修改,因為它們還需要經過更全面的測試。這時,你就可以使用git rebase命令的--onto選項,選中在client分支里但不在server分支里的修改(即C8和C9),將它們在master分支上重放: ```bash $ git rebase --onto master server client ``` 以上命令的意思是:“取出client分支,找出處於client分支和server分支的共同祖先之后的修改,然后把它們在 master分支上重放一遍”。效果如下: ![image_1clgorr5q5no1it51qt415nb18hsf1.png-19.7kB][32] 然后進行快速合並, ```bash $ git checkout master $ git merge client ``` 接下來你決定將server分支中的修改也整合進來。使用`git rebase [basebranch] [topicbranch]`命令可以直接將特性分支(即本例中的server)變基到目標分支(即master)上。這樣做能省去你先切換到 server 分支,再對其執行變基命令的多個步驟。 ```bash $ git rebase master server ``` 結果如下: ![image_1clgp3bke7cfdr1q3r1je0rn2fe.png-24kB][33] 最后我們進行快速合並以及刪除server,client分支。 ```bash $ git checkout master $ git merge server $ git branch -d client $ git branch -d client ``` 最終的提交歷史: ![image_1clgpimj51nbgk45d0hof2butfr.png-15.7kB][34]6.5.2.2 變基的風險
奇妙的變基也並非完美無缺,要用它得遵守一條准則:**不要對在你的倉庫外有副本的分支執行變基。**否則,人民群眾會仇恨你,你的朋友和家人也會嘲笑你,唾棄你先簡單說說這個意思,試想,A團隊在本地進行了一次三方合並,然后push到遠程服務器,B團隊發現倉庫有更改,通過git pull
將新的提交拉到本地進行合並。可不久,A團隊想對發到遠程服務器的版本做做變基,把上次的三方合並修改成了變基,再次push到遠程服務器。B團隊發送遠程服務器版本又更新,而且自己上一次pull下來的一些提交不見了(rebase丟棄掉了),只好再次進行合並,但發現沒有,A團隊兩次推送沒有更改最后的提交內容,也就是說B團隊合並了兩次相同的提交(歷史混亂,B團隊尷尬),不僅如此,A團隊是想清理掉一些提交歷史的,但B團隊還保留那些歷史,等B團隊push到遠程服務器時,A團隊看到自己rebase去掉的歷史又出現了(A團隊尷尬)。
這樣說不太容易理解,下面通過圖形進行描述。
- 克隆一個倉庫,然后在它的基礎上進行了一些開發
- 別人提交了一次合並,你抓取別人的提交,合並到自己的開發分支
- 有人推送了經過變基的提交,並丟棄了你的本地開發所基於的一些提交
- 你將相同的內容(C6,C4')合並了兩次
此時如果你執行git log
命令,你會發現有兩個提交的作者、日期、日志居然是一樣的,這會令人感到混亂。此外,如果你將這一堆又推送到服務器上,你實際上是將那些已經被變基拋棄的提交又找了回來,這會令人感到更加混亂。 很明顯對方並不想在提交歷史中看到C4和C6,因為之前就是他把這兩個提交通過變基丟棄的。
解決辦法:用變基解決變基,執行git rebase teamone/master,Git將會
- 檢查哪些提交是我們的分支上獨有的(C2,C3,C4,C6,C7)
- 檢查其中哪些提交不是合並操作的結果(C2,C3,C4)
- 檢查哪些提交在對方覆蓋更新時並沒有被納入目標分支(只有 C2 和 C3,因為 C4 其實就是 C4')
- 把查到的這些提交應用在 teamone/master 上面
當然這個辦法有一個前提,那就是C4'和C4要幾乎一樣,否則變基無法識別。還有一個緩解疼痛的方法,同git pull --rebase
替換git pull
,這個方法不會產生新的提交,也是變基。當然,最好的辦法還是那條准則:不要對在你的倉庫外有副本的分支執行變基!
6.6 合並沖突
當然在合並的過程中,可能會出現合並沖突的。合並沖突時,`git merge`命令會顯示是在哪個文件產生的沖突,我們來通過例子來試一試。 ``` $ git checkout master #接着上面的git rebase Switched to branch 'master' Your branch is ahead of 'origin/master' by 4 commits. (use "git push" to publish your local commits)$ git merge testing
Updating ce53a90..972909a
Fast-forward
README | 1 +
1 file changed, 1 insertion(+)
$ git branch -d testing
Deleted branch testing (was 972909a).
$ git checkout -b newtesting
Switched to a new branch 'newtesting'
$ echo "print("newtesting")" >> README
$ git commit -am "Newtesting commit"
warning: LF will be replaced by CRLF in README.
The file will have its original line endings in your working directory.
[newtesting 1aaf545] Newtesting commit
1 file changed, 1 insertion(+)
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 5 commits.
(use "git push" to publish your local commits)
$ echo "print("master")" >> README
$ git commit -am "master commit"
warning: LF will be replaced by CRLF in README.
The file will have its original line endings in your working directory.
[master 3883017] master commit
1 file changed, 1 insertion(+)
$ git merge newtesting
Auto-merging README
CONFLICT (content): Merge conflict in README
Automatic merge failed; fix conflicts and then commit the result.
可以看到最后出現了合並沖突,沖突出現在README文件中,我們需要打開文件看看沖突情況:
```bash
$ vi README
print(1)
print(2)
print(3)
<<<<<<< HEAD
print(master)
=======
print(newtesting)
>>>>>>> newtesting
為了解決沖突,你必須選擇使用由=======分割的兩部分中的一個,或者你也可以自行合並這些內容。我們選擇print(master),從<<<<<<<<HEAD到>>>>>>>>newtesting,除了print(master)那句話,其他的都刪除。
$ git add README
$ git status
On branch master
Your branch is ahead of 'origin/master' by 6 commits.
(use "git push" to publish your local commits)
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
$ git commit -am "solve conflicts"
[master dc6b322] solve conflicts
$ git log --oneline --decorate --graph --all -8
* dc6b322 (HEAD -> master) solve conflicts
|\
| * 1aaf545 (newtesting) Newtesting commit
* | 3883017 master commit
|/
* 972909a add print(3) into README
* ce53a90 add MIT LICENSE
* ebc9b45 add print(2) into README
* 326dd0b add print(1) into README
* b861d60 branch note begin
6.7 遠程分支
還記得我們之前通過`git push -u origin master`命令推送我們地倉庫嗎?最后推送的地址是`origin/master`,這個叫做遠程分支。其實在本地有一個`origin/master`的指針,這個叫做遠程跟蹤分支,用來跟蹤遠程分支(最后一次溝通)的狀態,這個指針所指向的位置不會隨着本地操作而發生改變,而當使用`git fetch`、`git pull`等命令會隨着遠程倉庫的狀態而改變。而本地的master指針是會默認追蹤`origin/master`,這個追蹤是從`git clone`或者`git remote add`那一刻起。當你想要公開分享一個分支時,需要將其推送到有寫入權限的遠程倉庫上。本地的分支並不會自動與遠程倉庫同步-你必須顯式地推送想要分享的分支。這樣,你就可以把不願意分享的內容放到私人分支上,而將需要和別人協作的內容推送到公開分支。下面通過實例進行學習。
首先我們點開在GitHub創建的倉庫——playground,然后如圖所示,創建一個新分支testing,由於我已經創建,所以已經顯示有testing分支。
我們在本地中使用git fetch
命令,將剛剛創建的分支下載到本地。
$ git fetch
From github.com:FangYang970206/playground
* [new branch] testing -> origin/testing
可以看到多出新的分支testing(本地分支)跟蹤origin/master(遠程跟蹤分支)。我們通過git checkout <branch_name>
看到分支是否處於跟蹤的狀態。
$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
$ git checkout testing
Switched to a new branch 'testing'
Branch 'testing' set up to track remote branch 'testing' from 'origin'.
現在分別對testing和master分支做一次提交並push
$ echo "print("testing")" >> README
$ git commit -am README
warning: LF will be replaced by CRLF in README.
The file will have its original line endings in your working directory.
[testing f2466c5] README
1 file changed, 1 insertion(+)
$ git push origin testing
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 367 bytes | 183.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To github.com:FangYang970206/playground.git
dc6b322..f2466c5 testing -> testing
$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
$ echo "print("master1")" >> README
$ git commit -am README
warning: LF will be replaced by CRLF in README.
The file will have its original line endings in your working directory.
[master b849b25] README
1 file changed, 1 insertion(+)
$ git push origin master
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 360 bytes | 360.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To github.com:FangYang970206/playground.git
dc6b322..b849b25 master -> master
還可以更改其他本地指針來跟蹤遠程跟蹤分支,下面通過實例來學習
$ git checkout -b foo origin/master
Switched to a new branch 'foo'
Branch 'foo' set up to track remote branch 'master' from 'origin'.
$ echo "# if" >> test.rb
$ git commit -am "commit test.rb"
warning: LF will be replaced by CRLF in test.rb.
The file will have its original line endings in your working directory.
[foo d3f4109] commit test.rb
1 file changed, 1 insertion(+)
$ git push origin HEAD:master
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 260 bytes | 86.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To github.com:FangYang970206/playground.git
b849b25..d3f4109 HEAD -> master
$ git checkout testing
Switched to branch 'testing'
Your branch is up to date with 'origin/testing'.
$ git checkout -b testing1 origin/testing
Switched to a new branch 'testing1'
Branch 'testing1' set up to track remote branch 'testing' from 'origin'.
$ echo "print("testing1")" >> README
$ git commit -am "print testing1"
warning: LF will be replaced by CRLF in README.
The file will have its original line endings in your working directory.
[testing1 2600339] print testing1
1 file changed, 1 insertion(+)
$ git push origin HEAD:testing
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 373 bytes | 124.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To github.com:FangYang970206/playground.git
f2466c5..2600339 HEAD -> testing
$ git log --oneline --decorate --graph --all -9
* d3f4109 (origin/master, origin/HEAD, foo) commit test.rb
* b849b25 (master) README
| * 2600339 (HEAD -> testing1, origin/testing) print testing1
| * f2466c5 (testing) README
|/
* dc6b322 solve conflicts
|\
| * 1aaf545 Newtesting commit
* | 3883017 master commit
|/
* 972909a (newtesting) add print(3) into README
* ce53a90 add MIT LICENSE
可以看到之前跟蹤遠程跟蹤分支的master和testing指針是沒有移動,新建的foo和testing1取代了它們。這樣要注意一點,使用不同於遠程分支名的分支進行push,一定要指定當前分支,不然默認還是通過branch_name推送到origin/branch_name,出現Everything up-to-date,指定可以通過以上的HEAD(source):testing(target)
這里的取代方式可以有兩種:一種是直接新建分支取代,也就是上面的git checkout -b testing1(newbranch) origin/testing(Remote tracking branch)
,還有一種是通過現在分支進行取代,可以通過git branch -u <Remote tracking branch> <existed branch>
命令,注意一點,不要出現遠程跟蹤分支和跟蹤的指針出現分叉,這樣會導致Your branch and 'origin/xxx' have diverged
這個問題,詳細可參考這個鏈接。
6.8 分支練習(強烈推薦)
強烈建議去https://learngitbranching.js.org/,這個網站有很多分支的練習,還有一部分是遠程控制的練習,配合動畫,非常適合正在學習git的朋友,相信可以讓你的git本領上一個台階。7. pull request的使用
如果你發現你感興趣的倉庫的bug或者你想添加某個新功能到你感興趣的倉庫中,這時候你就可以pull request這個溝通利器了(一般是先在Issue中提出想法或問題,溝通好后可以在對應編號上創建pull request)。這里介紹一下一個倉庫,可供你進行pull request練習,倉庫地址是https://github.com/Data4Democracy/github-playground,當然,也歡迎你對本文的倉庫https://github.com/FangYang970206/playground進行pull request,由於對自己的倉庫進行pull request,本質上就是本地合並再提交,所以沒有必要,完全可以在本地進行,pull request是以協作開發為目的,為了方便,對https://github.com/Data4Democracy/github-playground進行pull request,看本篇文章的朋友就可以隨意選擇兩者之一。
pull request流程:
- 先fork你感興趣的倉庫到自己的倉庫中(副本)
- 將副本倉庫克隆到本地
- 從master分支中創建一個新分支
- 在分支中進行修改,以此改進項目
- 將分支推送到github倉庫
- 創建一個pull request
- 討論,根據實際情況繼續修改
- 項目的擁有者合並或關閉你的合並請求
打開https://github.com/Data4Democracy/github-playground,點擊Fork,等待副本倉庫生成,復制下載url,使用git clone
到本地,然后在master分支下創建PR_practice分支,然后使用vi hello_test.py對文件進行修改,在后面添加一句print("PR practice, thanks"),然后Ese:wq保存后,再進行提交修改,最后push到origin/PR_practice分支就可以了,詳細過程如下:
$ git clone git@github.com:FangYang970206/github-playground.git
Cloning into 'github-playground'...
remote: Counting objects: 230, done.
remote: Compressing objects: 100% (7/7), done.
remote: Total 230 (delta 2), reused 4 (delta 1), pack-reused 221
Receiving objects: 100% (230/230), 307.80 KiB | 324.00 KiB/s, done.
Resolving deltas: 100% (85/85), done.
$ cd github-playground/
$ git checkout -b PR_practice
Switched to a new branch 'PR_practice'
$ vi hello_test.py
$ git add hello_test.py
$ git commit -m "add PR practice"
[PR_practice a874bdf] add PR practice
1 file changed, 1 insertion(+)
$ git push origin PR_practice
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 309 bytes | 309.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To github.com:FangYang970206/github-playground.git
* [new branch] PR_practice -> PR_practice
我們再進入我們感興趣的倉庫地址,就會發現如下頁面
我們點擊旁邊的compare&pull request或者直接點擊New pull request,寫一下標題和commit,然后creat pull request。
最后等待倉庫擁有者審核,對這個pull request進行討論,看是否要進行再修改等等。另外,每一個pull request都可以看files changed,可以看到有哪些行添加進去了,有哪些刪除了,很是方便。
以上,就是一個pull request的流程,記得動手操作一遍。
8. 參考
最后,希望這篇文章能對看的朋友有所幫助,歡迎給這篇文章來個star。本文大量參考了[Pro Git][43],建議讀者可以去讀一讀這本git官網推薦的書籍。[git-github-intro][44]對git有一個不錯大致簡介。[learngitbranching][45]是一個非常不錯的動手學習網站,推薦去動手學習,更多資源可以去參考[trygit][46]里面的內容。我的博客即將搬運同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=24bgt0s40480c