Git <整>

  内容主要整理自廖雪峰的官方网站,如果读后有收获可以去链接网站扫码打赏支持一下。

Git

第一节 什么是Git

  Git是一款开源的分布式版本控制系统。分布式即多人多用户,通过拷贝镜像的方式运转,版本控制即对项目的历史版本记录。Git最早来源于Linux的作者Linus,通过C语言开发,后来的GitHub则专门为开源项目提供免费的Git存储服务。

  CVS和SVN都是集中式的版本控制系统。

集中式和分布式的区别:

  集中式的版本库存放于中央服务器,开发者需要从服务器获取最新版本,然后将更新内容发送给服务器。所以集中式要求必须要在联网环境下才能工作。分布式系统则没有中央服务器,每台用户电脑都有完整的版本库,所以不需要联网环境,在修改时把各自修改的内容互相推送来进行协作。通常会有一台机器充当”中央服务器”,方便记录所有人的修改。


第二节 安装

  根据自己的操作系统(Linux,Unix,Mac,Windows等)进行安装,具体过程不贴,网上可以随处找到。


第三节 版本库

  版本库(repository),即项目仓库。可以理解为一个盒子或者目录,里面所有文件都会被Git管理,所以每个文件的各种操作都会被记录下来,可以随时追踪其历史,并还原某个时间点所处的状态。

添加、提交、查看

git init

  此命令可以把当前目录作为Git可以管理的仓库。执行后会多出.git目录,用来跟踪管理版本库,默认隐藏。

  所有的版本控制系统只能跟踪文本文件的改动,如txt,html,java等,可以记录文件中所有文本的变动。但对于图片和视频这些二进制文件,系统无法记录具体的变化(Word文件是二进制格式,所以无法跟踪其文本变更)。

git add readme.txt

  在目录下创建readme文件,然后执行上述命令,告诉Git将文件添加到仓库。

git commit -m “add a new txt”

  通过git commit指令告诉Git,把文件提交给仓库。-m后跟提交说明,可多次add添加,一次统一提交commit。

git status

  修改readme文件内容,再执行上述指令,返回信息表示文件已修改但未提交

  再一次执行git add,然后git status,发现返回信息更新,表示文件准备提交,最后git commit。然后git status,发现目录没有需要提交的文件。

git diff

  如果文件修改,可以通过上述指令查看修改内容。

版本回退

git log

  通过上述指令,可以查看操作日志,

git reset –hard HEAD^

git reset –hard 1094a

  git reset指令回退目录到指定版本,版本号不需要输入完整,Git会自动查询匹配

git reflog

  可以通过git reflog指令查看每次操作,而回退版本只是修改当前版本为旧版本,并更新文件,但不会影响被覆盖的版本,仍可以随时切换回。


第四节 工作区和暂存区

  工作区就是开发人员PC中可以看到的文件目录,工作区有一个隐藏目录.git,是Git的版本库

  版本库内有一块暂存区state,git add指令就是将文件修改添加到暂存区。而git commit则是将暂存区内容提交到当前分支。Git会默认自动创建一个master分支,所以此时会将修改内容提交到master分支。

文件修改

  Git专注于修改而不是文件,可以通过以下案例来了解相关内容。

  尝试执行以下指令顺序。

  1. 修改文件
  2. git add xx
  3. git status
  4. 再修改文件
  5. git commit -m “git tracks changes”
  6. git status

  会发现后一次修改并未提交,信息如下。

1
2
3
4
5
6
7
8
9
$ git status
On branch master
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: t.txt

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

  原因是在第一次修改后,执行了git add到暂存区,然后再进行二次修改,再提交时把暂存区内容提交,但二次修改还未加入暂存区。所以修改后必须要先add再提交才会有效果。

撤销修改

  1. 修改文件,添加一行文字
  2. add,commit
  3. 删除此行
  4. git status
1
2
3
4
5
6
7
8
On branch master
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: t.txt

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

git checkout –

  git checkout指令表示可以撤销修改,分为两种情况:1.文件修改后还未add到暂存区,撤销后就和版本库一致。2.文件已添加到暂存区,又做了修改,撤销修改就回到add后的状态。总之就是让此文件回到最近一次commit或add的状态。

