Git的藝術

一、基本概念:

注:對于git的分布式概念及其優點,不重復說明,自己百度或谷歌。本文中涉及到指令前面有$的,在cmd中運行時,不要加上,$代表當前用戶是root。

三種文件狀態
  • 已暫存:對某個文件使用了git add指令,就會進入暫存區
  • 已提交:對某個文件使用了git commit指令,就會變為已提交,進入Git倉庫。
  • 已修改:對某個已提交的文件做了修改,但是未使用git add指令,進入工作目錄。
三個工作區域:
  • Git 倉庫

是 Git 用來保存項目的元數據和對象數據庫的地方。 這是 Git 中最重要的部分,從其它計算機克隆倉庫時,拷貝的就是這里的數據。

  • 工作目錄

從 Git 倉庫的壓縮數據庫中,將項目的某個版本獨立提取出來,放在磁盤上供你使用或修改。

  • 暫存區域

是一個文件,保存了下次將提交的文件列表信息,一般在 Git 倉庫目錄中。 有時候也被稱作“索引”,不過一般說法還是叫暫存區域。

二、常用指令

git clone :克隆倉庫

  • 克隆遠程倉庫代碼,會克隆項目的所有版本的代碼,全部保存在.git文件夾中,并且master代碼將默認存在于工作目錄中。

示例:git clone https://github.com/libgit2/libgit2

git add :添加文件到暫存區

(1)可以用它開始跟蹤新文件,
(2)或者把已跟蹤的文件放到暫存區,
(3)還能用于合并時把有沖突的文件標記為已解決狀態等。
(4)使用文件或目錄的路徑作為參數;如果參數是目錄的路徑,該命令將遞歸地跟蹤該目錄下的所有文件。

示例:git add README CONTRIBUTING.md;這里提交了兩個文件,注意空格。

git status:列出文件的狀態

  • 假設有個README文件已被跟蹤,CONTRIBUTING.md已被修改

運行git status,顯示如下


On branch master

Initial commit

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

        new file:   CONTRIBUTING.md
        new file:   README

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:   CONTRIBUTING.md

  • On branch master:指的是現在分支名是 “master”,這是默認的分支名。
  • Changes to be committed:表明是文件是已暫存狀態
  • Untracked files:未跟蹤文件,即未git add
  • Changes not staged for commit:說明已跟蹤文件的內容發生了變化,但還沒有放到暫存區,要使用git add放入暫存區。
  • 可以看到CONTRIBUTING.md 現在既存在于暫存區,又存在于git倉庫,這是什么情況?好吧,其實你只要明白一件事,那就是你運行git commint命令的時候,提交的是最后一次git add過的文件,你git add之后可能還會再次修改文件且不立即提交,但是這個再次修改的文件假如不再次運行git add命令,將不會把再次修改過的東西提交到git倉庫,只會提交你最后一次git add時的文件狀態,這下明白了吧。

git status -s:列出文件的簡要狀態信息

 M README
MM Rakefile
A  lib/git.rb
M  lib/simplegit.rb
?? LICENSE.txt
  • 右邊M:修改了的文件,未git add,處于工作區;注意倒數第二行還有個左邊M
  • 左邊M:修改了的文件,已git add,處于暫存區;
  • MM:git add了之后,又去修改,就是之前提到的處于兩種狀態的文件,處于暫存區和工作區
  • A:第一次git add,但是還沒有git commit的文件
  • ??:從未git add過的文件

.gitignore:文件忽略列表

一般我們總會有些文件無需納入 Git 的管理,也不希望它們總出現在未跟蹤文件列表。通常都是些自動生成的文件,比如日志文件,或者編譯過程中創建的臨時文件等。

示例:
$ cat .gitignore
*.[oa]
*~
第一行*.[oa]告訴 Git 忽略所有以 .o 或 .a 結尾的文件。
第二行告訴 Git 忽略所有以波浪符(~)結尾的文件,許多文本編輯軟件(比如 Emacs)都用這樣的文件名保存副本。

  • 格式規范

所有空行或者以 # 開頭的行都會被 Git 忽略。
可以使用標準的 glob 模式匹配。
匹配模式可以以(/)開頭防止遞歸。
匹配模式可以以(/)結尾指定目錄。
要忽略指定模式以外的文件或目錄,可以在模式前加上驚嘆號(!)取反。

  • glob 模式是指 shell 所使用的簡化了的正則表達式
  • 星號(*)匹配零個或多個任意字符;
  • [oa] 匹配oa中任意一個字符
  • 問號(?)只匹配一個任意字符
  • [0-9] 表示匹配所有 0 到 9 的任意一個數字
  • 兩個星號(*) 表示匹配任意中間目錄,比如a/**/z 可以匹配 a/z, a/b/z 或 a/b/c/z等。

