文章目录
前言
git是一个免费的开源分布式版本控制系统,它最初是 Linus Torvalds 于2005 年 4 月,为了帮助管理 Linux 内核开发而开发的版本控制软件
版本控制系统(Version Control System, VCS)是一种可以记录一个或多个文件内容变化,以便将来查阅的系统。它有四个发展阶段:
-
起源
linux有两个工具diff和patch,可以计算两个文件的不同,并还原。这两个工具可以说是VCS的起源。据说1991-2002年之间,即使CVS出现之后,Linus一直使用diff和patch管理着Linux的代码
-
第一代:本地版本控制系统
1982 年开发的修订控制系统(Revision Control System,RCS) 是第一代的版本控制系统,它由一组 UNIX 命令构成。RCS把diff的集合,通过自己的格式保存到磁盘中,还可以通过这些diff集合,重新回到文件修改的任何历史中的点
-
第二代:集中式版本控制系统
1986 年开发的并发版本系统(Concurrent Versions System,CVS)是,CVS使用Copy-Modify-Merge(拷贝、修改、合并)变化表支持对文件的同时访问和修改。它明确地将源文件的存储和用户的工作空间独立开来,并使其并行操作,这意味着可以多人同时处理文件
但有一个明显的限制,用户必须在允许提交之前将当前修订合并到他们的工作中,这意味着一个缺点,如果服务器宕机了或者未连上服务器,开发者就不能对项目进行提交了
第二代版本控制系统主要有 CVS、SourceSafe、Subversion、Team Foundation Server、SVK
-
第三代:分布式版本控制系统
以git为首的第三代分布式VCS,解决了上述问题,每个使用者电脑上就有一个完整的数据仓库,没有网络依然可以使用,在网络具备时,再和服务器进行同步即可
第三代版本控制系统主要有 Git、Bazaar、Mercurial、BitKeeper、Monotone
在程序开发中,使用git的好处有
- 开发中出现错误很常见,使用版本控制不仅可以了解引入错误的时间和地点,而且还可以用于立即恢复到正确的版本
- 离线工作,如果git服务器出现问题,也可以在本地进行切换分支的操作,等联网后再提交、合并等操作
- 分支管理,分支之间的互不影响这种特性可以增加团队合作的效率,也方便不同版本的开发
一、git常用命令
1.1 新建代码库
#在当前目录新建一个Git代码库
git init
#新建一个目录,将其初始化为Git代码库
git init [project-name]
# 下载一个项目和它的整个代码历史
git clone [url]
1.2 配置
# 显示当前的Git配置
git config --list
# 编辑Git配置文件
git config -e [--global]
# 设置提交代码时的用户信息
git config [--global] user.name "[name]"
git config [--global] user.email "[email]"
# 设置当前项目用户名
git config --local user.name "name"
1.3 增加/删除文件
# 添加指定文件到暂存区
git add [file1] [file2] ...
# 添加指定目录到暂存区,包括子目录
git add [dir]
# 添加当前目录的所有文件到暂存区
git add .
# 添加每个变化前,都会要求确认
# 对于同一个文件的多处变化,可以实现分次提交
git add -p
# 删除工作区文件,并且将这次删除放入暂存区
git rm [file1] [file2] ...
# 停止追踪指定文件,但该文件会保留在工作区
git rm --cached [file]
# 改名文件,并且将这个改名放入暂存区
git mv [file-original] [file-renamed]
1.4 代码提交
# 提交暂存区到仓库区
git commit -m [message]
# 提交暂存区的指定文件到仓库区
git commit [file1] [file2] ... -m [message]
# 提交工作区自上次commit之后的变化,直接到仓库区
git commit -a
# 提交时显示所有diff信息
git commit -v
# 使用一次新的commit,替代上一次提交
# 如果代码没有任何新变化,则用来改写上一次commit的提交信息
git commit --amend -m [message]
# 重做上一次commit,并包括指定文件的新变化
git commit --amend [file1] [file2] ...
# 撤销提交
git reset --soft HEAD^
# 将上一次提交的用户名和邮箱修改
git commit --amend --author="name <email>"
1.5 分支
# 列出所有本地分支
git branch
# 列出所有远程分支
git branch -r
# 列出所有本地分支和远程分支
git branch -a
# 新建一个分支,但依然停留在当前分支
git branch [branch-name]
# 新建一个分支,并切换到该分支
git checkout -b [branch]
# 新建一个分支,指向指定commit
git branch [branch] [commit]
# 新建一个分支,与指定的远程分支建立追踪关系
git branch --track [branch] [remote-branch]
# 切换到指定分支,并更新工作区
git checkout [branch-name]
# 切换到上一个分支
git checkout -
# 建立追踪关系,在现有分支与指定的远程分支之间
git branch --set-upstream [branch] [remote-branch]
# 合并指定分支到当前分支
git merge [branch]
# 选择一个commit,合并进当前分支
git cherry-pick [commit]
# 删除分支
git branch -d [branch-name]
# 删除远程分支
git push origin --delete [branch-name]
git branch -dr [remote/branch]
1.6 标签
# 列出所有tag
git tag
# 新建一个tag在当前commit
git tag [tag]
# 新建一个tag在指定commit
git tag [tag] [commit]
# 删除本地tag
git tag -d [tag]
# 删除远程tag
git push origin :refs/tags/[tagName]
# 查看tag信息
git show [tag]
# 提交指定tag
git push [remote] [tag]
# 提交所有tag
git push [remote] --tags
# 新建一个分支,指向某个tag
git checkout -b [branch] [tag]
1.7 查看信息
# 显示有变更的文件
git status
# 显示当前分支的版本历史
git log
# 显示commit历史,以及每次commit发生变更的文件
git log --stat
# 每个提交在一行内显示
git log --oneline
# 格式化输出log
# 例子git log --pretty=format:"%h - %an, %ar : %s"
# %h:commit缩略哈希
# %an:用户名
# %ar:用户从近到远相对时间
# %s:commit信息
# 详细文档可以见:https://git-scm.com/docs/git-log
git log --pretty=format:"<format-string>"
# 显示两个commit之间的所有commit
git log <commit1>...<commit2>
# 显示某个文件的版本历史,包括文件改名
git log --follow [file]
git whatchanged [file]
# 显示指定文件相关的每一次diff
git log -p [file]
# 显示过去5次commit
git log -5 --pretty --oneline
# 显示所有提交过的用户,按提交次数排序
git shortlog -sn
# 显示指定文件是什么人在什么时间修改过
git blame [file]
# 显示暂存区和工作区的差异
git diff
# 显示暂存区和上一个commit的差异
git diff --cached [file]
# 显示工作区与当前分支最新commit之间的差异
git diff HEAD
# 显示两次提交之间的差异
git diff [commit1]...[commit2]
# 显示两个分支之间的差异
git diff [master]..[my-branch]
# 显示两次提交之间的改动文件
git diff --numstat [commit1]...[commit2]
# 显示今天你写了多少行代码
git diff --shortstat "@{0 day ago}"
# 显示某次提交的元数据和内容变化
git show [commit]
# 显示某次提交发生变化的文件
git show --name-only [commit]
# 显示某次提交时,某个文件的内容
git show [commit]:[filename]
# git reflog可以查看删除和reset的commit信息(git log查不到)
# git reglog包含所有分支信息,gitlog当前分支信息
git reflog
1.8 远程操作
# 下载远程仓库的所有变动
git fetch [remote]
# 显示所有远程仓库
git remote -v
# 显示某个远程仓库的信息
git remote show [remote]
# 增加一个新的远程仓库,并命名
git remote add [shortname] [url]
# 取回远程仓库的变化,并与本地分支合并
git pull [remote] [branch]
# 上传本地指定分支到远程仓库
git push [remote] [branch]
# 强行推送当前分支到远程仓库,即使有冲突
git push [remote] --force
# 推送所有分支到远程仓库
git push [remote] --all
1.9 撤销
# 恢复暂存区的指定文件到工作区
git checkout [file]
# 恢复某个commit的指定文件到暂存区和工作区
git checkout [commit] [file]
# 恢复暂存区的所有文件到工作区
git checkout .
# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
git reset [file]
# 重置暂存区与工作区,与上一次commit保持一致
git reset --hard
# 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
git reset [commit]
# 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
git reset --hard [commit]
# 重置当前HEAD为指定commit,但保持暂存区和工作区不变
git reset --keep [commit]
# 新建一个commit,用来撤销指定commit
# 后者的所有变化都将被前者抵消,并且应用到当前分支
git revert [commit]
# 暂时将未提交的变化移除,稍后再移入
git stash
git stash pop
1.10 变基解决冲突
Git 冲突是指在合并分支时,git不清楚两个分支都修改的文件哪个版本是正确的,这在多人合作时经常会出现。
GIT会在文件的冲突位置添加以下信息
<<<<<<<<<<
==========
>>>>>>>>>>
本地版本和他人版本冲突内容通过======分隔开来,这需要选择正确的版本,来告诉git这才是对的
合并分支一般使用git merge
或 git rebase
,在实际开发中为了时间线保持一条直线,使用git rebase
比较多
(feature1)$ git rebase master
执行以上操作, git会执行以下操作
- git 会把 feature1 分支里面的每个 commit 取消掉
- 把上面的操作临时保存成 patch 文件,存在 .git/rebase 目录下
- 把 feature1 分支更新到最新的 master 分支
- 把上面保存的 patch 文件应用到 feature1 分支上
在 rebase 的过程中,也许会出现冲突 conflict。在这种情况,git 会停止 rebase 并会让你去解决冲突。在解决完冲突后,用 git add
命令去更新这些内容, git commit --amend
不修改commit信息继续提交,然后使用 git rebase --continue
继续rebase直到合并完成
# 继续下个冲突解决
git rebase --continue
# 取消本次rebase
git rebase --abort
rebase变基,还可以用来修改历史commit信息,合并commit等操作
# 修改刚提交的3个commit
git rebase -i HEAD~3
会开启新窗口,将pick根据需求修改为自己想要的
pick 242f87598 commit-msg1
pick f7656a723 commit-msg2
pick 11f2d0297 commit-msg3
- p,pick:使用该次提交
- r,reword:使用该次提交,但重新编辑commit信息
- e,edit:使用该次提交,但停止到该次提交
- s,squash:将该commit和前一个commit合并
- f,fixup:将该commit和前一个commit合并,但不保留该提交的commit信息
- x,exec:执行shell命令
- d,drop:丢弃该commit
解决冲突时使用e,修改历史commit信息时使用r,合并commit使用s
二、git 源码探秘
2.1 初始源码
git的第一个commit
git 初始版本命令 | git当前版本命令 | 含义 |
---|---|---|
init-db | git init | 初始化git仓库 |
update-cache | git add | 添加文件到暂存区 |
write-tree | git write-tree | 使用临时索引中的内容将树对象写入Git仓库 |
commit-tree | git commit | 基于指定的树在Git仓库中创建新的commit对象 |
read-tree | git read-tree | 显示Git仓库中树对象内容 |
show-diff | git diff | 显示暂存区与工作区之间的差异 |
cat-file | git cat-file | 显示存储在Git仓库中的对象内容 |
具体命令作用,可以查看这篇博文:源码解析:Git的第一个提交是什么样的?
2.2 编译v1.3.0
最近的git版本为v2.37,git自2005年至今,不断迭代,功能不断完善、也增加好多代码,阅读最新版的源码很困难,所以选择一个简单版本来阅读源码,最好可以编译,更利于理解代码运行
在github找到git 第一个发布版本是v0.99,但是有个文件始终编译不过去,猜测是openssl版本问题,但git并没有说明对应版本,最后找到v1.3这个版本编译成功
cd git-1.3.0
sudo apt-get install libcurl4-openssl-dev
sudo apt-get install expat-devel
sudo apt-get install libexpat1-dev
make
make install
根据git-1.3.0\Documentation\tutorial.txt 可以练习这个版本的命令
mkdir git-tutorial
cd git-tutorial
git-init-db
echo "Hello World" >hello
echo "Silly example" >example
git add .
git commit
git branch expr
不能commit,报错fatal: empty ident,需要更新用户
git-repo-config "user.name" "aabond"
2.3 源码阅读
2.3.1 git add
发现这个版本的git add
实际执行的是个shell脚本git-add.sh, 最终调用git-update-index
git-update-index --add $verbose -z --stdin ;;
而git-update-index
的源码在update-index.c, 将文件遍历,根据SHA1算法写入.git/objects,并添加到缓存
int main(int argc, const char **argv)
{
...
entries = read_cache();
...
if (read_from_stdin) {
struct strbuf buf;
strbuf_init(&buf);
while (1) {
char *path_name;
read_line(&buf, stdin, line_termination);
if (buf.eof)
break;
if (line_termination && buf.buf[0] == '"')
path_name = unquote_c_style(buf.buf, NULL);
else
path_name = buf.buf;
update_one(path_name, prefix, prefix_length);
if (path_name != buf.buf)
free(path_name);
}
}
...
}
static void update_one(const char *path, const char *prefix, int prefix_length)
{
...
if (add_file_to_cache(p))
die("Unable to process file %s", path);
...
}
static int add_file_to_cache(const char *path)
{
...
// 写入objects
if (index_path(ce->sha1, path, &st, !info_only))
return -1;
...
// 添加缓存
if (add_cache_entry(ce, option))
return error("%s: cannot add to the index - missing --add option?",
path);
return 0;
}
写入.git/objects: 通过zlib对文件压缩,计算sha1
int write_sha1_file(void *buf, unsigned long len, const char *type, unsigned char *returnsha1)
{
int size;
unsigned char *compressed;
z_stream stream;
unsigned char sha1[20];
char *filename;
static char tmpfile[PATH_MAX];
unsigned char hdr[50];
int fd, hdrlen;
/* Normally if we have it in the pack then we do not bother writing
* it out into .git/objects/??/?{38} file.
*/
filename = write_sha1_file_prepare(buf, len, type, sha1, hdr, &hdrlen);
...
}
根据不同的对象类型type生成header
char *write_sha1_file_prepare(void *buf,
unsigned long len,
const char *type,
unsigned char *sha1,
unsigned char *hdr,
int *hdrlen)
{
SHA_CTX c;
/* Generate the header */
*hdrlen = sprintf((char *)hdr, "%s %lu", type, len)+1;
/* Sha1.. */
SHA1_Init(&c);
SHA1_Update(&c, hdr, *hdrlen);
SHA1_Update(&c, buf, len);
SHA1_Final(sha1, &c);
return sha1_file_name(sha1);
}
通过sha1_file_name这个函数,可以看到将sha1的头两个字符用/分隔开来,得到文件的路径和名称
char *sha1_file_name(const unsigned char *sha1)
{
static char *name, *base;
if (!base) {
const char *sha1_file_directory = get_object_directory();
int len = strlen(sha1_file_directory);
base = xmalloc(len + 60);
memcpy(base, sha1_file_directory, len);
memset(base+len, 0, 60);
base[len] = '/';
base[len+3] = '/';
name = base + len + 1;
}
fill_sha1_path(name, sha1);
return base;
}
缓存实际存储在.git/index
int read_cache(void)
{
int fd, i;
...
fd = open(get_index_file(), O_RDONLY);
...
}
char *get_index_file(void)
{
if (!git_index_file)
setup_git_env();
return git_index_file;
}
static void setup_git_env(void)
{
...
git_index_file = getenv(INDEX_ENVIRONMENT);
if (!git_index_file) {
git_index_file = xmalloc(strlen(git_dir) + 7);
sprintf(git_index_file, "%s/index", git_dir);
}
...
}
2.3.2 git commit
这个版本的git commit
实际执行也是个shell脚本git-commit.sh,会调用git-commit-tree
,源码位于commit-tree.c
int main(int argc, char **argv)
{
int i;
int parents = 0;
unsigned char tree_sha1[20];
unsigned char commit_sha1[20];
char comment[1000];
char *buffer;
unsigned int size;
// 设置用户邮箱 name + '@' + hostname [+ '.' + domainname
setup_ident();
setup_git_directory();
...
/* Person/date information */
add_buffer(&buffer, &size, "author %s\n", git_author_info(1));
add_buffer(&buffer, &size, "committer %s\n\n", git_committer_info(1));
/* And add the comment */
while (fgets(comment, sizeof(comment), stdin) != NULL)
add_buffer(&buffer, &size, "%s", comment);
if (!write_sha1_file(buffer, size, commit_type, commit_sha1)) {
printf("%s\n", sha1_to_hex(commit_sha1));
return 0;
}
else
return 1;
}
2.3.3 git branch
同上,这个版本的git branch
也是通过调用sh脚本git-branch.sh来实现,下述是创建分支相关源码
...
branchname="$1"
# 验证参数
rev=$(git-rev-parse --verify "$head") || exit
# 检测分支名字合法
git-check-ref-format "heads/$branchname"
...
# 创建.git/refs/heads/$branchname文件,将当前分支的Head指向的commit对象的hash写入文件中
git update-ref "refs/heads/$branchname" $rev
三、IDEA中使用git
3.1 推荐插件
-
使用.ignore插件生成忽略文件
使用方法:在项目名字上右键,点击New->.ignore file->.ignore file(git),然后选择对应的编程语言、框架
-
GitToolBox
最直观的感受是能够直接查看每行是谁commit的,还有能够自动fetch remote
3.2 提交
使用快捷键ctrl+k快速commit
3.3 解决冲突
当完成把本地分支完成的任务推送到gitlab时,有时会发现有冲突,提示
这种情况下,需要在本地解决冲突,再推送到gitlab
点击git rebase, 会提示冲突文件
点击冲突文件,显示冲突部分。开始修改,可选择魔棒工具, x和>>来选择合适部分
最后完成
最后将rebase的commit提交,pull到gitlab, 最终显示