git reset HEAD

  通过此指令可以把暂存区的修改撤销,重新放回工作区。

  如果想回退已经commit的修改,则需要通过前文的版本回退。

删除文件

  先删除文件,再git status。

1
2
3
4
5
6
7
8
9
$ git status
On branch master
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

deleted: t.txt

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

  Git发现文件被删除,所以我们通过rm删除,并commit,使文件从版本库也删除。

git rm

  还有可能删除了,可以通过checkout恢复。


第五节 远程仓库

  实际开发中,我们需要一台服务器作为远程仓库来备份版本库以及作为用户的协调中枢。而GitHub就是提供Git仓库托管服务的网站。

  注册账号过程跳过。本地Git仓库和GitHub远程仓库是通过SSH加密传输的,所以需要配置密钥。

$ ssh-keygen -t rsa -C “youremail@example.com

  可以通过上述命令创建SSH Key,然后在用户文档,找到.ssh目录,会有id_rsa和id_rsa.pub两个文件,即SSH Key的密钥对,pub表示公钥。然后在github网站配置SSH Keys。

  然后可以在GitHub上创建一个远程仓库。然后复制SSH地址:Clone with SSH

$ git remote add origin git@github.com:xx1/xx2

  通过此命令可以关联本地仓库和远程仓库,xx1是用户名,xx2为项目名。Git默认会给远程库起名origin。

$ git push -u origin master

$ git push origin master

  通过此命令把本地库所有内容推送到远程库。由于远程库是空的,所以首次推送添加了-u参数,Git不但会把本地master分支内容推送到远程新的master分支,还会将他们关联,以后的推送或拉取就可以简化命令。

$ git clone git@github.com:xx1/xx2

  上面是先有本地库再开启远程库,此命令则是先有远程库时克隆其内容到本地。


第六节 分支管理

  Git的分支管理要远远优秀于SVN。

Git分支图

  master分支即主分支,可以把Head看作指针,HEAD指向当前分支,而分支则指向提交。每个点就是一次提交,所以每次commit就会推动分支前进一步。新的分支则处在一条新的时间线,在平时各个分支间没有任何关系。

创建与合并分支

  Git创建分支效率很高,创建新的分支指针,再更新HEAD的指向即可。当一个分支上任务结束,则需要合并到主分支上,合并就是matser直接指向当前提交,就完成了合并工作。合并之后可以删除没用的分支。

$ git checkout -b dev

$ git branch dev

$ git checkout dev

  创建并切换到dev分支,-b参数表示创建并切换,等价于后两条指令。

$ git branch

  查看当前分支。

$ git checkout master

  切换回master分支。

$ git merge dev

  合并指定分支到当前分支。

$ git branch -d dev

  合并后删除分支。

$ git switch -c dev

$ git switch matser

  新版本推荐用switch指令代替checkout来进行切换,后者容易和撤销修改搞混。-c参数表示创建。

解决冲突

  1. 创建新分支:$ git switch -c feature1
  2. 修改文件内容。
  3. 提交修改到分支:$ git add x.txt $ git commit -m “feature1 submit”
  4. 切换到master分支(Git提醒当前master分支比origin/master分支超前):$ git switch master
  5. 在修改文件上新增内容。
  6. 提交:$ git add x.txt $ git commit -m “master submit”
  7. 执行合并分支请求:$ git merge feature1

  结果Git提示发生冲突,可以通过git status查看冲突文件,也可以直接查看冲突文件。修改文件内容,再提交解决冲突部分。可以删除分支feature1了。

分支管理策略

  通常情况下,合并分支是fast forward模式,但此模式删除分支会失去分支信息。如果禁用fast forward模式,Git就会在merge时生成一个新的commit,这样,从分支历史上就可以看出分支信息。

$ git switch -c dev

  创建并切换dev分支

$ git add readme.txt

$ git commit -m “add merge”

  修改readme.txt,并提交。

$ git switch matser

  切回master

$ git merge –no-ff -m “merge with no-ff” dev

  合并dev分支,–no-ff参数,表示禁用Fast forward

$ git log

  合并后,用git log看看分支历史

  在实际开发中,master分支应该仅用来发布新版本,不能直接在上面干活;干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master上,在master分支发布1.0版本。