git diff:差異比較

  • 不加參數:查看尚未暫存的文件更新了哪些部分,直接輸入 git diff
  • file:查看尚未暫存且指定的文件,更新了哪些部分
  • --cached:查看已暫存的,即將commit的文件和上次commit的文件的差異。
  • ffd98b291e0caa6c33575c1ef465eae661ce40c9 b8e7b00c02b95b320f14b625663fdecf2d63e74c 查看某兩個版本之間的差異
  • ffd98b291e0caa6c33575c1ef465eae661ce40c9:filename b8e7b00c02b95b320f14b625663fdecf2d63e74c:filename 查看某兩個版本的某個文件之間的差異

git commit:提交

  • -m :表示提交到倉庫,并且添加注釋
  • -a :跳過暫存區域,不git add,直接提交。

移除文件

rm PROJECTS.md  * 刪除工作目錄中的文件
rm -f PROJECTS.md  *刪除暫存區域的文件
git rm PROJECTS.md *提交刪除
git rm --cached README * 把文件從 Git 倉庫中刪除(亦即從暫存區域移除),但仍然希望保留在當前工作目錄中。適用于不小心git add了。
git rm log/\*.log: 刪除log文件夾下的所有log結尾的文件

git mv:重命名

git mv相當于以下三條指令:

$ mv README.md README
$ git rm README.md
$ git add README

git log:查看提交歷史


git clone https://github.com/schacon/simplegit-progit
進入到項目中執行: git log
  • git log -p -2:p表示顯示每次提交的內容差異。用 -2 來僅顯示最近兩次提交
  • git log --stat:打印每次提交的簡略的統計信息
  • git log --pretty=format:"%h - %an, %ar : %s":按照一定的格式打印
  • git log --pretty=format:"%h %s" --graph:更加優雅的打印
  • git log --since=2.weeks:列出最近兩周提交的內容
  • git log -Sfunction_name:列出包含名為function_name提交內容的歷史

git commit --amend:追加提交

假如你提交后發現忘記了暫存某些需要的修改,可以像下面這樣操作:
$ git commit -m 'initial commit'

$ git add forgotten_file
$ git commit --amend

最終你只會有一個提交 - 第二次提交將代替第一次提交的結果。
這個命令也可以修改提交信息。

git reset:從暫存區撤回到工作目錄

例如,你已經修改了兩個文件并且想要將它們作為兩次獨立的修改提交,但是卻意外地輸入了 git add * 暫存了它們兩個。如何只取消暫存兩個中的一個呢?

$ git add *
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    renamed:    README.md -> README
    modified:   CONTRIBUTING.md

在 “Changes to be committed” 文字正下方,提示使用 git reset HEAD <file>... 來取消暫存。所以,我們可以這樣來取消暫存 CONTRIBUTING.md 文件:

$ git reset HEAD CONTRIBUTING.md
Unstaged changes after reset:
M   CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    renamed:    README.md -> README

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:   CONTRIBUTING.md

CONTRIBUTING.md 文件已經是修改未暫存的狀態了。

git checkout:完全撤銷

如果你并不想保留對 CONTRIBUTING.md 文件的修改怎么辦?
運行:git checkout -- CONTRIBUTING.md,另外這個指令比較危險,因為它拷貝了另一個文件來覆蓋當前文件。

git remote:查看遠程倉庫

  • -v:列出遠程倉庫使用的 Git 保存的簡寫與其對應的 URL。

示例:git remote -v

git remote add:添加遠程倉庫

示例:$ git remote add pb https://github.com/paulboone/ticgit

  • 其中pb相當于后面那個連接的一個別名或引用,現在你可以在命令行中使用字符串 pb 來代替整個 URL。例如,如果你想拉取 Paul 的倉庫中有但你沒有的信息,可以運行 git fetch pb;

git fetch:從遠程倉庫中抓取與拉取

示例:$ git fetch [remote-name]

  • 這個命令會訪問遠程倉庫,從中拉取所有你還沒有的數據。執行完成后,你將會擁有那個遠程倉庫中所有分支的引用,可以隨時合并或查看。

git pull :從遠程倉庫中抓取并merge

由于 git pull 的魔法經常令人困惑所以通常單獨顯式地使用 fetch 與 merge 命令會更好一些。

git push [remote-name] [branch-name]:推送到遠程倉庫

$ git push origin master

  • 只有當你有所克隆服務器的寫入權限,并且之前沒有人推送過時,這條命令才能生效。當你和其他人在同一時間克隆,他們先推送到上游然后你再推送到上游,你的推送就會毫無疑問地被拒絕。你必須先將他們的工作拉取下來并將其合并進你的工作后才能推送。

git remote show [remote-name]:查看遠程倉庫

示例:$ git remote show origin
* remote origin
  Fetch URL: https://github.com/schacon/ticgit
  Push  URL: https://github.com/schacon/ticgit
  HEAD branch: master
  Remote branches:
    master                               tracked
    dev-branch                           tracked
  Local branch configured for 'git pull':
    master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (up to date)

