git 實作練習 2

@ver 0.1.2 @date 2019-02-28 04:16:24 (星期四)

@ver 0.1.1 @date 2019-02-21 19:49:17 (星期四)

這一章要先介紹 git 裡很重要的設定檔, 檔名 .gitignore, 隨著開發時間流轉, 專案會越來越龐大, 塞滿了各式各樣令人驚奇的程式或文字或圖片. 重要的可能是跟加密或密碼有關的檔案, 例如 .key, .pem 密碼檔或 .crt 證書或儲存資料庫密碼的設定檔. 有些是開發過程中必然產生的中間檔, 例如編輯器自動備份的 .bak 或是 C/C++ Compiler 產生的 .o obj 檔, 會跟著程式更新而異動. 有些是換了又換的圖片, 或是曾經在建立資料時要用到卻又曇花一現的 .xml 或 .json, .csv 等內容. 又有些是暫時備而不用的 .log 記錄檔.

以上種種各式各樣的檔案, 必須留在專案工作目錄裡, 以備不時之需, 但又不想時時刻刻記錄到 git 倉庫, 此時就要善用 .gitignore 的專長.

接著會教學如何使用 git log, git diff 工具來抓住各個版本之間的變化; 以及使用 git reset 施行還原大法, 把過去某一段美好時間點的程式碼, 重新呈現出來.

  1. .gitignore 忽略文件清單

    .gitignore 指定 git 刻意要忽略索引追踪的文件. 先前 git 已經索引追踪的文件不受影響, 如果也要把它從暫存區(索引)刪除, 可用 git rm --cached 檔名

    一般忽略文件的原則如下 :

    • 作業系統自動產生的文件, 目錄資訊, 縮略圖 ... 等.
      # windows 系統
      Thumbs.db
      ehthumbs.db
      Desktop.ini
      # macOS 系統
      .DS_Store
    • 編譯產生的中間檔、可執行檔案, 自動產生的備份, 記錄 ... 等
      *.o
      *.a
      *.class
      *.exe
      *.bak
      *.log
    • 跟加密或密碼儲存有關的檔案
      .env
      *.key
      *.pem
      *.crt
    • 其他任何不想 git 的目錄或檔案
      my_key/
      my_birthday_data.txt
    • 若為空的子目錄, 此目錄會被忽略, 除非至少 touch 一個檔案在目錄之下, 例如 touch /res1/.gitkeep
  2. .gitignore 忽略文件清單的語法規則

    什麼是 glob pattern

    所謂的 glob pattern 是指 shell 所使用的簡化版本的正則表達式.

    檔名及目錄可以使用標準的 glob pattern (以下規則簡稱為模式) 匹配

    問號 ? 只匹配一個任意字符

    星號 * 匹配零個或多個任意字符.

    [abc] 匹配任何一個列在方括號中的字符 ( 這個例子匹配一個 a, 或匹配一個 b, 或匹配一個 c ); 如果在方括號中使用 - 短划線 分隔兩個字符, 表示所有在這兩個字符範圍內的都可以匹配 [0-9] 表示匹配所有 0 到 9 的數字.

    使用雙星號 \*\* 表示匹配任意中間目錄, 比如 a/**/b 可以匹配 a/ba/x/b 或 a/x/y/b 等.

    忽略文件語法規則簡化版本 :

    • 空行或 # 開頭表示註解
      # Windows 作業系統相關
      # 自動產生
      # 重要個人檔案, 不能備份
    • 指定模式匹配檔案或目錄
      my_key/
      my_*day_data.txt
    • 在前面添加正斜線 / 來匹配頂層目錄, 下面的例子表示 /datas 裡的所有子目錄及檔案全部忽略, 但如果是其他子目錄裡有相同名稱, 例如 public/datas/ 則不會被忽略
      /datas
    • 在後面添加正斜線 / 來匹配各層級目錄, 例如 logs/ 表示所有各個層級的目錄裡, 只要有 logs 目錄名稱就把它忽略
      logs/
    • 以驚嘆號 ! 表示不能忽略, 通常要加在最後
      !/todo.bak/1.md.bak

    忽略文件語法規則完整版本 : (翻譯自原文使用手冊)

    1. { style=list-style-type:decimal; } 空行 沒有任何作用, 可用來分段, 以便於閱讀.
    2. # 開頭表示註解, 如果檔名第一字元剛好是 #, 就用老方法, 在前方加入反斜線 \#.
    3. 除非使用反斜線引用尾隨空格 \空格, 否則將忽略行末尾隨空格.
    4. 前綴 驚嘆號 ! 表示不忽略此目錄或文件, 通常要加在最後一段, 讓先前已匹配為忽略再次包含進來.
    5. 如果檔名剛好有 ! 開頭, 而且要忽略, 例如 !important!.txt, 也是用老方法, 在前方加入反斜線 \!important!.txt 避免被誤判為前一個不忽略的規則.
    6. 模式開頭為 正斜線 / , 它只會從頂層目錄開始來匹配. 假如設為 /datas, 則頂層目錄名為 datas 會忽略(也包括它裡面的所有檔案), 但如果是其他非頂層目錄裡的子目錄有相同名稱, 例如 /public/datas/ 則不會被忽略. 或者設為 /docs/security.md 則只有此路徑的檔案會忽略, 而其他目錄下, 如 /public/docs/security.md, 不會忽略.
    7. 模式開頭為 正斜線 / , 例如, /*.c 匹配頂層目錄的 cat-file.c 但不匹配 /src/sha1.c.
    8. 模式以 正斜線 / 結尾, 它會找所有目錄來匹配. 換句話說, foo/ 將匹配目錄名 foo 和它下層的所有子目錄, 但是不匹配檔名為 foo 的一般檔案或符號鏈接.
    9. 如果模式前後不包含 正斜線 /, git 將其視為一般路徑檔案來忽略, 例如 Desktop.ini, *.bak, /src/hello?.cpp ... 等符合模式的各個路徑下的檔案或指定路徑的檔案
    10. 與完整路徑名匹配的兩個連續星號 ** 具有特殊含義. 在它之後加正斜線 **/ 意味匹配所有的路徑, 不管階層. 例如 **/foo 會忽略所有路徑之下有 foo 的路徑或檔名. 又如 **/foo/bar 則會忽略所有 foo 路徑之下有 bar 的路徑或檔名.
    11. ** 之前加正斜線 /** 匹配目錄裡的所有內容. 例如 foo/** 匹配 foo 目錄下 所有子目錄及檔案, 具有無限深度.
    12. ** 前後加正斜線 /**/ 匹配零個或多個目錄. 例如 a/**/b 匹配 a/b, a/x/b, a/x/y/b ...等, 無限延伸.
  3. .gitignore 忽略文件清單範例及規則說明

    以下表格逐行說明 .gitignore 的引用語法規則

    範例 規則 說明
    # 這是一個範例檔案 2 註解
    1 空行, 用來分段
    # Laravel 相關 2 註解
    .env 9 前後不含正斜線, 任何目錄下的相同檔名或目錄名
    /public/storage 6 開頭為正斜線, 頂層目錄下的 /public/storage 檔名或目錄名
    /private/*.key 6,7 開頭為正斜線, 頂層目錄下的 /private/ 裡的任何 .key 檔案
    /vendor 6 開頭為正斜線, 頂層目錄下的 /vendor 檔名或目錄名
    logs/ 8 結尾為正斜線, 任何目錄下的 logs 目錄名
    1 空行, 用來分段
    # Windows 作業系統相關 2 註解
    Thumbs.db 9 前後不含正斜線, 任何目錄下的相同檔名或目錄名
    ehthumbs.db 9 前後不含正斜線, 任何目錄下的相同檔名或目錄名
    Desktop.ini 9 前後不含正斜線, 任何目錄下的相同檔名或目錄名
    1 空行, 用來分段
    # Mac osx 作業系統相關 2 註解
    .DS_Store 9 前後不含正斜線, 任何目錄下的相同檔名或目錄名
    .DS_Store? 9 前後不含正斜線, 任何目錄下的類似檔名或目錄名
    ~* 9 前後不含正斜線, 任何目錄下, 以 ~ 開頭, 備份檔或鎖定檔或目錄名
    *~ 9 前後不含正斜線, 任何目錄下, 以 ~ 結尾, 備份檔檔名或目錄名
    .*.swp 9 前後不含正斜線, 任何目錄下, 以 . 開頭 .swp 結尾, vi 的編輯中鎖定檔
    1 空行, 用來分段
    # GCC compiler 2 註解
    *.[oa] 9 前後不含正斜線, 任何目錄下的 .o (c 的物件檔) 或 .a (c 的執行檔) 檔名或目錄名
    1 空行, 用來分段
    # 其他 2 註解
    *.bak 9 前後不含正斜線, 任何目錄下, 文件備份檔名或目錄名
    *.log 9 前後不含正斜線, 任何目錄下, .log 記錄檔名或目錄名
    .*.marks 9 前後不含正斜線, 任何目錄下, .marks 結尾, 備份檔名或目錄名
    **/.sass-cache 10 任何目錄下, SCSS 快取目錄
    **/.sass-cache/* 10 任何目錄下, SCSS 快取目錄下的所有檔案
    1 空行, 用來分段
    # 特例 2 註解
    \#*\# 2,9 不是註解, # 開頭, # 結尾, 未命名備份檔
    !/todo.bak/todo.md.bak 4 驚嘆號開頭, 此檔案不能忽略, 特別注意, 前面有設定 *.bak, 所以這個要放在它之後, 才能正常運作
  4. 實際測試 .gitignore

    回到上次實作練習的目錄下, 我們實際產生一些檔案來 ignore 一下

    現在建立第一個 .gitignore 檔案

    命令

    vi .gitignore

    .gitignore 內容

    [alex@nvda /tmp/prj1 ]$ cat .gitignore
    # 第一個 .gitignore
    
    *.bak
    private*
    logs/
    /my-data

    .gitignore 內容對照說明(檔案每列的模式之後裡不能有 #)

    *.bak                       # 任何子目錄, 所有名稱以 .bak 結尾的檔案或目錄
    private*                    # 任何子目錄, 所有名稱以 private 開頭的檔案或目錄
    logs/                       # 任何子目錄, 目錄名稱為 logs
    /my-data                    # 頂層目錄下的 my-data 目錄

    使用 RSA 1024 位元加密,產出公鑰 public_1024.pem 與私鑰 private_1024.key

    命令

    openssl genrsa -out private_1024.key 1024
    openssl rsa -in private_1024.key -out public_1024.pem -outform PEM -pubout
    ls -a
    git st -s

    執行結果

    [alex@nvda /tmp/prj1 ]$ openssl genrsa -out private_1024.key 1024
    Generating RSA private key, 1024 bit long modulus (2 primes)
    .........+++++
    ......+++++
    e is 65537 (0x010001)
    [alex@nvda /tmp/prj1 ]$ openssl rsa -in private_1024.key -out public_1024.pem -outform PEM -pubout
    writing RSA key
    [alex@nvda /tmp/prj1 ]$ rm -fr logs
    [alex@nvda /tmp/prj1 ]$ ls -a
    .  ..  .git  .gitignore  index.php  private_1024.key  public_1024.pem  README.md
    [alex@nvda /tmp/prj1 ]$ git st -s
    ?? .gitignore
    ?? public_1024.pem
    [alex@nvda /tmp/prj1 ]$

    觀察檔案的變化後, 私鑰 private_1024.key 這個檔案, 並沒有在 git st -s 的內容裡, 顯然, .gitignore 符合我們的期待, 接著測試其他項目

    命令

    mkdir public
    mkdir public/logs
    touch public/main.css
    touch public/logs/1.log
    touch public/logs/2.log
    tree public

    執行結果

    [alex@nvda /tmp/prj1 ]$ mkdir public
    [alex@nvda /tmp/prj1 ]$ mkdir public/logs
    [alex@nvda /tmp/prj1 ]$ touch public/main.css
    [alex@nvda /tmp/prj1 ]$ touch public/logs/1.log
    [alex@nvda /tmp/prj1 ]$ touch public/logs/2.log
    [alex@nvda /tmp/prj1 ]$ tree public
    public
    ├── logs
    │   ├── 1.log
    │   └── 2.log
    └── main.css
    
    1 directory, 3 files
    [alex@nvda /tmp/prj1 ]$

    命令

    git st -s
    git add .
    git st -s

    執行結果

    [alex@nvda /tmp/prj1 ]$ git st -s
    ?? .gitignore
    ?? public/
    ?? public_1024.pem
    [alex@nvda /tmp/prj1 ]$ git add .
    [alex@nvda /tmp/prj1 ]$ git st -s
    A  .gitignore
    A  public/main.css
    A  public_1024.pem
    [alex@nvda /tmp/prj1 ]$

    public 目錄裡的 logs 子目錄及其下的所有檔案, 沒有被 git 索引追踪, 但其他檔案 main.css 有被 git 索引追踪, 也符合期待

    命令

    mkdir my-data
    mkdir public/my-data
    touch my-data/1st.md
    touch public/my-data/2nd.md
    git st -s
    git add .
    git st -s

    執行結果

    [alex@nvda /tmp/prj1 ]$ mkdir my-data
    [alex@nvda /tmp/prj1 ]$ mkdir public/my-data
    [alex@nvda /tmp/prj1 ]$ touch my-data/1st.md
    [alex@nvda /tmp/prj1 ]$ touch public/my-data/2nd.md
    [alex@nvda /tmp/prj1 ]$ git st -s
    A  .gitignore
    A  public/main.css
    A  public_1024.pem
    ?? public/my-data/
    [alex@nvda /tmp/prj1 ]$ git add .
    [alex@nvda /tmp/prj1 ]$ git st -s
    A  .gitignore
    A  public/main.css
    A  public/my-data/2nd.md
    A  public_1024.pem
    [alex@nvda /tmp/prj1 ]$

    從最後的 git st -s 結果得知, public/my-data/2nd.md 有被 git 索引追踪, 但最頂層的 my-data/ 沒有被 git 索引追踪, 這樣的結果也是正確的

    命令

    mkdir 190228.bak
    touch 190228.bak/a1.txt
    touch 190228.bak/b2.md
    touch public/c3.bak
    touch public/d4.js
    git st -s

    執行結果

    [alex@nvda /tmp/prj1 ]$ mkdir 190228.bak
    [alex@nvda /tmp/prj1 ]$ touch 190228.bak/a1.txt
    [alex@nvda /tmp/prj1 ]$ touch 190228.bak/b2.md
    [alex@nvda /tmp/prj1 ]$ touch public/c3.bak
    [alex@nvda /tmp/prj1 ]$ touch public/d4.js
    [alex@nvda /tmp/prj1 ]$
    [alex@nvda /tmp/prj1 ]$ git st -s
    A  .gitignore
    A  public/main.css
    A  public/my-data/2nd.md
    A  public_1024.pem
    ?? public/d4.js
    [alex@nvda /tmp/prj1 ]$

    子目錄 190228.bakpublic/c3.bak 檔案, 沒有被 git 索引追踪, 因為它們都符合 *.bak 的規則, 只有最後的 public/d4.js 有被 git 索引追踪, 完全沒問題.

    實測結束, 未來你們再根據自己開發進行中的狀態隨時更新 .gitignore 檔案

    命令

    git add .
    git commit -m '實測 .gitignore 完成'
    git log

    執行結果

    [alex@nvda /tmp/prj1 ]$ git add .
    [alex@nvda /tmp/prj1 ]$ git commit -m '實測 .gitignore 完成'
    [master cd002dc] 實測 .gitignore 完成
     5 files changed, 12 insertions(+)
     create mode 100644 .gitignore
     create mode 100644 public/d4.js
     create mode 100644 public/main.css
     create mode 100644 public/my-data/2nd.md
     create mode 100644 public_1024.pem
    [alex@nvda /tmp/prj1 ]$
    [alex@nvda /tmp/prj1 ]$ git log
          1 commit cd002dc3023c214120b3eb9e50a177d28a66fb26
          2 Author: alex <alex@forblind.org.tw>
          3 Date:   Fri Mar 1 10:44:21 2019 +0800
          4
          5     實測 .gitignore 完成
          6
          7 commit f2ed5d757c9a81de0d1ccd601af1e4a6a48333e4
          8 Author: alex <alex@forblind.org.tw>
          9 Date:   Tue Feb 19 16:24:19 2019 +0800
         10
         11     第一次提交到 git repo
    (END)

    命令

    tree -fU

    執行結果

    [alex@nvda /tmp/prj1 ]$ tree -fU
    .
    ├── ./private_1024.key
    ├── ./public_1024.pem
    ├── ./my-data
    │   └── ./my-data/1st.md
    ├── ./190228.bak
    │   ├── ./190228.bak/a1.txt
    │   └── ./190228.bak/b2.md
    ├── ./public
    │   ├── ./public/d4.js
    │   ├── ./public/my-data
    │   │   └── ./public/my-data/2nd.md
    │   ├── ./public/c3.bak
    │   ├── ./public/main.css
    │   └── ./public/logs
    │       ├── ./public/logs/2.log
    │       └── ./public/logs/1.log
    ├── ./index.php
    └── ./README.md
    
    5 directories, 13 files
    [alex@nvda /tmp/prj1 ]$
  5. One More Thing 萬一改了 .gitignore

    每接到一次任務就會改變一點命運, 隨著開發過程千變萬化. 假設有一個套件會產生 cache 目錄及檔案, 在還沒測試完成前, 沒注意到, 就把這些檔案都加入暫存區(索引).

    我們先手動模擬建置, 並加入暫存區(索引)

    命令

    mkdir cache
    touch cache/x1.php
    touch cache/x2.php

    執行結果

    [alex@nvda /tmp/prj1 ]$ mkdir cache
    [alex@nvda /tmp/prj1 ]$ touch cache/x1.php
    [alex@nvda /tmp/prj1 ]$ touch cache/x2.php
    [alex@nvda /tmp/prj1 ]$ 

    命令

    git add .
    git st -s

    執行結果

    [alex@nvda /tmp/prj1 ]$ git add .
    [alex@nvda /tmp/prj1 ]$ git st -s
    A  cache/x1.php
    A  cache/x2.php
    [alex@nvda /tmp/prj1 ]$ 

    此刻 cache/ 裡的所有檔案都暫存區(索引), 但還沒 commit, 沒關係, 先修改 .gitignore , 免得以後又發生相同錯誤

    命令

    vi .gitignore
    cat .gitignore
    git st -s

    執行結果

    [alex@nvda /tmp/prj1 ]$ vi .gitignore       # 在最後加一行 cache/
    [alex@nvda /tmp/prj1 ]$ cat .gitignore
    # 第一個 .gitignore
    
    *.bak
    private*
    logs/
    /my-data
    cache/
    [alex@nvda /tmp/prj1 ]$ git st -s
     M .gitignore
    A  cache/x1.php
    A  cache/x2.php
    [alex@nvda /tmp/prj1 ]$ 

    觀察狀態, 看到 .gitignore 已修改, 但原先的 cache/x1.php, cache/x2.php 都仍然存在暫存區(索引). 現在要施法術 git rm --cached 把它們兩個移出暫存區(索引).

    命令

    git rm --cached cache/x1.php
    git rm --cached cache/x2.php
    git st -s
    git add .
    git commit -m '從暫存區(索引)移除不要追踪的檔案'

    執行結果

    [alex@nvda /tmp/prj1 ]$ git rm --cached cache/x1.php
    rm 'cache/x1.php'
    [alex@nvda /tmp/prj1 ]$ git rm --cached cache/x2.php
    rm 'cache/x2.php'
    [alex@nvda /tmp/prj1 ]$ git st -s
     M .gitignore
    [alex@nvda /tmp/prj1 ]$ git add .
    [alex@nvda /tmp/prj1 ]$ git commit -m '從暫存區(索引)移除不要追踪的檔案'
    [master 821764e] 從暫存區(索引)移除不要追踪的檔案
     1 file changed, 1 insertion(+)
    [alex@nvda /tmp/prj1 ]$

    最後觀察狀態, 只剩下 .gitignore 顯示為已修改, 這就我們所要的.

  6. 本章總結

    本章使用 git 命令總結

    .gitignore                          # 忽略文件清單
    git rm --cached <filename>          # 將此檔案從暫存區(索引)移走