Bug分支

  在Git创建和删除分支是比较简洁的操作,所以每个bug都可以通过一个新的临时分支来修复,修复后再合并分支,然后将临时分支删除。

  假定一个工作场景:你接到一个编号101的bug修复任务,很自然地你想创建一个分支issue-101来修复它,但是当前正在dev上进行的工作还没有提交。

$ git status

  预计完成还需1天时间。但是必须在两个小时内修复该bug,怎么办?幸好,Git还提供了一个stash功能,可以把当前工作现场“储藏”起来,等以后恢复现场后继续工作。

$ git stash

  现在用git status查看工作区,就是干净的(除非有没有被Git管理的文件),因此可以放心地创建分支来修复bug。

  首先确定要在哪个分支上修复bug,假定需要在master分支上修复,就从master创建临时分支。

$ git switch master

$ git switch -c issue-101

  然后修复bug,提交。

$ git add readme.txt

$ git commit -m “fix bug 101”

  修复完成后,切换到master分支,并完成合并,最后删除issue-101分支。

$ git switch master

$ git merge –no-ff -m “merged bug fix 101” issue-101

  回到dev分支继续工作。

$ git switch dev

$ git status

  通过git stash list查看之前保存的工作现场

$ git stash list

  找到工作现场后,就要恢复工作现场。一是用git stash apply恢复,但是恢复后stash内容并不删除,需要用git stash drop来删除;另一种方式是用git stash pop,恢复的同时把stash内容也删了。

$ git stash pop

$ git stash list

  如果保存了多次现场,可以指定恢复。

$ git stash apply stash@{0}

  修改BUG是在master上开分支最后合并的,那么当前dev分支一定还存在相同BUG,为了方便操作,Git专门提供了一个cherry-pick命令,让我们能复制一个特定的提交到当前分支。

$ git branch

$ git cherry-pick 4c805e2

  复制提交:4c805e2 fix bug 101。Git自动给dev分支做了一次提交,注意这次提交的commit是1d4b803,它并不同于master的4c805e2,因为这两个commit只是改动相同,但确实是两个不同的commit。用git cherry-pick,我们就不需要在dev分支上手动再把修bug的过程重复一遍。

  既然可以在master分支上修复bug后,在dev分支上可以“重放”这个修复过程,那么直接在dev分支上修复bug,然后在master分支上“重放”行不行?当然可以,不过你仍然需要git stash命令保存现场,才能从dev分支切换到master分支。

Feature分支

  添加一个新功能时,你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以每添加一个新功能,最好新建一个feature分支,在上面进行开发,完成后再合并,最后删除该feature分支。

  假设我们收到了一个开发任务:开发代号为Vulcan的新功能,该功能计划用于下一代星际飞船。

$ git switch -c feature-vulcan

  开发完毕,提交。

$ git add vulcan.py

$ git status

$ git commit -m “add feature vulcan”

  切回dev,准备合并。

$ git switch dev

  正常流程,合并成功然后删除。但是有些特殊情况,比如上级命令因经费不足,新功能必须取消。虽然白干了,但是这个包含机密资料的分支还是必须就地销毁。

$ git branch -d feature-vulcan

1
2
error: The branch 'feature-vulcan' is not fully merged.
If you are sure you want to delete it, run 'git branch -D feature-vulcan'.

  销毁失败。Git友情提醒,feature-vulcan分支还没有被合并,如果删除,将丢失掉修改,如果要强行删除,需要使用大写的-D参数。。

  强行删除。

$ git branch -D feature-vulcan

多人协作

  当从远程仓库克隆时,实际上Git自动把本地的master分支和远程的master分支对应起来了,并且远程仓库的默认名称是origin。

  要查看远程库的信息,就用git remote,或者用git remote -v显示更详细的信息,上面显示了可以抓取和推送的origin的地址。如果没有推送权限,就看不到push的地址。

$ git remote

推送分支

  推送分支就是把该分支上的所有本地提交推送到远程库。推送时,要指定本地分支,这样Git就会把该分支推送到远程库对应的远程分支上。

$ git push origin master

  如果要推送其他分支,比如dev,就改成。