git remote rename:重命名遠程倉庫的引用

示例:git remote rename pb paul

git remote rm:遠程倉庫的移除

示例:git remote rm paul

git rm -r --cached :撤回遠程倉庫中不必要的文件,但不刪除工作區的文件

 git rm -r --cached .idea/
 git commit
 git push

git tag :創建與查看標簽

  • 不加參數:查看所有標簽
  • -a [version]:創建附注標簽
  • [version]:創建輕量標簽

示例:
$ git tag -a v1.4
$ git tag
v0.1
v1.3
v1.4

git push origin

[version]:將標簽推送到遠程倉庫
--tags:將所有未推送到遠程的標簽推送到遠程

示例:
git push origin v1.5
git push origin --tags

git checkout -b version2 v2.0.0:同步標簽

git config --global alias:創建指令的別名

示例如下:
$ 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 commit時,只需要輸入 git ci。

添加取消暫存的別名:

$ git config --global alias.unstage 'reset HEAD --'

這會使下面的兩個命令等價:

$ git unstage fileA

$ git reset HEAD -- fileA

如何輕松地看到最后一次提交:

$ git config --global alias.last 'log -1 HEAD'

$ git last
commit 66938dae3329c7aebe598c2246a8e6af90d04646
Author: Josh Goebel <dreamer3@example.com>
Date:   Tue Aug 26 19:48:51 2008 +0800

    test for current head

    Signed-off-by: Scott Chacon <schacon@example.com>

git add -i :交互式暫存

當你修改一組文件后,希望這些改動能放到若干提交而不是混雜在一起成為一個提交時,這幾個工具會非常有用。 通過這種方式,可以確保提交是邏輯上獨立的變更集,同時也會使其他開發者在與你工作時很容易地審核。

可以看到這個命令以非常不同的視圖顯示了暫存區 - 基本上與 git status 是相同的信息,但是更簡明扼要一些。 它將暫存的修改列在左側,未暫存的修改列在右側。

(1) 暫存與取消暫存文件
  • 如果在 What now> 提示符后鍵入 2 或 u,腳本將會提示想要暫存哪個文件:


每個文件前面的 * 意味著選中的文件將會被暫存。 如果在 Update>> 提示符后不輸入任何東西并直接按回車,Git 將會暫存之前選擇的文件。

  • 如果這時想要取消暫存 TODO 文件,使用 3 或 r(撤消)選項:


  • 如果想要查看已暫存內容的區別,可以使用 6 或 d(區別)命令。 它會顯示暫存文件的一個列表,可以從中選擇想要查看的暫存區別。 這跟你在命令行指定 git diff --cached 非常相似:


(2) 暫存補丁

Git 也可以暫存文件的特定部分。 例如,如果在 simplegit.rb 文件中做了兩處修改,但只想要暫存其中的一個而不是另一個,Git 會幫你輕松地完成。

  • 假如現在你在文件中修改了如下圖的兩個部分:



    現在鍵入5 或 p(補丁),將會一個個顯示你的修改區塊,如下:


注意最下面一行,指的是你當前可選的操作,再鍵入?,查看選項的詳細說明。



通常情況下可以輸入 y 或 n 來選擇是否要暫存每一個區塊,當然,暫存特定文件中的所有部分或為之后的選擇跳過一個區塊也是非常有用的。
繼續輸入“y”:



退出,然后輸入:git commit

也可以不必在交互式添加模式中做部分文件暫存 - 可以在命令行中使用 git add -p 或 git add --patch 來啟動同樣的腳本。

git stash:儲藏與清理修改

儲藏修改:即,在某個分支上修改了部分代碼,然后將未完成的修改保存到一個棧上,而你可以在任何時候重新應用這些改動到其他的分支上。

進入項目并改動幾個文件,然后可能暫存其中的一個改動。 如果運行 git status,可以看到有改動的狀態:

$ git status
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   index.html

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:   lib/simplegit.rb

現在想要切換分支,但是還不想要提交之前的工作;所以儲藏修改。 將其推送到棧上,運行git stash 或 git stash save:

$ git stash
Saved working directory and index state \
  "WIP on master: 049d078 added the index file"
HEAD is now at 049d078 added the index file
(To restore them type "git stash apply")

然后查看工作目錄,發現是干凈的了:

$ git status
# On branch master
nothing to commit, working directory clean

在這時,你能夠輕易地切換分支并在其他地方工作;你的修改被存儲在棧上。 要查看儲藏的東西,可以使用 git stash list:

$ git stash list
stash@{0}: WIP on master: 049d078 added the index file
stash@{1}: WIP on master: c264051 Revert "added file_size"
stash@{2}: WIP on master: 21d80a5 added number to log

在本例中,有兩個之前做的儲藏,所以你接觸到了三個不同的儲藏工作。 可以通過原來 stash 命令的幫助提示中的命令將你剛剛儲藏的工作重新應用:git stash apply。 如果想要應用其中一個更舊的儲藏,可以通過名字指定它,像這樣:git stash apply stash@{2}。 如果不指定一個儲藏,Git 認為指定的是最近的儲藏:

$ git stash apply
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#
#      modified:   index.html
#      modified:   lib/simplegit.rb
#

但是這里有個問題,lib/simplegit.rb不是暫存狀態,這和未切換分支前的狀態不一樣。所以可以使用以下指令,將這兩個文件的狀態和未切換分支前保持一致:

$ git stash apply --index
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#      modified:   index.html
#
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#
#      modified:   lib/simplegit.rb
#
  • 幾個創造性的儲藏方式
    (1)git stash --keep-index:已暫存的文件其狀態儲藏為已修改
    (2)git stash -u:儲藏時,不要跟蹤任何文件,應用儲藏之后由自己手動git add
    (3)git stash --patch:交互式儲藏
git stash branch:從儲藏創建一個分支

如果儲藏了一些工作,將它留在那兒了一會兒,然后繼續在儲藏的分支上工作,在重新應用工作時可能會有問題。 如果應用嘗試修改剛剛修改的文件,你會得到一個合并沖突并不得不解決它。 如果想要一個輕松的方式來再次測試儲藏的改動,可以運行 git stash branch 創建一個新分支,檢出儲藏工作時所在的提交,重新在那應用工作,然后在應用成功后扔掉儲藏:

$ git stash branch testchanges
Switched to a new branch "testchanges"
# On branch testchanges
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#      modified:   index.html
#
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#
#      modified:   lib/simplegit.rb
#
Dropped refs/stash@{0} (f0dfc4d5dc332d1cee34a634182e168c4efc3359)

這是在新分支輕松恢復儲藏工作并繼續工作的一個很不錯的途徑。

git clean:從工作目錄中移除未被追蹤的文件(請謹慎使用)

你需要謹慎地使用這個命令,因為它被設計為從工作目錄中移除未被追蹤的文件。 如果你改變主意了,你也不一定能找回來那些文件的內容。 一個更安全的選項是運行 git stash --all 來移除每一樣東西并存放在棧中。

如果只是想要看看它會做什么,可以使用 -n 選項來運行命令,這意味著 “做一次演習然后告訴你將要 移除什么”。

$ git clean -d -n
Would remove test.o
Would remove tmp/

git grep:搜索

Git 提供了一個 grep 命令,你可以很方便地從提交歷史或者工作目錄中查找一個字符串或者正則表達式。

你可以傳入 -n 參數來輸出 Git 所找到的匹配行行號:


如果你想看匹配的行是屬于哪一個方法或者函數,你可以傳入 -p 選項:


git log :查看代碼是什么時候引入的

行日志搜索

行日志搜索是另一個相當高級并且有用的日志搜索功能。 這是一個最近新增的不太知名的功能,但卻是十分有用。 在 git log 后加上 -L 選項即可調用,它可以展示代碼中一行或者一個函數的歷史。
比如想要查看listShareLink方法在ConsoleController.java文件中的提交歷史,可以運行以下命令:

git log -L '/listShareLink/',/^}/:src/main/java/com/jss/jssfinance/controller/ConsoleController.java

git reset:重點理解

git reset指令用于撤銷,revert到之前的版本,理解了它,才能更好的理解git;

幾個基本概念(三種樹)
  • HEAD樹:分支的引用,里面包含最后一次提交的快照對象,可以粗狂的理解為Git倉庫
  • Index樹:暫存區的快照
  • Working Directory樹:可以粗狂的理解為工作目錄

假設某個master分支有三次提交,看起來像下面:


三種樹的狀態如下:


如果這個時候運行git status,發現三者狀態一樣,是clean狀態。

緊接著運行git reset,它主要做了三件事情:

第 1 步:移動 HEAD

三種樹的狀態如下:


但如果你不運行get reset “上一次序列號”,而是運行 git reset --soft “上一次序列號”,它就不繼續往下。這種狀態相當于撤回commit,將HEAD的引用指向上一次提交的序列號,但是不會改變索引和工作目錄。

第 2 步:更新索引(--mixed)

接下來,reset 會用 HEAD 指向的當前快照的內容來更新索引。
三種樹的狀態如下:


如果指定 --mixed 選項,reset 將會在這時停止。 這也是默認行為,所以如果沒有指定任何選項(在本例中只是 git reset HEAD~),這就是命令將會停止的地方。這樣表示將取消暫存,相當于撤回了git add命令。

第 3 步:更新工作目錄(--hard)

reset 要做的的第三件事情就是讓工作目錄看起來像索引。 如果使用 --hard 選項,它將會繼續這一步。



必須注意,--hard 標記是 reset 命令唯一的危險用法,它也是 Git 會真正地銷毀數據的僅有的幾個操作之一。 其他任何形式的 reset 調用都可以輕松撤消,但是 --hard 選項不能,因為它強制覆蓋了工作目錄中的文件。

