本文章将简要介绍Windows下Git及其基本操作,并结合用C++实现单链表开发实例学会使用Git进行版本控制和项目开发。
关于Git
什么是Git和GitHub
GitHub是世界上最大的代码托管平台、远程仓库,同性交友网站,它利用Git进行版本控制。
Git是世界上最流行的分布式版本控制工具,简单来说就是当你在项目开发版本迭代的过程中,可以利用Git方便地回溯到任何一个历史版本,而不需要对历史文件进行任何存储和备份。当你试图利用word进行版本控制并与导师愉快交流:
此外,Git也是团队合作的工具,使用Git能够记录谁在哪个时间对项目的哪一部分进行了修改,进行跟踪,保证团队成员的同步和协作开发。
使用Git
可以通过下面的方法使用Git:
利用Git Bash
Git Bash是方便在windows下使用git命令的模拟终端(windows自带的cmd功能太弱):
vscode中使用Git
两种方法:
Git项目开发流程演练
Git基本操作
Git的基本操作如下:
Git的原理可以参考孟宁老师的文章:
五⼤场景玩转 Git,只要这一篇就够了!
下面通过一个简单的实例来熟悉这些基本操作:使用C++实现单链表,通过Git来进行版本控制,用GitHub作为远程仓库(Remote)。示例所用仓库地址放在最后。
我们使用Git Bash进行命令行操作,使用vscode作为默认编辑器。
单人版本控制
假设现在只有你一个人在独立开发。
Git config
首先在本地利用Git Bash进行Git的用户配置:
$ git config --global user.email "Lanjun1998@163.com" #使用你的邮箱作为用户标识
$ git config --global user.name "FayunYm" #英文名
$ git config --global core.editor "code -w" #使用vscode作为默认编辑器,你可以使用code -h命令查看w参数的意思
配置成功后可通过以下命令查看用户(--global)配置:
$ git config --global --list
或通过vscode查看或更改配置:
$ git config --global -e
git clone
然后在GitHub上创建仓库命名为Link-List:
然后将其克隆到本地仓库。但在这之前需要取得clone权限:
获取git clone权限
使用clone指令将GitHub仓库的内容克隆至本地:
$ git clone git@github.com:LanjunC/Link-List.git #SSH地址
生成了Link-List文件夹:
git add 和git commit
通过vscode打开Link-List文件夹,将其作为工作区,编辑README并保存:
接着使用git add指令将文件添加至暂存区index,表示已经暂存了更改,文件已经被纳入追踪了,但是还未纳入本地仓库的版本控制中。你可以随时add新的更改到index中:
$ git add README.md # add指定文件如README.md
$ git add . # 所有文件
若你觉得这次的更改已经进行的差不多了,则使用commit将暂存的文件提交到本地仓库,生成可以被追踪的版本:
$ git commit
需要编辑此次commit的日志,方便以后查看:
也可以直接通过以下命令实现commit和提交日志:
$ git commit -m "init README"
.gitignore, git status
接下来进行单链表的实现,先加入头文件link_list.h以及实现link_list.cpp,以及测试用例test.cpp:
// link_list.h --单链表头文件
#ifndef LINK_LIST_NODE_H_
#define LINK_LIST_NODE_H_
typedef int DATA_TYPE;
//单链表结点
struct Node {
DATA_TYPE data;
Node *next;
};
class LinkList
{
private:
Node *head;
int size;
public:
LinkList();
~LinkList();
};
#endif
//link_list.cpp --单链表的实现
#include <iostream>
#include"link_list.h"
LinkList::LinkList()
{
head = new Node;
head->data = 0;
head->next = NULL;
size = 0;
}
LinkList::~LinkList()
{
delete head;
}
// test.cpp --测试用例
#include "link_list.h"
int main() {
return 0;
}
如果我们在本地工作区进行编译调试,此时会在文件夹下生成.vscode文件夹,但我们并不希望将此文件夹纳入版本管理,而且我们以后在遇到实际项目时会附带很多的插件以及cmake管理文件等,我们也不希望纳入版本控制,这时我们可以在文件夹下创建.gitignore文件夹:
此时.vscode文件夹就不再纳入版本控制,我们可以使用git status命令查看当前工作区状态,在其他时刻你也可以随时查看工作区状态以便掌握情况:
$ git status
可以看到待add的项目中并没有.vscode。此时就可以add并commit啦。
git diff, 文件恢复
继续实现单链表,添加2个成员函数用来建立和摧毁单链表:
// link_list.h --单链表头文件
...
class LinkList {
...
int CreateLinkList(int size);
int BYELinkList();
};
...
//link_list.cpp --单链表的实现
...
using namespace std;
...
int LinkList::CreateLinkList(int n)
{
if (n<0) {
printf("error\n");
return -1;
}
Node *ptemp = NULL;
Node *pnew = NULL;
this->size = n;
ptemp = this->head;
for(int i =0 ; i<n ; i++)
{
pnew = new Node;
pnew->next = NULL;
cout << "输入第" << i+1 << "个节点值" << endl;
cin >> pnew->data;
ptemp->next = pnew;
ptemp = pnew;
}
cout << "创建完成" << endl;
return 0;
}
int LinkList::BYELinkList()
{
Node *ptemp;
if (this->head == NULL) {
cout << "链表原本就为空" << endl;
return -1;
}
while (this->head)
{
ptemp = head->next;
free(head);
head = ptemp;
}
cout << "销毁链表完成" << endl;
return 0;
}
// test.cpp --测试用例
#include "link_list.h"
int main() {
LinkList list;
list.CreateLinkList(5);
return 0;
}
为了方便地看到改动的地方,我们可以用git diff指令查看改动前后的差异:
$ git diff #比对所有差异
$ git diff test.cpp #比对特定文件的差异如test.cpp
$ git diff --staged #如果文件已经add到暂存区,则需要用此指令比对staged(即已经纳入暂存区的)文件
若我们做了大量错误的更改,还未add,想快速回到修改前的状态,则我们可以使用git checkout 签出到修改前状态:
$ git checkout -- FILE #一定要加--
$ git checkout -- .
回到了修改前状态:
如果我们已经add,但还未commit,可以先使用下面的命令将暂存的文件移除,再签出:
git reset HEAD filename #取消将文件添加到暂存区
做完修改后,我们就可以add并commit了。可以使用如下带-a的命令对已经纳入版本控制的文件一口气完成add和commit,但新建的文件未纳入版本控制则不能:
$ git commit -am "develop CreateLinkList and BYELinkList"
提交日志,版本回退
git可以让你根据需要回到你想回到的版本,这里用了我们前面多次commit所产生的日志,使用git log查看日志:
$ git log
$ git log --oneline #查看简单版本的日志
$ git log --graph ##以图的形式
HEAD可以理解为实现版本控制的指针,他始终指向当前所在的版本。
使用如下命令回退到需要的版本:
$ git reset --hard HEAD^ #HEAD回退到上一次提交,即回退到上一个版本
$ git reset --hard HEAD^^ #回退两个版本
$ git reset --hard [commit-id] #上图每次commit前面的字符串即为对应commit-id。到达对应的版本
$ git reset --hard HEAD~2 #回退2个版本
回退一个版本后工作区恢复到上个版本:
此时的log:
但是当前的log无法显示将来的更改,那么怎么回到将来的更改呢,答案是使用git reflog查看所有的日志:
就能根据对应哈希值回到未来的版本。
分支
使用git的分支功能,在分支上所进行的更改不会影响到其他分支,因此分支的引入使得团队协作更为方便。此外主分支的程序在运转中时,可以通过创建分支,在分支上修改bug、增加功能,再并入的主分支的方式防止干扰主分支的运转。
建立分支,切换分支
$ git branch [name] #创建特定名称的分支
$ git branch #查看分支
$ git checkout [branch] #切换到某一分支
$ git checkout -b [branch] #建立特定名称的分支并切换到那
建立分支testbranch并checkout到该分支:
git rebase
我们在testbranch分支上继续进行单链表实现。
// link_list.h --单链表头文件
#ifndef LINK_LIST_NODE_H_
#define LINK_LIST_NODE_H_
typedef int DATA_TYPE;
//单链表结点
struct Node {
DATA_TYPE data;
Node *next;
};
class LinkList
{
private:
Node *head;
int size;
public:
LinkList();
~LinkList();
int CreateLinkList(int size);
int BYELinkList();
int TravalLinkList();
int GetLen();
bool IsEmply();
};
#endif
//link_list.cpp --单链表的实现
#include <iostream>
#include "link_list.h"
using namespace std;
LinkList::LinkList()
{
head = new Node;
head->data = 0;
head->next = NULL;
size = 0;
}
LinkList::~LinkList()
{
delete head;
}
int LinkList::CreateLinkList(int n)
{
if (n<0) {
printf("error\n");
return -1;
}
Node *ptemp = NULL;
Node *pnew = NULL;
this->size = n;
ptemp = this->head;
for(int i =0 ; i<n ; i++)
{
pnew = new Node;
pnew->next = NULL;
cout << "输入第" << i+1 << "个节点值" << endl;
cin >> pnew->data;
ptemp->next = pnew;
ptemp = pnew;
}
cout << "创建完成" << endl;
return 0;
}
int LinkList::BYELinkList()
{
Node *ptemp;
if (this->head == NULL) {
cout << "链表原本就为空" << endl;
return -1;
}
while (this->head)
{
ptemp = head->next;
free(head);
head = ptemp;
}
cout << "销毁链表完成" << endl;
return 0;
}
int LinkList::TravalLinkList()
{
Node *ptemp = this->head->next;
if (this->head == NULL) {
cout << "链表为空" << endl;
return -1;
}
while(ptemp)
{
cout << ptemp->data << "->";
ptemp = ptemp->next;
}
cout <<"NULL"<< endl;
return 0;
}
int LinkList::GetLen()
{
return this->size;
}
bool LinkList::IsEmply()
{
if (this->head == NULL) {
return true;
}
else{
return false;
}
}
// test.cpp --测试用例
#include "link_list.h"
int main() {
LinkList list;
list.CreateLinkList(5);
list.TravalLinkList();
return 0;
}
我们在分支上,每次增加一个成员函数并进行相应更改,都进行一次commit,接着修改了一些代码,再进行commit,因此从建立分支开始总共进行了4次commit:
为了使得我们的日志简单干净,我们常常需要在提交前进行一个叫rebase的操作,该操作可以合并某些提交,使得最终的提交日志更为简洁,更易阅读。
$ git rebase -i [start] [end] #后面一般省略,-i表示--interactive,即弹出交互式的界面让用户编辑
rebase从start指示的地方一直到end指示的地方,我们常常省略end,默认end为当前版本。
我们使用如下指令合并这四次提交:
$ git rebase -i HEAD~4 #注意这里的数字
我们只保留最新的一次提交,将其他几次提交合并到此,因此我们删除前三行提交:
由于我们这几次commit进行了很多了增删,因此文件内容会发生冲突,你可以根据需要决定最终保留什么文本,冲突的指示如下:
解决冲突后,我们要进行一次add,继续rebase:
$ git rebase --continue
rebase成功后commit,完成。
git merge
完成分支上的修改后,我们就可以把分支上的新内容并入主分支main了。
我们切换到main主分支,使用如下指令将testbrain并入:
$ git merge --no-ff testbrain #no-ff参数表示关闭快速合并
此时查看版本日志,可以看到历史上有支路的产生和合并:
简单的多人开发
现在来模拟一下简单的多人开发。
我们首先使用push命令将我们修改的内容更新到远端仓库:
$ git push
可以看到远端仓库已被更新:
接着我们假设和Bob一起进行单链表的开发,你和Bob分别进行单链表的插入和删除的成员方法的实现和测试。Bob和你一样,在本地进行更新,并将新内容push到远端仓库,并且Bob比你先一步完成,将内容push到GitHub,因此现在的情况是远端仓库的内容领先于本地,此时本地不知道远端仓库已经进行了更新。
待你完成你的任务后,你想将内容push到远端:
Git会提示push失败,原因就是远端领先于本地,你push的内容和远端内容发生了冲突。因此你首先要做的是将最新的内容pull(拉取)到本地,这样你就能在本地尝试解决冲突:
$ git pull
解决完毕后commit,再次push就能成功啦!(我们最后还在本地修复了一些小bug)
日常开发中我们要养成先pull再push的习惯。
至此,整个过程的提交日志如下所显示:
其他
git help
使用如下指令可以获取特定指令的详细说明:
$ git help [指令名如clone]
pull和push详解
实际开发中参与人数众多,分支数量大,版本网络复杂,需要使用更复杂的pull和push指令。
git clone,push,pull,fetch命令详解
实际开发中还会涉及Fork和Pull Request,这里就不讲解了。
一点心得体会
本人以前有简单学习Git,但对于Git的理解始终是模糊的,碰巧这次孟宁老师亲自教授Git的使用,因此这一次也一鼓作气好好地学习了一下Git并写下这篇Blog,才发现一旦学会了Git的指令和原理,会对实际开发产生巨大的帮助。我们能够把更多的精力集中在代码本身而不是琐碎的版本控制上,帮助我们成倍地提升效率。
接下来还需要结合实际中的项目,对Git的多人协作版本控制进行更深入的了解才行。
其他参考资料
孟宁老师通过五大场景深入浅出讲解Git
万门大学视频讲解Git
Pro Git中文手册
GitHub入门与实践电子书
本次示例所用仓库