$ git push origin dev

  但是,并不是一定要把本地分支往远程推送,那么,哪些分支需要推送,哪些不需要呢?

  • master分支是主分支,因此要时刻与远程同步;

  • dev分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;

  • bug分支只用于在本地修复bug,就没必要推到远程了,除非老板要看看你每周到底修复了几个bug;

  • feature分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。

抓取分支

  多人协作时,大家都会往master和dev分支上推送各自的修改。

$ git clone git@github.com:xx/xx

  当团队成员从远程库clone时,默认情况下,他只能看到本地的master分支,可以用git branch命令看看。

$ git branch

  当成员需要在dev分支上开发,就必须创建远程origin的dev分支到本地,于是他用这个命令创建本地dev分支。

$ git switch -c dev origin/dev

  现在,他就可以在dev上继续修改,然后时不时地把dev分支push到远程:

$ git add env.txt

$ git commit -m “add env”

$ git push origin dev

解决冲突

  其他成员已经向origin/dev分支推送了他的提交,可能碰巧你也对同样的文件作了修改,并试图推送。

$ git add env.txt

$ git commit -m “add new env”

$ git push origin dev

1
2
3
4
5
6
7
To github.com:xx/xx
! [rejected] dev -> dev (non-fast-forward)
error: failed to push some refs to 'git@github.com:xx/xx'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

  推送失败,因为他人的最新提交和你试图推送的提交有冲突,解决办法也很简单,Git已经提示我们,先用git pull把最新的提交从origin/dev抓下来,然后,在本地合并,解决冲突,再推送。

$ git pull

1
2
3
4
5
6
7
8
9
There is no tracking information for the current branch.
Please specify which branch you want to merge with.
See git-pull(1) for details.

git pull <remote> <branch>

If you wish to set tracking information for this branch you can do so with:

git branch --set-upstream-to=origin/<branch> dev

  git pull也失败了,原因是没有指定本地dev分支与远程origin/dev分支的链接,根据提示设置dev和origin/dev的链接。

$ git branch –set-upstream-to=origin/dev dev

1
Branch 'dev' set up to track remote branch 'dev' from 'origin'.

  再pull。

$ git pull

1
2
3
Auto-merging env.txt
CONFLICT (add/add): Merge conflict in env.txt
Automatic merge failed; fix conflicts and then commit the result.

  这回git pull成功,但是合并有冲突,需要手动解决,解决的方法和分支管理中的解决冲突完全一样。解决后提交,再push:

$ git commit -m “fix env conflict”

$ git push origin dev

Rebase

  多人在同一个分支上协作时,很容易出现冲突。即使没有冲突,后push的开发人员不得不先pull,在本地合并,然后才能push成功。

  所以每次合并再push后,分支变成了这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ git log --graph --pretty=oneline --abbrev-commit

* d1be385 (HEAD -> master, origin/master) init hello
* e5e69f1 Merge branch 'dev'
|\
| * 57c53ab (origin/dev, dev) fix env conflict
| |\
| | * 7a5e5dd add env
| * | 7bd91f1 add new env
| |/
* | 12a631b merged bug fix 101
|\ \
| * | 4c805e2 fix bug 101
|/ /
* | e1e9c68 merge with no-ff
|\ \
| |/
| * f52c633 add merge
|/
* cf810e4 conflict fixed

  看上去很乱,Git的提交历史可以是一条干净的直线吗?Git有一种称为rebase的操作,有人把它翻译成“变基”。

  在和远程分支同步后,我们对hello.py这个文件做了两次提交。用git log命令看看:

1
2
3
4
5
6
7
8
9
10
11
12
$ git log --graph --pretty=oneline --abbrev-commit

* 582d922 (HEAD -> master) add author
* 8875536 add comment
* d1be385 (origin/master) init hello
* e5e69f1 Merge branch 'dev'
|\
| * 57c53ab (origin/dev, dev) fix env conflict
| |\
| | * 7a5e5dd add env
| * | 7bd91f1 add new env
...

  注意到Git用(HEAD -> master)和(origin/master)标识出当前分支的HEAD和远程origin的位置分别是582d922 add author和d1be385 init hello,本地分支比远程分支快两个提交。

  现在我们尝试推送本地分支。

1
2
3
4
5
6
7
8
9
10
$ git push origin master