三、分支

什么是分支?看圖,看完圖你就明白了。

這里有個假設,假設某個目錄下有三個待保存到暫存區的文件。


產生的提交對象會包含一個指向上次提交對象(父對象)的指針


創建分支指令:git branch testing ,其中master是默認存在的分支



注意:HEAD是一個標記,HEAD指向哪個分支,就代表你當前的倉庫處于哪個分支上

分支切換: git checkout testing



可以看到HEAD已經指向了testing

這時候提交一次版本,發現testing分支在向前移動:


再切換回master分支:git checkout master



這條命令做了兩件事。一是使 HEAD 指回 master 分支,二是將工作目錄恢復成 master 分支所指向的快照內容。也就是說,你現在做修改的話,項目將始于一個較舊的版本。本質上來講,這就是忽略 testing 分支所做的修改,以便于向另一個方向進行開發。

再提交一個版本:git commit -a -m 'made other changes'



現在,這個項目的提交歷史已經產生了分叉,你可以在不同分支間不斷地來回切換和工作,并在時機成熟時將它們合并起來。而所有這些工作,你需要的命令只有 branch、checkout 和 commit。

查看分叉歷史

git log --oneline --decorate --graph --all
$ git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) made other changes
| * 87ab2 (testing) made a change
|/
* f30ab add feature #32 - ability to add new formats to the
* 34ac2 fixed bug #1328 - stack overflow under certain conditions
* 98ca9 initial commit of my project

由于 Git 的分支實質上僅是包含所指對象校驗和(長度為 40 的 SHA-1 值字符串)的文件,所以它的創建和銷毀都異常高效。創建一個新分支就像是往一個文件中寫入 41 個字節(40 個字符和 1 個換行符),如此的簡單能不快嗎?

四、分支的創建

假設你現在有一個默認分支master,并且已經有了一些提交;現在,你已經決定要解決你的公司使用的問題追蹤系統中的 #53 問題,所以你新建了一個分支并同時切換到那個分支上,你可以運行一個帶有 -b 參數的 git checkout 命令:

$ git checkout -b iss53
Switched to a new branch "iss53"

  • 它是下面兩條命令的簡寫:

$ git branch iss53
$ git checkout iss53

你繼續在 #53 問題上工作,并且做了一些提交,結果如下圖:


現在你接到個電話,有個緊急問題(hotfix)等待你來解決。有了 Git 的幫助,你不必把這個緊急問題和 iss53 的修改混在一起,你也不需要花大力氣來還原關于 53# 問題的修改,然后再添加關于這個緊急問題的修改,最后將這個修改提交到線上分支。你所要做的僅僅是切換回 master 分支。


但是要留意你的工作目錄和暫存區里那些還沒有被提交的修改,它可能會和你即將檢出的分支產生沖突從而阻止 Git 切換到該分支。最好的方法是,在你切換分支之前,保持好一個干凈的狀態。可以使用保存進度(stashing) 和 修補提交(commit amending)的方式達到這一目的。

$ git checkout master
Switched to branch 'master'

接下來,你要修復這個緊急問題,于是建立一個針對該緊急問題的分支(hotfix branch):

$ git checkout -b hotfix
Switched to a new branch 'hotfix'
$ vim index.html
$ git commit -a -m 'fixed the broken email address'
[hotfix 1fb7853] fixed the broken email address
1 file changed, 2 insertions(+)

于是當前分支情況如下圖:


然后你可以運行你的測試,確保你的修改是正確的,然后將其合并回你的 master 分支來部署到線上。你可以使用 git merge 命令來達到上述目的:

$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
index.html | 2 ++
1 file changed, 2 insertions(+)

在合并的時候,你應該注意到了"快進(fast-forward)"這個詞。當你試圖合并兩個分支時,如果順著一個分支走下去能夠到達另一個分支,那么 Git 在合并兩者的時候,只會簡單的將指針向前推進(指針右移),因為這種情況下的合并操作沒有需要解決的分歧——這就叫做 “快進(fast-forward)”。

合并完成之后,如下圖:


關于這個緊急問題的解決方案發布之后,你準備回到被打斷之前時的工作中。然而,你應該先刪除 hotfix 分支,因為你已經不再需要它了 —— master 分支已經指向了同一個位置。你可以使用帶 -d 選項的 git branch 命令來刪除分支:

$ git branch -d hotfix
Deleted branch hotfix (3a0874c).

現在你可以切換回你正在工作的分支繼續你的工作,也就是針對 #53 問題的那個分支(iss53 分支)。

$ git checkout iss53
Switched to branch "iss53"
$ vim index.html
$ git commit -a -m 'finished the new footer [issue 53]'
[iss53 ad82d7a] finished the new footer [issue 53]
1 file changed, 1 insertion(+)

當前分支情況如下圖:


git merge --abort:遇到沖突時,退出合并。

五、分支的合并

