前言

git是一个免费的开源分布式版本控制系统,它最初是 Linus Torvalds 于2005 年 4 月,为了帮助管理 Linux 内核开发而开发的版本控制软件

​ 版本控制系统(Version Control System, VCS)是一种可以记录一个或多个文件内容变化,以便将来查阅的系统。它有四个发展阶段:

  1. 起源

    ​ linux有两个工具diff和patch,可以计算两个文件的不同,并还原。这两个工具可以说是VCS的起源。据说1991-2002年之间,即使CVS出现之后,Linus一直使用diff和patch管理着Linux的代码

  2. 第一代:本地版本控制系统

    ​ 1982 年开发的修订控制系统(Revision Control System,RCS) 是第一代的版本控制系统,它由一组 UNIX 命令构成。RCS把diff的集合,通过自己的格式保存到磁盘中,还可以通过这些diff集合,重新回到文件修改的任何历史中的点

  3. 第二代:集中式版本控制系统

    ​ 1986 年开发的并发版本系统(Concurrent Versions System,CVS)是,CVS使用Copy-Modify-Merge(拷贝、修改、合并)变化表支持对文件的同时访问和修改。它明确地将源文件的存储和用户的工作空间独立开来,并使其并行操作,这意味着可以多人同时处理文件

    ​ 但有一个明显的限制,用户必须在允许提交之前将当前修订合并到他们的工作中,这意味着一个缺点,如果服务器宕机了或者未连上服务器,开发者就不能对项目进行提交了

    ​ 第二代版本控制系统主要有 CVS、SourceSafe、Subversion、Team Foundation Server、SVK

  4. 第三代:分布式版本控制系统

    ​ 以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 mergegit rebase,在实际开发中为了时间线保持一条直线,使用git rebase比较多

(feature1)$ git rebase master

执行以上操作, git会执行以下操作

  1. git 会把 feature1 分支里面的每个 commit 取消掉
  2. 把上面的操作临时保存成 patch 文件,存在 .git/rebase 目录下
  3. 把 feature1 分支更新到最新的 master 分支
  4. 把上面保存的 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-dbgit init初始化git仓库
update-cachegit add添加文件到暂存区
write-treegit write-tree使用临时索引中的内容将树对象写入Git仓库
commit-treegit commit基于指定的树在Git仓库中创建新的commit对象
read-treegit read-tree显示Git仓库中树对象内容
show-diffgit diff显示暂存区与工作区之间的差异
cat-filegit 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

    使用.ignore插件生成忽略文件

    使用方法:在项目名字上右键,点击New->.ignore file->.ignore file(git),然后选择对应的编程语言、框架

    idea-gitignore-new-file

  • GitToolBox
    最直观的感受是能够直接查看每行是谁commit的,还有能够自动fetch remote

3.2 提交

使用快捷键ctrl+k快速commit

ctrl+k show commit dialog

3.3 解决冲突

当完成把本地分支完成的任务推送到gitlab时,有时会发现有冲突,提示

there are merge conflicts.

这种情况下,需要在本地解决冲突,再推送到gitlab

点击git rebase, 会提示冲突文件

idea git rebase t resolve conflict

点击冲突文件,显示冲突部分。开始修改,可选择魔棒工具, x和>>来选择合适部分

conflict side by side

最后完成

resolve ok

最后将rebase的commit提交,pull到gitlab, 最终显示

push gitlab ok

参考

  1. 版本控制系统 之一 概念、分类、常见版本控制系统(CVS、SVN、BitKeeper、Git 等)
  2. https://git-scm.com/book/zh/v2
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