To github.com:xx/xx
! [rejected] master -> master (fetch first)
error: failed to push some refs to 'git@github.com:xx/xx'
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.

  很不幸失败了,这说明有人先于我们推送了远程分支。按照经验,先pull一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git pull

remote: Counting objects: 3, done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
Unpacking objects: 100% (3/3), done.
From github.com:xx/xx
d1be385..f005ed4 master -> origin/master
* [new tag] v1.0 -> v1.0
Auto-merging hello.py
Merge made by the 'recursive' strategy.
hello.py | 1 +
1 file changed, 1 insertion(+)

  再用git status看看状态。

1
2
3
4
5
6
7
$ git status

On branch master
Your branch is ahead of 'origin/master' by 3 commits.
(use "git push" to publish your local commits)

nothing to commit, working tree clean

  加上刚才合并的提交,现在我们本地分支比远程分支超前3个提交。用git log看看。

1
2
3
4
5
6
7
8
9
10
$ git log --graph --pretty=oneline --abbrev-commit

* e0ea545 (HEAD -> master) Merge branch 'master' of github.com:xx/xx
|\
| * f005ed4 (origin/master) set exit=1
* | 582d922 add author
* | 8875536 add comment
|/
* d1be385 init hello
...

  现在的状况就是提交历史分叉了,如果现在把本地分支push到远程,也是正确的,但对于一些人来说会觉得有些影响阅读。这个时候,rebase就派上了用场。我们输入命令git rebase试试。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git rebase

First, rewinding head to replay your work on top of it...
Applying: add comment
Using index info to reconstruct a base tree...
M hello.py
Falling back to patching base and 3-way merge...
Auto-merging hello.py
Applying: add author
Using index info to reconstruct a base tree...
M hello.py
Falling back to patching base and 3-way merge...
Auto-merging hello.py

  输出了一大堆操作,到底是啥效果?再用git log看看:

1
2
3
4
5
6
7
$ git log --graph --pretty=oneline --abbrev-commit

* 7e61ed4 (HEAD -> master) add author
* 3611cfe add comment
* f005ed4 (origin/master) set exit=1
* d1be385 init hello
...

  原本分叉的提交现在变成一条直线了。原理非常简单,Git把我们本地的提交“挪动”了位置,放到了f005ed4 (origin/master) set exit=1之后,这样整个提交历史就成了一条直线。rebase操作前后,最终的提交内容是一致的,但是我们本地的commit修改内容已经变化了,它们的修改不再基于d1be385 init hello,而是基于f005ed4 (origin/master) set exit=1,但最后的提交7e61ed4内容是一致的。

  这就是rebase操作的特点:把分叉的提交历史“整理”成一条直线,看上去更直观。缺点是本地的分叉提交已经被修改过了。

  最后,通过push操作把本地分支推送到远程。

1
2
3
4
5
6
7
8
9
10
$ git push origin master

Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (6/6), 576 bytes | 576.00 KiB/s, done.
Total 6 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 1 local object.
To github.com:michaelliao/learngit.git
f005ed4..7e61ed4 master -> master

  再用git log看看效果,远程分支的提交历史也是一条直线。

1
2
3
4
5
6
7
$ git log --graph --pretty=oneline --abbrev-commit

* 7e61ed4 (HEAD -> master, origin/master) add author
* 3611cfe add comment
* f005ed4 set exit=1
* d1be385 init hello
...

  rebase操作可以把本地未push的分叉提交历史整理成直线;rebase的目的是使得我们在查看历史提交的变化时更容易,因为分叉的提交需要三方对比。


第七节 标签管理

  发布一个版本时,我们通常先在版本库中打一个标签(tag),这样就唯一确定了打标签时刻的版本。将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来。所以标签也是版本库的一个快照。

  Git的标签虽然是版本库的快照,但其实它就是指向某个commit的指针(跟分支很像对不对?但是分支可以移动,标签不能移动),所以,创建和删除标签都是瞬间完成的。

创建标签

  首先切换到需要打标签的分支上。

$ git branch

$ git checkout master

  然后,敲命令git tag就可以打一个新标签。

$ git tag v1.0

  可以用命令git tag查看所有标签。