手動合并

假設你已經修正了 #53 問題,并且打算將你的工作合并入 master 分支。為此,你需要合并 iss53 分支到 master 分支,這和之前你合并 hotfix 分支所做的工作差不多。你只需要檢出到你想合并入的分支,然后運行 git merge 命令:

$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index.html | 1 +
1 file changed, 1 insertion(+)

這和你之前合并 hotfix 分支的時候看起來有一點不一樣。在這種情況下,你的開發歷史從一個更早的地方開始分叉開來(diverged)。因為,master 分支所在提交并不是 iss53 分支所在提交的直接祖先,Git 不得不做一些額外的工作。出現這種情況的時候,Git 會使用兩個分支的末端所指的快照(C4 和 C5)以及這兩個分支的工作祖先(C2),做一個簡單的三方合并。參照下圖:

和之前將分支指針向前推進所不同的是,Git 將此次三方合并的結果做了一個新的快照并且自動創建一個新的提交指向它。這個被稱作一次合并提交,它的特別之處在于他有不止一個父提交。如下圖:


需要指出的是,Git 會自行決定選取哪一個提交作為最優的共同祖先,并以此作為合并的基礎;這和更加古老的 CVS 系統或者 Subversion (1.5 版本之前)不同,在這些古老的版本管理系統中,用戶需要自己選擇最佳的合并基礎。Git 的這個優勢使其在合并操作上比其他系統要簡單很多。

既然你的修改已經合并進來了,你已經不再需要 iss53 分支了。現在你可以在任務追蹤系統中關閉此項任務,并刪除這個分支。

$ git branch -d iss53

其他類型的合并

如果我們回到之前我們使用的 “hello world” 例子中,我們可以看到合并入我們的分支時引發了沖突。

$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Resolved 'hello.rb' using previous resolution.
Automatic merge failed; fix conflicts and then commit the result.

然而如果我們運行時增加 -Xours 或 -Xtheirs 參數就不會有沖突。

$ git merge -Xours mundo
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 test.sh  | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)
 create mode 100644 test.sh

撤銷合并

有時候,你不小心合并了兩個分支,但是你想撤銷。
首先查看合并歷史:


Merge: 6b9c6bc d70c00b,表示e9c5e9e 是從6b9c6bc和d70c00b這兩次提交合并生產的,然后運行以下指令:

git revert -m 1 HEAD

這會使得當前的代碼還原為6b9c6bc的提交狀態,如果-m后面指定的是2,則會還原為d70c00b的提交狀態。

如果這個時候,另一個被合并的分支繼續提交一次,然后合并,git會告訴你已經是最新狀態,無需合并了,這就讓人困惑了。然后繼續運行以下指令:

git revert ^M

再合并:

git merge 指定分支

這樣就解決無法合并的問題。

六、遇到沖突時的分支合并

有時候合并操作不會如此順利。如果你在兩個不同的分支中,對同一個文件的同一個部分進行了不同的修改,Git 就沒法干凈的合并它們。如果你對 #53 問題的修改和有關 hotfix 的修改都涉及到同一個文件的同一處,在合并它們的時候就會產生合并沖突:

$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

此時 Git 做了合并,但是沒有自動地創建一個新的合并提交。Git 會暫停下來,等待你去解決合并產生的沖突。你可以在合并沖突后的任意時刻使用 git status 命令來查看那些因包含合并沖突而處于未合并(unmerged)狀態的文件:

$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")

Unmerged paths:
  (use "git add <file>..." to mark resolution)

    both modified:      index.html

no changes added to commit (use "git add" and/or "git commit -a")

任何因包含合并沖突而有待解決的文件,都會以未合并狀態標識出來。Git 會在有沖突的文件中加入標準的沖突解決標記,這樣你可以打開這些包含沖突的文件然后手動解決沖突。出現沖突的文件會包含一些特殊區段,看起來像下面這個樣子:

<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
 please contact us at support@github.com
</div>
>>>>>>> iss53:index.html

這表示 HEAD 所指示的版本(也就是你的 master 分支所在的位置,因為你在運行 merge 命令的時候已經檢出到了這個分支)在這個區段的上半部分(======= 的上半部分),而 iss53 分支所指示的版本在 ======= 的下半部分。為了解決沖突,你必須選擇使用由 ======= 分割的兩部分中的一個,或者你也可以自行合并這些內容。例如,你可以通過把這段內容換成下面的樣子來解決沖突:

<div id="footer">
please contact us at email.support@github.com
</div>

上述的沖突解決方案僅保留了其中一個分支的修改,并且 <<<<<<< , ======= , 和 >>>>>>> 這些行被完全刪除了。在你解決了所有文件里的沖突之后,對每個文件使用 git add 命令來將其標記為沖突已解決。一旦暫存這些原本有沖突的文件,Git 就會將它們標記為沖突已解決。

