# Git - 基礎篇 ## 前言 在軟體開發領域,對原始碼進行**版本控管**是非常重要的一件事。Git 是一套分散式版本控管系統(Distributed Version Control System),支援本地操作、遠端協作、彈性的分支與合併、原始碼審核(Code Review)等功能,對專案原始碼的品質管理很有幫助。 ## What is Git? 一個檔案可能會經過多次的修改,如果將每一次的修改都保存下來,這個行為就是「版本控制」。一般使用者在進行版本控制的時候,大多都是以「資料夾」為單位,如下圖所示:  現在有一個名為 `resume` 的目錄,目錄最初存放了 3 個檔案。在此之後也許會對 `resume` 目錄中的檔案有些操作,不論是新增、刪除檔案,或是修改檔案內容,只要「`resume` 目錄的**狀態有變化**」,就稱之為一個「**版本**」。  以日期為目錄名稱進行版本控制,雖然可以知道在什麼時間點修改過檔案,但是卻不能在第一時間反應,在那個時間點對檔案做了什麼變動,也無法比較兩個時間點間的檔案差異。 Git 就是一種版本控制系統,清楚地記錄每個檔案在什麼時候被新增、被修改或被刪除,透過 Commit Message 可以看到以前的歷史紀錄、和上一個版本差異;在多人協作方面,Git 擁有快速的分支與合併機制,強力支援分散式開發模式,有效率的處理大型專案。 ## Git 基本指令 ### 初始化目錄 ```bash $ cd /desktop # 切換至 /desktop 目錄 $ mkdir git-practice # 建立 git-practice 目錄 $ cd git-practice # 切換至 git-practice 目錄 $ git init # 初始化目錄,讓 Git 對這個目錄開始進行版控 Initialized empty Git repository in /desktop/git-practice/.git/ ``` `git init` 會在目錄建立一個 `.git` 隱藏目錄,整個版控的精華就在這裡面。 ### 查詢目錄狀態 ```bash $ git status On branch master No commits yet nothing to commit (create/copy files and use "git add" to track) ``` 在這個目錄裡,除了 Git 初始化目錄後產生的 `.git` 隱藏目錄外,其他什麼都沒有,所以上面這段訊息的意思是「現在沒有東西可以提交(nothing to commit)」。 :::danger **注意** **空的目錄**是無法被 Commit 的,因為 Git 在計算、產生物件的時候,是根據「**檔案變化的內容**」去做計算的,**內容不可以為空**。 ::: ### 加入版控追蹤 在目錄裡新增一個 `index.html` 檔案,使用 `git status` 指令查詢目錄狀態: ```bash $ touch index.html $ git status On branch master No commits yet Untracked files: (use "git add <file>..." to include in what will be committed) index.html nothing added to commit but untracked files present (use "git add" to track) ``` `index.html` 檔案目前的狀態是 **Untracked files**,意思是這個檔案**尚未被加到 Git 版控系統**裡,還沒開始正式被 Git「追蹤」,它只是剛剛加入這個目錄而已。 使用 `git add index.html` 指令,將 `index.html` 檔案交給 Git,讓 Git 開始追蹤它。再次使用 `git status` 指令查詢目錄狀態: ```bash $ git add index.html $ git status On branch master No commits yet Changes to be committed: (use "git rm --cached <file>..." to unstage) new file: index.html ``` `index.html` 檔案的狀態從 **Untracked files** 變成 **new file** 狀態了,表示這個檔案已經被安置到**暫存區**(**Staging Area**),等待稍後跟其它檔案一起被存到**儲存庫**(**Repository**)裡。 :::success **狀況題** 如果在 **`git add`** 之後,又修改了那個檔案的內容? 在 **`index.html`** 檔案被安置到**暫存區**後,又對這個檔案進行了修改,使用 **`git status`** 指令查詢目錄狀態: ```bash $ git status On branch master No commits yet Changes to be committed: (use "git rm --cached <file>..." to unstage) new file: index.html Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: index.html ``` 對 Git 來說,**`index.html`** 檔案修改後的內容並沒有被加到暫存區裡,所以這時候在暫存區裡的 **`index.html`** 檔案是維持在**修改前**的狀態。如果確定檔案修改完畢,再次使用 **`git add index.html`** 指令,將 **`index.html`** 檔案安置到暫存區中。 ::: ### 把暫存區的內容提交到儲存庫裡存檔 要讓暫存區的內容永久地保存下來,需要使用 `git commit` 指令: ```bash $ git commit -m '<create> index.html' [master (root-commit) 7f7ec9c] <create> index.html 1 file changed, 9 insertions(+) create mode 100644 index.html ``` 對 Git 來說,`git commit` 的意思是「**把暫存區(Staging Area)的東西存放到儲存庫(Repository)裡**」,對開發者而言,就是「我完成一個存檔(備份)的動作了」,這樣就建立了一個新的版本。 在 `git commit` 後面加上 `-m '<create> index.html'` 的用意是說明「在這次的 Commit 做了什麼事」。在 Commit 的時候,如果沒有輸入這個訊息,Git 預設是不會讓你完成此次的 Commit。Commit Message 最主要的目的就是告訴自己以及其它人「這次的修改做了什麼」。  :::danger **注意** 要完成 **`git commit`** 指令,才是**完整建立了一個版本**的流程。每一次的 Commit 都只會處理暫存區裡的內容。 ::: ### 檢視提交的歷史記錄 ```bash $ git log commit 7f7ec9cf6b7d1bdbc3972c8882f1278b9414d35e (HEAD -> master) Author: AveryChen <avery210412@gmail.com> Date: Thu Nov 2 15:03:16 2023 +0800 <create> index.html ``` 最新提交的 Commit 會先被列出來,歷史紀錄會列出每筆 Commit 的 SHA-1 校驗碼、作者名字與電子郵件、寫入日期以及 Commit Message 等資訊。 ### Git 檔案追蹤機制  * Untracked file:尚未被追蹤的檔案 * Changes to be committed:已加入暫存區,準備變成一個 Commit 的檔案 * Changes not staged for commit:尚未加入到暫存區,但有被加入到追蹤的檔案 ### 刪除檔案 / 變更檔名 #### 刪除檔案 在 Git 裡,不管是刪除檔案或是變更檔名,對 Git 來說都是一種「修改」。 ```bash $ git rm index.html $ git status On branch master Changes to be committed: (use "git restore --staged <file>..." to unstage) deleted: index.html ``` `index.html` 檔案目前的狀態是 **deleted**,而且已經被安置到暫存區中,可以直接進行 Commit。 如果只是想把檔案從 `.git` 目錄中移除,**不再進行 Git 版本控制**,可以在 `git rm <file>` 指令後面加上 `-–cached` 參數: ```bash $ git rm index.html --cached rm 'index.html' ``` 這個訊息並不是真的把 `index.html` 檔案刪除了,只是把檔案從 Git 裡移除而已。使用 `git status` 指令查詢目錄狀態: ```bash $ git status On branch master Changes to be committed: (use "git restore --staged <file>..." to unstage) deleted: index.html Untracked files: (use "git add <file>..." to include in what will be committed) index.html ``` `index.html` 檔案的狀態從原本已經在暫存區裡的 **Staged** 變成 **Untracked**,這個檔案就沒有在 Git 版本控制的範圍內了。 #### 變更檔名 ```bash $ git mv index.html home.html $ git status On branch master Changes to be committed: (use "git restore --staged <file>..." to unstage) renamed: index.html -> home.html ``` `index.html` 檔案目前的狀態是 **renamed**,而且已經被安置到暫存區中,可以直接進行 Commit。 ### 取消刪除 / 修改檔案 在 Git 裡面如果不小心把檔案或目錄刪掉是救得回來的,而當你修改某個檔案但後悔了,也可以把檔案回復成它上一次 Commit 的狀態。 使用 `git checkout <file>` 指令,可以把檔案從 `.git` 目錄裡拉一份到目前的工作目錄(Working Directory),也就是說這個指令**會把暫存區裡的內容,拿來覆蓋工作目錄的內容**,它會把檔案回復到上一次 Commit 的狀態。 * 刪除專案裡所有 HTML 檔案 ```bash $ rm *.html $ git status On branch master Changes not staged for commit: (use "git add/rm <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) deleted: about.html deleted: home.html deleted: index.html deleted: product.html no changes added to commit (use "git add" and/or "git commit -a") ``` * 把被刪除的 `home.html` 檔案救回來: ```bash $ git checkout home.html Updated 1 path from the index ``` * 把所有被刪掉的檔案救回來: ```bash $ git checkout . Updated 3 paths from the index ``` ### 修改 Commit 紀錄 原本的歷史紀錄是這個樣子: ```bash $ git log --oneline e624291 (HEAD -> master) <update> 7f7ec9c <create> index.html ``` 需要修改**最近一期**的 Commit Message,在 `git commit` 指令後面加上 `--amend` 參數: ```bash $ git commit --amend -m '<update> rename index.html to home.html' [master 51032f3] <update> rename index.html to home.html Date: Thu Nov 2 15:34:16 2023 +0800 1 file changed, 0 insertions(+), 0 deletions(-) rename index.html => home.html (100%) ``` 再次使用 `git log` 指令查詢歷史紀錄: ```bash $ git log --oneline 51032f3 (HEAD -> master) <update> rename index.html to home.html 7f7ec9c <create> index.html ``` 雖然只是修改 Commit Message,其它檔案內容都沒有修改,但對 Git 來說,因為 **Commit Message 的內容**改變了,所以 Git 會重新計算並產生一個全新的 Commit。 :::danger **注意** 盡量不要在已經 Push 出去之後再修改 Commit Message,否則可能會造成其它開發者的困擾。 ::: 如果檔案臨時做了一些小修改,不想再提交一個新的 Commit;或者是需要追加檔案到**最近一期**的 Commit,例如新增一個 `style.css` 檔案到目錄中: ```bash $ touch style.css $ git status On branch master Untracked files: (use "git add <file>..." to include in what will be committed) style.css nothing added to commit but untracked files present (use "git add" to track) ``` 可以使用 `--amend` 參數進行 Commit,將 `style.css` 檔案併入最後一次的 Commit: ```bash $ git add style.css $ git commit --amend -m '<update / create> index.html / style.css' [master c40dda8] <update / create> index.html / style.css Date: Thu Nov 2 15:42:46 2023 +0800 2 files changed, 0 insertions(+), 0 deletions(-) rename index.html => home.html (100%) create mode 100644 style.css ``` 使用 `git log` 指令查詢歷史紀錄: ```bash $ git log --oneline c40dda8 (HEAD -> master) <update / create> index.html / style.css 7f7ec9c <create> index.html ``` ### Git 版本還原 當送出 Commit 之後卻後悔了,有可能是發現了修改的檔案裡有 bugs,因此想要取消這次的 Commit,可以使用 `git reset` 指令。 Reset 這個英文單字的翻譯是「重新設定」,但事實上 `git reset` 指令用中文來說比較像是「前往」,也就是「go to」的概念。因為實際上 `git reset` 並不是真的刪除或是重新設定 Commit,只是「前往」到指定的 Commit,那些看起來好像不見的東西只是暫時看不到,但隨時都可以再撿回來。 使用 `git log` 指令查詢歷史紀錄: ```bash $ git log --oneline 9dcfb12 (HEAD -> master) <create> database.yml in config folder babb78d <create> product.html d2ba818 <create> index.html 9d5b7d1 <create> about.html c49e0a8 <crete> gitignore c40dda8 <update / create> index.html / style.css 7f7ec9c <create> index.html ``` 如果想要拆掉**最後一次**的 Commit,也就是回到前一次 Commit,可以使用「**相對**」或「**絕對**」的做法。 ```bash # 使用相對作法 $ git reset 9dcfb12^ $ git reset master^ $ git reset HEAD^ # 使用絕對做法 $ git reset babb78d ``` ### Reset 的模式 | 模式 | mixed | soft | hard | |-----|-----|-----|-----| | 工作目錄(Untracked) | 不變 | 不變 | 不變 | | 工作目錄(Modified) | 不變 | 不變 | 捨棄 | | 暫存區 | 丟回工作目錄 | 不變 | 捨棄| | 拆分 Commit,從儲存庫被拆出的檔案 | 丟回工作目錄 | 丟回暫存區 | 捨棄 | #### Soft reset  #### Hard reset  ### 忽略不要版本控制的檔案 一些比較機密的檔案,例如:資料庫的存取密碼、AWS 伺服器的存取金鑰;或是一些程式碼編譯的中間檔、暫存檔,不想放在 Git 裡面一起備份,只要在專案目錄裡放一個 `.gitignore` 檔案,並且設定想要忽略的規則就行了。 ```bash # .gitignore.text # 忽略 secret.yml 檔案 secret.yml # 忽略 config 目錄下的 database.yml 檔案 config/database.yml # 忽略所有 db 目錄下附檔名是 .sqlite3 的檔案 /db/*.sqlite3 # 忽略所有附檔名是 .tmp 的檔案 *.tmp ``` :::danger **注意** **`.gitignore`** 檔案設定的規則,只針對在規則設定**之後**有成效,對於在 **`.gitignore`** 檔案設定規則以前,就已經存在的檔案就像既得利益者一樣,這些規則是對他們沒有效果的。 ::: ### 拿到一個新專案的步驟 1. 加入版本控制 `git init` 2. 建立 `.gitignore` 排除需要忽略的檔案,`.gitignore` 檔案也需要加入版控(其他協作者才知道) 3. 使用 `git add .` 將檔案加入版控,因為是**新建立的檔案**,不能直接使用 `git commit -am`(新增的檔案還不在暫存區) 4. 如果是**已經加入版控**的檔案,可以直接使用 `git commit -am` 合併指令 5. 建立 `develop`、`feature` 分支,進行開發作業
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up