$ git tag

  默认标签是打在最新提交的commit上的。有时候如果忘了打标签怎么办?方法是找到历史提交的commit id,然后打上就可以了。

$ git log –pretty=oneline –abbrev-commit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
12a631b (HEAD -> master, tag: v1.0, origin/master) merged bug fix 101
4c805e2 fix bug 101
e1e9c68 merge with no-ff
f52c633 add merge
cf810e4 conflict fixed
5dc6824 & simple
14096d0 AND simple
b17d20e branch test
d46f35e remove test.txt
b84166e add test.txt
519219b git tracks changes
e43a48b understand how stage works
1094adb append GPL
e475afc add distributed
eaadf4e wrote a readme file

  比方说要对add merge这次提交打标签,它对应的commit id是f52c633,敲入命令。

$ git tag v0.9 f52c633

  再用命令git tag查看标签。

$ git tag

1
2
v0.9
v1.0

  注意,标签不是按时间顺序列出,而是按字母排序的。可以用git show查看标签信息。

$ git show v0.9

1
2
3
4
5
6
7
8
commit f52c63349bc3c1593499807e5c8e972b82c8f286 (tag: v0.9)
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 21:56:54 2018 +0800

add merge

diff --git a/readme.txt b/readme.txt
...

  可以看到,v0.9确实打在add merge这次提交上。

  还可以创建带有说明的标签,用-a指定标签名,-m指定说明文字。

$ git tag -a v0.1 -m “version 0.1 released” 1094adb

  用命令git show可以看到说明文字:

$ git show v0.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tag v0.1
Tagger: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 22:48:43 2018 +0800

version 0.1 released

commit 1094adb7b9b3807259d8cb349e7df1d4d6477073 (tag: v0.1)
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 21:06:15 2018 +0800

append GPL

diff --git a/readme.txt b/readme.txt
...

  注意:标签总是和某个commit挂钩。如果这个commit既出现在master分支,又出现在dev分支,那么在这两个分支上都可以看到这个标签。

操作标签

  如果标签打错了,也可以删除。

$ git tag -d v0.1

  因为创建的标签都只存储在本地,不会自动推送到远程。所以,打错的标签可以在本地安全删除。

  如果要推送某个标签到远程,使用命令git push origin。

$ git push origin v1.0

  或者,一次性推送全部尚未推送到远程的本地标签。

$ git push origin –tags

  如果标签已经推送到远程,要删除远程标签就麻烦一点,先从本地删除。

$ git tag -d v0.9

  然后,从远程删除。删除命令也是push,但是格式如下。

$ git push origin :refs/tags/v0.9

  要看看是否真的从远程库删除了标签,可以登陆GitHub查看。


第八节 使用GitHub参与开源项目

  我们一直用GitHub作为免费的远程仓库,如果是个人的开源项目,放到GitHub上是完全没有问题的。其实GitHub还是一个开源协作社区,通过GitHub,既可以让别人参与你的开源项目,也可以参与别人的开源项目。

  在GitHub出现以前,开源项目开源容易,但让广大人民群众参与进来比较困难,因为要参与,就要提交代码,而给每个想提交代码的群众都开一个账号那是不现实的,因此,群众也仅限于报个bug,即使能改掉bug,也只能把diff文件用邮件发过去,很不方便。但是在GitHub上,利用Git极其强大的克隆和分支功能,广大人民群众真正可以第一次自由参与各种开源项目了。

  如何参与一个开源项目呢?比如人气极高的bootstrap项目,这是一个非常强大的CSS框架,你可以访问它的项目主页https://github.com/twbs/bootstrap,点“Fork”就在自己的账号下克隆了一个bootstrap仓库,然后,从自己的账号下clone:

git clone git@github.com:michaelliao/bootstrap.git

  一定要从自己的账号下clone仓库,这样你才能推送修改。如果从bootstrap的作者的仓库地址git@github.com:twbs/bootstrap.git克隆,因为没有权限,你将不能推送修改。

  Bootstrap的官方仓库twbs/bootstrap、你在GitHub上克隆的仓库my/bootstrap,以及你自己克隆到本地电脑的仓库,他们的关系就像下图显示的那样:

1
2
3
4
5
6
7
8
9
10
11
┌─ GitHub ────────────────────────────────────┐
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ twbs/bootstrap │────>│ my/bootstrap │ │
│ └─────────────────┘ └─────────────────┘ │
│ ▲ │
└──────────────────────────────────┼──────────┘

┌─────────────────┐
local/bootstrap
└─────────────────┘

  如果你想修复bootstrap的一个bug,或者新增一个功能,立刻就可以开始干活,干完后,往自己的仓库推送。

  如果你希望bootstrap的官方库能接受你的修改,你就可以在GitHub上发起一个pull request。当然,对方是否接受你的pull request就不一定了。

小结

  在GitHub上,可以任意Fork开源仓库;

  自己拥有Fork后的仓库的读写权限;

  可以推送pull request给官方仓库来贡献代码。


第九节 搭建Git服务器

  GitHub就是一个免费托管开源代码的远程仓库。但是对于某些视源代码如生命的商业公司来说,既不想公开源代码,又舍不得给GitHub交保护费,那就只能自己搭建一台Git服务器作为私有仓库使用。

  搭建Git服务器需要准备一台运行Linux的机器,强烈推荐用Ubuntu或Debian,这样通过几条简单的apt命令就可以完成安装。

  假设你已经有sudo权限的用户账号,下面正式开始安装。

  1. 第一步,安装git:

$ sudo apt-get install git

  1. 第二步,创建一个git用户,用来运行git服务

$ sudo adduser git

  1. 第三步,创建证书登录:

  收集所有需要登录的用户的公钥,就是他们自己的id_rsa.pub文件,把所有公钥导入到/home/git/.ssh/authorized_keys文件里,一行一个。

  1. 第四步,初始化Git仓库:

  先选定一个目录作为Git仓库,假定是/srv/sample.git,在/srv目录下输入命令:

$ sudo git init –bare sample.git

  Git就会创建一个裸仓库,裸仓库没有工作区,因为服务器上的Git仓库纯粹是为了共享,所以不让用户直接登录到服务器上去改工作区,并且服务器上的Git仓库通常都以.git结尾。然后,把owner改为git:

$ sudo chown -R git:git sample.git

  1. 第五步,禁用shell登录:

  出于安全考虑,第二步创建的git用户不允许登录shell,这可以通过编辑/etc/passwd文件完成。找到类似下面的一行:

git:x:1001:1001:,,,:/home/git:/bin/bash

  改为:

git:x:1001:1001:,,,:/home/git:/usr/bin/git-shell

  这样,git用户可以正常通过ssh使用git,但无法登录shell,因为我们为git用户指定的git-shell每次一登录就自动退出。

  1. 第六步,克隆远程仓库:

现在,可以通过git clone命令克隆远程仓库了,在各自的电脑上运行:

$ git clone git@server:/srv/sample.git

Cloning into ‘sample’…
warning: You appear to have cloned an empty repository.

  剩下的推送就简单了。

管理公钥

  如果团队很小,把每个人的公钥收集起来放到服务器的/home/git/.ssh/authorized_keys文件里就是可行的。如果团队有几百号人,就没法这么玩了,这时,可以用Gitosis来管理公钥。这里我们不介绍怎么玩Gitosis了,几百号人的团队基本都在500强了,相信找个高水平的Linux管理员问题不大。

管理权限

  有很多不但视源代码如生命,而且视员工为窃贼的公司,会在版本控制系统里设置一套完善的权限控制,每个人是否有读写权限会精确到每个分支甚至每个目录下。因为Git是为Linux源代码托管而开发的,所以Git也继承了开源社区的精神,不支持权限控制。不过,因为Git支持钩子(hook),所以,可以在服务器端编写一系列脚本来控制提交等操作,达到权限控制的目的。Gitolite就是这个工具。这里我们也不介绍Gitolite了,不要把有限的生命浪费到权限斗争中。

小结

  搭建Git服务器非常简单,通常10分钟即可完成;

  要方便管理公钥,用Gitosis;

  要像SVN那样变态地控制权限,用Gitolite。


参考博客和文章书籍等:

廖雪峰的官方网站

A successful Git branching model

因博客主等未标明不可引用,若部分内容涉及侵权请及时告知,我会尽快修改和删除相关内容