如果你想使用圖形化工具來解決沖突,你可以運行 git mergetool,該命令會為你啟動一個合適的可視化合并工具,并帶領你一步一步解決這些沖突:

$ git mergetool

This message is displayed because 'merge.tool' is not configured.
See 'git mergetool --tool-help' or 'git help config' for more details.
'git mergetool' will now attempt to use one of the following tools:
opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse diffmerge ecmerge p4merge araxis bc3 codecompare vimdiff emerge
Merging:
index.html

Normal merge conflict for 'index.html':
  {local}: modified file
  {remote}: modified file
Hit return to start merge resolution tool (opendiff):

如果你想使用除默認工具(在這里 Git 使用 opendiff 做為默認的合并工具,因為作者在 Mac 上運行該程序)外的其他合并工具,你可以在 “下列工具中(one of the following tools)” 這句后面看到所有支持的合并工具。然后輸入你喜歡的工具名字就可以了。

如果你需要更加高級的工具來解決復雜的合并沖突,我們會在 “高級合并” 介紹更多關于分支合并的內容。

等你退出合并工具之后,Git 會詢問剛才的合并是否成功。如果你回答是,Git 會暫存那些文件以表明沖突已解決:你可以再次運行 git status 來確認所有的合并沖突都已被解決:

$ git status
On branch master
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:

    modified:   index.html

如果你對結果感到滿意,并且確定之前有沖突的的文件都已經暫存了,這時你可以輸入 git commit 來完成合并提交。默認情況下提交信息看起來像下面這個樣子:

Merge branch 'iss53'

Conflicts:
    index.html
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
#   .git/MERGE_HEAD
# and try again.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# All conflicts fixed but you are still merging.
#
# Changes to be committed:
#   modified:   index.html
#

六、遇到沖突時的分支合并(二)

上面處理沖突是比較手工的方式,有沒有優雅一點的方式呢?

1. 非手動修改文件再合并

首先要理解,沖突是怎么來的,為什么會產生沖突。

在版本控制工具里面沖突是這樣的,大家從同一個分支中克隆代碼,然后都修改了同一個地方,先提交的不會有沖突的問題,但是緊接著后提交的就會產生一個沖突。

注意上面這段話的關鍵,是一個文件同一個地方被連續的提交。

假設有一個hello.rb文件,產生沖突的原因是兩個分支上的hello.rb文件所使用的換行符是不同的,一個是unix的換行符,一個是dos的換行符,當merge之后,產生了沖突,你可以運行以下指令導出三個版本的文件:

$ git show :1:hello.rb > hello.common.rb #代表祖先版本
$ git show :2:hello.rb > hello.ours.rb  #自己的版本
$ git show :3:hello.rb > hello.theirs.rb #他們的版本

解決的方案是這樣的:先轉換換行符,然后調用git merge-file指令進行合并,如下:

$ dos2unix hello.theirs.rb
dos2unix: converting file hello.theirs.rb to Unix format ...

$ git merge-file -p \
    hello.ours.rb hello.common.rb hello.theirs.rb > hello.rb

$ git diff -b
diff --cc hello.rb
index 36c06c8,e85207e..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,8 -1,7 +1,8 @@@
  #! /usr/bin/env ruby

 +# prints out a greeting
  def hello
-   puts 'hello world'
+   puts 'hello mundo'
  end

  hello()

你還可以運行以下指令查看引入了什么:

  • git diff --ours
  • git diff --theirs
  • git diff --base

2. 檢出沖突

現在有只在 master 分支上的三次單獨提交,還有其他三次提交在 mundo 分支上。 如果我們嘗試將 mundo 分支合并入 master 分支,我們得到一個沖突。

$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.

我們想要看一下合并沖突是什么。 如果我們打開這個文件,我們將會看到類似下面的內容:

#! /usr/bin/env ruby

def hello
<<<<<<< HEAD
  puts 'hola world'
=======
  puts 'hello mundo'
>>>>>>> mundo
end

hello()

合并的兩邊都向這個文件增加了內容,但是導致沖突的原因是其中一些提交修改了文件的同一個地方。

繼續運行以下指令:

$ git checkout --conflict=diff3 hello.rb

會得到這樣一個結果:

#! /usr/bin/env ruby

def hello
<<<<<<< ours
  puts 'hola world'
||||||| base
  puts 'hello world'
=======
  puts 'hello mundo'
>>>>>>> theirs
end

hello()

不僅僅只給你 “ours” 和 “theirs” 版本,同時也會有 “base” 版本在中間來給你更多的上下文。

七、分支管理

git branch 命令不只是可以創建與刪除分支。如果不加任何參數運行它,會得到當前所有分支的一個列表:

$ git branch
  iss53
* master
  testing

注意 master 分支前的 * 字符:它代表現在檢出的那一個分支(也就是說,當前 HEAD 指針所指向的分支)。這意味著如果在這時候提交,master 分支將會隨著新的工作向前移動。如果需要查看每一個分支的最后一次提交,可以運行 git branch -v 命令:

$ git branch -v
  iss53   93b412c fix javascript issue
* master  7a98805 Merge branch 'iss53'
  testing 782fd34 add scott to the author list in the readmes

--merged 與 --no-merged 這兩個有用的選項可以過濾這個列表中已經合并或尚未合并到當前分支的分支。如果要查看哪些分支已經合并到當前分支,可以運行 git branch --merged:

$ git branch --merged
  iss53
* master

因為之前已經合并了 iss53 分支,所以現在看到它在列表中。在這個列表中分支名字前沒有 * 號的分支通常可以使用 git branch -d 刪除掉;你已經將它們的工作整合到了另一個分支,所以并不會失去任何東西。

查看所有包含未合并工作的分支,可以運行 git branch --no-merged:

$ git branch --no-merged
  testing

這里顯示了其他分支。因為它包含了還未合并的工作,嘗試使用 git branch -d 命令刪除它時會失敗:

$ git branch -d testing
error: The branch 'testing' is not fully merged.
If you are sure you want to delete it, run 'git branch -D testing'.

如果真的想要刪除分支并丟掉那些工作,如同幫助信息里所指出的,可以使用 -D 選項強制刪除它。

八、建立遠程倉庫

1.安裝遠程和本地git,參考百度或谷歌

或參考:https://git-scm.com/book/zh/v1/%E8%B5%B7%E6%AD%A5-%E5%AE%89%E8%A3%85-Git

2.使用ssh的方式作為git協議

  • (1)首先要在本地生成一對密鑰,使用以下指令:

ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

進入你的家目錄(不知道什么是家目錄,請谷歌或百度自補), 可以看到在家目錄下面多了一個.ssh文件夾,且多了兩個文件,id_rsa是私鑰,id_rsa.pub是公鑰,如下圖:


  • (2)添加公鑰
    然后將id_rsa.pub放到遠程主機,也就是git服務器上面的git用戶的家目錄的.ssh文件夾下面,你最好重命名再上傳。假設你現在已經將id_rsa.pub放到了.ssh文件夾下面,然后你需要運行以下指令:

cat id_rsa.pub >> authorized_keys

重啟ssh服務:

service sshd restart

3.將項目上傳到遠程服務器,并建立分支

(1)以項目Thirddevops為例,進入Thirddevops文件夾下,運行以下指令:

git init

(2)運行以下指令,生成一個裸倉庫:

git clone --bare Thirddevops Thirddevops.git

緊接著會在當前文件夾下面生成了一個Thirddevops.git文件夾

(3)將Thirddevops.git文件夾復制到遠程主機

scp -r Thirddevops.git 用戶名@遠程主機ip或域名:目標路徑/Thirddevops.git

(4)將Java項目push到遠程倉庫

# on my computer
$ cd Thirddevops
$ git add .
(或git add 指定的文件夾)
$ git commit -m 'initial commit'
$ git remote add origin git@gitserver:/opt/git/Thirddevops.git
$ git push origin master

(4)讓別人拉取遠程倉庫的代碼

$ git clone git@gitserver:/opt/git/Thirddevops.git
$ cd Thirddevops
$ vim README
$ git commit -am 'fix for the README file'
$ git push origin master

九、一些其他的小問題

1.有關于跨平臺換行符的問題

windows用戶需要做這樣一個全局的配置:

$ git config --global core.autocrlf true

unix或mac用戶則需要這樣配置:

$ git config --global core.autocrlf input

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,283評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,947評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,094評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,485評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,268評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,817評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,906評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,039評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,551評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,502評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,662評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,188評論 5 356
  • 茶點故事閱讀 43,907評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,304評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,563評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,255評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,637評論 2 370

推薦閱讀更多精彩內容

  • Git 基礎 基本原理 客戶端并不是只提取最新版本的文件快照,而是把代碼倉庫完整的鏡像下來。這樣一來,任何一處協同...
    __silhouette閱讀 15,922評論 5 147
  • Git 命令行學習筆記 Git 基礎 基本原理 客戶端并不是只提取最新版本的文件快照,而是把代碼倉庫完整的鏡像下來...
    sunnyghx閱讀 3,932評論 0 11
  • 如今天氣熱到已經可以煎雞蛋,西瓜已然不能滿足我們對涼爽的渴望,那不如先放下手邊會讓人熱血沸騰的各類番劇,比...
    腦洞舍七閱讀 470評論 0 0
  • 在車上突然想起一次在揭陽機場遇到的姑娘,我們在點煙室遇到,她不會用,然后我走過去幫她點,本來想拇指摁煙頭,結果猝不...
    潤澤清流閱讀 529評論 0 0
  • 我坐在曾祖母的搖椅上,太陽暖洋洋的,像一層被子鋪在身上。 曾祖母在我印象中一直是沉默寡言的。對于家庭的一些瑣事,家...
    借山居閱讀 603評論 0 3