Git
資訊系必備的版本管理工具
- 基本介紹
- git 原理
- 基本操作
- 實戰體驗
- 進階操作
大綱
Git 前言
關於這是什麼
為什麼需要版本控制?
- 不小心改到檔案然後按下儲存
- 有雷隊友把 code 整個刪掉了
- 大專案出 bug 了,想找出是什麼時候開始出了問題
為什麼需要版本控制?
Git 可以做什麼?
Cloud
Local
Cloud
Clone
Local
Repository
Cloud
Pull
Push
Local
Cloud
Local 1
Local 2
Merge
關於「Git」這個名字
- 可以發音唸出的隨機三個字母組合,而且並未被實際用在任何 UNIX 指令上
- Global Information Tracker,如果想硬湊的話
- 愚蠢的、鄙視和卑鄙的,來自字典的解釋
關於「Git」和「Github」
- Github 是一個「線上軟體原始碼代管服務平台」,它會用 Git 來進行版本控制
- 還有其他和 Github 類似的平台,例如 gitlab, bitbucket 等等
基本概念
區域劃分
檔案狀態
基本流程
- 修改 / 新增檔案
- 移到 staging area
- 存到 repository
誰說資訊系不用GUI?
終端機
終端機
Sublime Merge
- 簡潔易懂
- 指令操作
- 功能齊全
- 顯示分支樹
- 免費
下載安裝
官網:https://www.sublimemerge.com
介面
操作
指令
Ctrl + p
Git 基礎
關於自己做事
基礎指令
初始設定
創建倉庫 / 環境設定
創建倉庫
- repository(repo)
- 以 資料夾 為單位
- 讓 git 知道這些內容要被管理
- 初始化 git
儲存位置
Sublime Merge
Sublime Merge
選單 > File > New Repository
環境設定
- 記錄作者資訊
- 調整記錄資訊
- file mode
- 調整環境設置
- 編輯器
e.g. 作者資訊
儲存位置
- Global
~/.gitconfig
-
影響所有 repo
- Local
-
<repo>/.git/config
-
目前所在 repo
-
儲存位置 - Global
儲存位置 - Local
Sublime Merge
Sublime Merge
創建 commit
放上卡車 / 檢查卡車 / 送進倉庫
放上卡車
git add
用途?
- 掌控每個 commit 大小
- 避免把某些資訊丟進倉庫
- 為工作設定斷點
Sublime Merge
檢查卡車
內容資訊
Sublime Merge
modified
untracked
staged
紀錄「改變」
modified
staged
送進倉庫
git commit
Sublime Merge
commit message
成功!
Demo & Lab
- 新增一個 repo
- 做好環境設定
- 新增檔案
a.txt
並放入卡車 - 把卡車送進倉庫
指令 ver.
git init / config / add / status / commit
Review
- 新增倉庫
git init
- 環境設定
git config
- 放上卡車
git add
- 檢查卡車
git status
- 送進倉庫
git commit
創建倉庫
$ git init
$ git init <repo_name>
環境設定
-
$ git config [--global] user.name "<your_name>"
-
$ git config [--global] user.email "<your_email>"
放上卡車
$ git add a.txt
$ git add *.c
$ git add .
$ git add --all
檢查卡車
$ git status
modified
untracked
staged
送進倉庫
$ git commit
$ git commit -m "<message>"
組合技
$ git status
$ git commit
請養成 commit 前檢查 status 的好習慣
Demo
$
指令 time
復原操作
git restore / rm
unstaged
- 剛剛
git add
某些檔案 - 發現這個東西還不能丟到倉庫
- 從 staging area 移除
Sublime Merge
unmodified
- 剛剛修改了一些檔案
- 測試時發現整陀爛光光
- 只能回復到上一個 commit 的狀態
Sublime Merge
Sublime Merge
Demo & Lab
Task: 創建並讓 b.txt
在 staged 跟 modified 間反覆橫跳移動
指令 ver.
git restore / rm
unstage
$ git restore --staged a.txt
$ git restore --staged *.c
第一個 staged
- 新的 repo
- 新增
a.txt
並放入 staging area - 從 staging area 移除
指令
哭阿
git restore
-
git restore
是以 commit 為基底進行復原 - 沒有半個 commit 當然會出事情
怎麼辦
- 下載 Sublime Merge
- 重開一個 repo
- 不管了直接 commit
- ...?
指令
指令
$ git rm --cached a.txt
$ git rm --cached *.c
git rm
- 把檔案從 tracked 直接刪除
-
--cached:
改成變為 untracked
unmodified
$ git restore a.txt
複習
$ git restore [--staged] <file>
$ git rm [--cached] <file>
太多好難記?
Demo
$
指令 time
其他指令
操作歷史
查看歷史 / 刪除歷史 / 暫存桌面 / 回到過去
查看歷史
- commit 順序
- 作者與內容
- 搜尋功能
- 作者
- 時間
- 檔案
Sublime Merge
Sublime Merge
進階搜尋
刪除歷史
- 不小心手殘把錯的東西
git add
- 不小心手殘把錯的東西
git commit
- 完蛋zzz
刪除歷史
- 創造一個新的 commit 修改
-
git log
長很醜 - 強迫症發作
- 做事沒效率
- 專案沒進度
人生沒希望
Sublime Merge
刪除 commit
-
soft
:改成 staged -
mixed
:改成 modified -
hard
:直接刪除
mixed
結果
WARNING !
git reset --hard
是一個十分危險的指令,一旦使用所有進度都會直接消失。請勿搭配酒精使用,並確認無誤再下達。
回到過去
- 做了一堆 commit
- 突然想回到過去看看以前的狀態
Sublime Merge
結果
桌面不乾淨
暫存桌面
- 現在的檔案不足以 commit
- 但又需要清空以放下其他東西
- 暫時存檔之後取出
Sublime Merge
只會儲存 staged
- 先
stage all
- 再
stash
結果
取回暫存 - pop
回歸正題
- 原本想要回到過去被卡住
-
stash
清空桌面 -
pop
之後取回
回到過去
結果
回到現在
修改歷史?
- 先回到過去
- 在歷史紀錄上做不同的修改
- 甚至做出 commit...
結果
Detached HEAD
- 目前 HEAD 所在的位置不屬於任何 branch
- 離開後不容易找到這些 commits
- 謹慎使用
先不要碰
Demo
把桌面清空 回到第一個 commit 再回來並回復桌面
Review
- 查看歷史
git log
- 刪除歷史
git reset
- 暫存桌面
git stash
- 回到過去
git checkout
查看歷史
$ git log
刪除歷史
$ git reset <commit_ID>
$ git reset --soft <commit_ID>
- $ git reset --hard <commit_ID>
回到過去 - 未清空
暫存桌面
$ git add --all
$ git stash
只會暫存 staging area
取回暫存
$ git stash pop
回到過去
$ git checkout <commit_ID>
回到現在
$ git checkout master
git checkout
- git checkout 其實更常用於切換分枝(branch)
- 其中 master 就是分支名稱
- 有時視情況會是 main / 其他名稱
- 下半堂會更詳細介紹
Demo
$
指令 time
隱藏資訊
.gitignore
隱藏資訊
- 機密檔:
secret_key.txt
- 執行檔:
a.exe a.out
- 系統檔:
.vscode/ .DS_Store
- 暫存檔:
a.txt~ b.txt.swp
小心再小心
- 每次都不要 add 到那些檔案
- 不能使用
git add --all
-
git status
很醜 - 強迫症發作
- ...
.gitignore
- 名稱就叫 "
.gitignore"
- 告訴 git 哪些檔案 / 資料夾底下不要紀錄
- 不會被
git status / add
看到
.gitignore
secret_key.txt
*.exe
*.out
.vscode/
.DS_Store
*~
*.swp
Sublime Merge
指令
$ vim .gitignore
結果
影響範圍
-
.gitignore
可以每個資料夾底下都放一個 - 影響範圍會因為位置而有所不同
- 若撰寫前該檔案已 tracked 則不受影響
- 可透過
git rm --cached <file>
變回 untracked
Demo & Lab
Task: 撰寫 .gitignore
使 git 無法追蹤 secret_key.txt
Q & A
$ git checkout break_time !
Git 進階
關於分工合作
分工 = branch
合作 = merge
A
B
版本
+修改
=版本
b
A
B
E
C
D
F
G
branch
merge
g \(\approx\) e + f
b
e
c
f
d
g
branch:分化出不同的分支完成不同的部分
merge:將不同分支的內容整合在一起
branch
some rules:
- 一個 branch 指向一個 commit
- 同個 commit 可以有很多分支,但 Git 同時只會關注(checkout)一個分支
- 每次 commit ,當前的 branch 也會跟著往後移動
A
git checkout main
B
main
A
git checkout main
git commit -m "C"
B
C
main
A
git checkout main
git commit -m "C"
git commit -m "D"
B
C
main
D
A
git checkout main
git commit -m "C"
git commit -m "D"
git checkout <commit ID of B>
B
C
HEAD
main
D
A
git checkout main
git commit -m "C"
git commit -m "D"
git checkout <commit ID of B>
git checkout -b dev
B
C
dev
main
D
A
git checkout main
git commit -m "C"
git commit -m "D"
git checkout <commit ID of B>
git checkout -b dev
git commit -m "E"
B
C
dev
main
D
E
A
git checkout main
git commit -m "C"
git commit -m "D"
git checkout <commit ID of B>
git checkout -b dev
git commit -m "E"
git checkout main
B
C
dev
main
D
E
A
git checkout main
git commit -m "C"
git commit -m "D"
git checkout <commit ID of B>
git checkout -b dev
git commit -m "E"
git checkout main
git commit -m "F"
B
C
dev
main
D
E
F
A
git checkout main
git commit -m "C"
git commit -m "D"
git checkout <commit ID of B>
git checkout -b dev
git commit -m "E"
git checkout main
git commit -m "F"
git checkout dev
B
C
dev
main
D
E
F
A
git checkout main
git commit -m "C"
git commit -m "D"
git checkout <commit ID of B>
git checkout -b dev
git commit -m "E"
git checkout main
git commit -m "F"
git checkout dev
git commit -m "G"
B
C
dev
main
D
E
F
G
merge
some rules:
- A 併進 B ≠ B 併進 A
- A 併進 B:將 branch A 的資訊帶回 B
- 「合併」就可能發生「衝突」(兩邊的意見不合)
main
main
dev
main
dev
main
dev
merge!
int stack[100], top = 0;
void push(int x){
}
int pop(int x){
}
int stack[100], top = 0;
void push(int x){
stack[top++] = x;
}
int pop(){
}
int stack[100], top = 0;
void push(int x){
}
int pop(){
return stack[--top];
}
int stack[100], top = 0;
void push(int x){
stack[top++] = x;
}
int pop(){
return stack[--top];
}
A
git checkout main
B
C
dev
main
D
E
F
D
F
A
git checkout main
git merge dev
B
C
dev
main
D
E
F
G
G
Merge this?
main
dev
main
dev
?
conflict!
conflict
-
兩邊對相同的地方做不同的修改
-
Git 無法自動判斷哪邊該留下
-
需要手動合併
int gcd(int a, int b){
<<<<<<< HEAD
while(b != 0){
int tmp = b;
b = a % b;
a = tmp;
}
return a;
=======
return a % b == 0
? b
: gcd(b, a % b);
>>>>>>> rec
}
int gcd(int a, int b){
}
int gcd(int a, int b){
while(b != 0){
int tmp = b;
b = a % b;
a = tmp;
}
return a;
}
int gcd(int a, int b){
return a % b == 0
? b
: gcd(b, a % b);
}
int gcd(int a, int b){
return a % b == 0
? b
: gcd(b, a % b);
}
<<<<<<< HEAD
合併前的檔案內容
=======
合併進來的分支的檔案內容
>>>>>>> 合併進來的分支名稱
Git 進階
關於異地同步
Sync strategy
push & pull
pull
push
「嘿,現在的版本樹長怎樣」
「嘿,我做了某某某修改」
git pull
for synchronization
A
B
dev
main
C
D
E
F
A
B
dev
main
C
D
E
A
B
dev
main
C
D
E
F
git fetch
A
B
dev
main
C
D
E
F
A
B
dev
main
C
D
E
A
B
origin/dev
origin/main
C
D
E
F
git fetch
git merge
A
B
dev
main
C
D
E
F
A
B
dev
main
C
D
E
A
B
origin/dev
origin/main
C
D
E
F
= git pull
git pull
some rules:
- (fetch)會將所有遠端的 commit 都抓下來
- (merge)只會 merge 當前的 branch
- 有 merge 就可能有 conflict 發生
A
B
dev
main
C
D
E
F
A
B
dev
main
C
D
E
G
origin/dev
A
B
main
dev
C
D
E
F
main
C
dev
G
A
B
origin/main
C
D
E
F
A
B
main
C
D
E
F
main
C
G
A
B
origin/main
C
D
E
F
H
dev
dev
origin/dev
git push
for contribution
A
B
dev
main
C
D
E
A
B
dev
main
C
D
E
F
A
B
dev
main
C
D
E
F
A
B
main
C
D
E
A
B
dev
main
C
D
E
F
git push origin dev
A
B
dev
main
C
D
E
F
git push <remote> <branch>
some rules:
- 將本地的 branch 併進遠端的 branch
- 只會動到指定的 branch
- 必須是 fast-forward,否則會被 reject
- remote 永遠是對的
git push origin dev
A
B
main
C
D
E
F
dev
A
B
main
dev
C
D
E
G
To github.com:username/reponame.git
! [rejected] dev -> dev (fetch first)
error: failed to push some refs to 'github.com:username/reponame.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.
E
F
origin/dev
G
dev
H
E
F
origin/dev
G
dev
E
F
dev
pull
merge
push
E
G
dev
E
F
G
dev
H
> git push origin dev
To github.com:username/reponame.git
! [rejected] dev -> dev (fetch first)
error: failed to push some refs to 'github.com:username/reponame.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.
> git pull
...
Auto-merging <filename>
CONFLICT (content): Merge conflict in <filename>
Automatic merge failed; fix conflicts and then commit the result.
> git add .
> git commit -m "merged"
> git push
Demo & Lab
Task:合作開發 1A2B 猜數字遊戲
background
-
三個人一組
-
透過 Git / GitHub 協作開發猜數字遊戲
For each group...
A
B
C
A
B
C
A
B
C
3.
新增協作者
Repo 主頁
> Settings
> Collaborators
> Add people
A
B
C
local
ssh-keygen
cat ~/.ssh/id_rsa.pub
github
4.
把 repo clone 到本機上
github.com/username/repo-name
A
B
C
git@github.com:username/repo-name.git
cd /path/to/any/directory
git clone <repo_url>
cd repo-name
5.
A 先打個基本設定
A
B
C
*.exe
.gitignore
#include<stdio.h>
#include<stdlib.h>
int main()
{
}
main.c
git add .
git commit -m "template"
git push origin main
5
main
6.
BC 把 A 的修改 pull 下來
A
B
C
git pull
5
main
7.
B 強迫症發作,改了 A 的 coding style
A
B
C
#include<stdio.h>
#include<stdlib.h>
int main(){
}
main.c
git add .
git commit -m "coding style!!"
git push origin main
5
main
7
main
8.
C 寫了一些遊戲基本流程
A
B
C
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#define len 4
void generate(char *s){
// TODO
}
void input(char *s){
// TODO
}
void response(char *s, char *a){
// TODO
}
int main()
{
char ans[10000], str[10000];
generate(ans);
while(true){
input(str);
response(str, ans);
}
printf("You won!\n");
}
main.c
git add .
git commit -m "game flow"
git push origin main
5
7
main
To github.com:username/repo-name.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'github.com:username/repo-name.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.
void response(char *s, char *a){
// TODO
}
<<<<<<< HEAD
int main()
{
char ans[10000], str[10000];
generate(ans);
=======
int main(){
>>>>>>> <commit ID>
while(true){
input(str);
response(str, ans);
git pull
void response(char *s, char *a){
// TODO
}
int main(){
char ans[10000], str[10000];
generate(ans);
while(true){
input(str);
response(str, ans);
git add .
git commit -m "merge!"
git push origin main
C
5
7
origin/main
8
main
8
5
7
origin/main
8
main
5
7
main
5
7
8
8
main
pull
merge
push
5
8
main
9.
分工:
A 維護 main
B 處理 input
C 處理 output
A
B
C
5
7
8
8
main
input
output
git pull
git checkout -b "input"
B
C
git pull
git checkout -b "output"
10.
A 簡單加上了分隔線
A
B
C
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#define len 4
void generate(char *s){
// TODO
}
void input(char *s){
// TODO
}
void response(char *s, char *a){
// TODO
}
int main(){
char ans[10000], str[10000];
generate(ans);
while(true){
printf("==========\n");
input(str);
response(str, ans);
}
printf("You won!\n");
}
main.c
git add .
git commit -m "add separator"
git push origin main
5
7
8
8
main
input
output
10
main
11.
B 完成了數字生成和使用者輸入
A
B
C
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<time.h>
#include<string.h>
#define len 4
bool valid(char *s){
if(strlen(s) != len) return false;
for(int i = 0; i < len; i++){
for(int j = 0; j < len; j++){
if(i != j && s[i] == s[j]){
return false;
}
}
}
return true;
}
void generate(char *s){
printf("Set the answer (0 for auto generation): ");
scanf("%s", s);
if(s[0] == '0' && strlen(s) == 1){
srand(time(0));
while(true){
for(int i = 0; i < len; i++){
s[i] = '0' + rand() % 10;
}
s[len] = '\0';
if(valid(s)){
break;
}
}
}else{
if(!valid(s)){
printf("Invalid input\n");
generate(s);
}else{
printf("\033[1A\x1b[2K");
}
}
}
void input(char *s){
printf("Guess a 4-digit number: ");
scanf("%s", s);
}
void response(char *s, char *a){
// TODO
}
int main(){
char ans[10000], str[10000];
generate(ans);
while(true){
input(str);
if(!valid(str)){
printf("Invalid input\n");
}else{
response(str, ans);
}
}
printf("You won!\n");
}
main.c
git add .
git commit -m "finish input"
git push origin input
5
7
8
8
input
output
10
main
11
input
12.
C 完成回覆函數和勝利判斷
A
B
C
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<string.h>
#define len 4
void generate(char *s){
// TODO
}
void input(char *s){
// TODO
}
bool win(char *s, char *a){
return strcmp(s, a) == 0;
}
void response(char *s, char *a){
int cnt_a = 0, cnt_b = 0;
for(int i = 0; i < len; i++){
if(s[i] == a[i]){
cnt_a++;
}else{
for(int j = 0; j < len; j++){
if(s[i] == a[j]){
cnt_b++;
break;
}
}
}
}
if(cnt_a) printf("%dA", cnt_a);
if(cnt_b) printf("%dB", cnt_b);
printf("\n");
}
int main(){
char ans[10000], str[10000];
generate(ans);
while(true){
input(str);
if(win(str, ans)){
break;
}else{
response(str, ans);
}
}
printf("You won!\n");
}
main.c
git add .
git commit -m "finish output"
git push origin output
5
7
8
8
10
main
11
input
12
output
output
13.
身為 main branch 的維護者, A 要負責將 feature branches 合併進 main
A
B
C
git pull
git merge origin/input
5
7
8
8
10
main
11
input
12
output
13
main
14.
身為 main branch 的維護者, A 要負責將 feature branches 合併進 main
A
B
C
git merge origin/output
5
7
8
8
10
11
input
12
output
13
main
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
<<<<<<< HEAD
#include<time.h>
=======
>>>>>>> output
#include<string.h>
#define len 4
bool valid(char *s){
if(strlen(s) != len) return false;
for(int i = 0; i < len; i++){
for(int j = 0; j < len; j++){
if(i != j && s[i] == s[j]){
return false;
}
}
}
return true;
}
void generate(char *s){
printf("Set the answer (0 for auto generation): ");
scanf("%s", s);
if(s[0] == '0' && strlen(s) == 1){
srand(time(0));
while(true){
for(int i = 0; i < len; i++){
s[i] = '0' + rand() % 10;
}
s[len] = '\0';
if(valid(s)){
break;
}
}
}else{
if(!valid(s)){
printf("Invalid input\n");
generate(s);
}else{
printf("\033[1A\x1b[2K");
}
}
}
void input(char *s){
printf("Guess a 4-digit number: ");
scanf("%s", s);
}
bool win(char *s, char *a){
return strcmp(s, a) == 0;
}
void response(char *s, char *a){
int cnt_a = 0, cnt_b = 0;
for(int i = 0; i < len; i++){
if(s[i] == a[i]){
cnt_a++;
}else{
for(int j = 0; j < len; j++){
if(s[i] == a[j]){
cnt_b++;
break;
}
}
}
}
if(cnt_a) printf("%dA", cnt_a);
if(cnt_b) printf("%dB", cnt_b);
printf("\n");
}
int main(){
char ans[10000], str[10000];
generate(ans);
while(true){
printf("==========\n");
input(str);
<<<<<<< HEAD
if(!valid(str)){
printf("Invalid input\n");
=======
if(win(str, ans)){
break;
>>>>>>> output
}else{
response(str, ans);
}
}
printf("You won!\n");
}
14.
身為 main branch 的維護者, A 要負責將 feature branches 合併進 main
A
B
C
git add .
git commit -m "merge output"
5
7
8
8
10
11
input
12
output
13
main
14
main
5
7
8
8
10
11
input
12
output
13
14
main
git push origin main
gcc game.c -o game.exe
./game.exe
-
更系統化的分工合作
-
一個團隊的紀律
-
avoid conflicts!!
5
7
8
8
10
11
input
12
output
13
14
main
大家都想在 main branch 上做事
\(\Rightarrow\) 衝突多、merge 多、歷史記錄亂
每個人在自己的 branch 上做事
彼此不互相干擾
- 同一個 branch 盡量少人操作
- main 作為正式版本
- merge 進主要分支前要 code review
Feature Branching
後話
git rebase
A
B
C
A
B
C
A
B
C
B
C
A
B
C
git merge
git rebase
A
B
C
A
B
C
A
B
C
B
D
A
B
C
git merge
git rebase
git merge
git rebase
A
B
C
B
D
A
B
C
- commit D 幾乎沒有任何意義
- merge 會產生多餘的 commit
- merge 會造成非線性的歷史紀錄
- rebase 就是將 C 的修改重新作用在 B 上面
- rebase 兩邊的 commit 鏈越長,就容易產生越多的 conflict
- rebase 對版本樹結構影像較大
Git
By thomaswang2003
Git
- 533