三.gulp 常用插件

1.插件安装


在我们编写gulp代码时候,需要用到一些gulp的插件,可以通过npm install –save-dev 插件名称 来安装。如下代码来安装自动加载插件:

npm install –save-dev gulp-load-plugins
  要使用gulp的插件,首先得用require来把插件加载进来。

1
2
3
4
5
var gulp=require('gulp'),
plugins=require('gulp-load-plugins')(),
uglify = require('gulp-uglify'),
minifyHtml = require('gulp-minify-html'),
rename = require('gulp-rename');

  gulp的插件有很多种,后面介绍几个插件的用法。如还想了解更多插件,请查阅相关资料。

2.自动加载


ulp-load-plugins这个插件能自动帮你加载package.json文件里的gulp插件。例如假设你的package.json文件里的依赖是这样的:

1
2
3
4
5
6
7
8
{
"devDependencies": {
"gulp": "~3.6.0",
"gulp-rename": "~1.2.0",
"gulp-ruby-sass": "~0.4.3",
"gulp-load-plugins": "~0.5.1"
}
}

然后我们可以在gulpfile.js中使用gulp-load-plugins来帮我们加载插件:

1
2
3
var gulp = require('gulp');
//加载gulp-load-plugins插件,并马上运行它
var plugins = require('gulp-load-plugins')();

  然后我们要使用gulp-rename和gulp-ruby-sass这两个插件的时候,就可以使用plugins.rename和plugins.rubySass来代替了,也就是原始插件名去掉gulp-前缀,之后再转换为驼峰命名。

3.重命名


gulp-rename插件用来重命名文件流中的文件。用gulp.dest()方法写入文件时,文件名使用的是文件流中的文件名,如果要想改变文件名,那可以在之前用gulp-rename插件来改变文件流中的文件名。

1
2
3
4
5
6
7
8
9
var gulp = require('gulp'),
rename = require('gulp-rename'),
uglify = require("gulp-uglify");
gulp.task('rename', function () {
gulp.src('src/1.js')
.pipe(uglify()) //压缩
.pipe(rename('1.min.js')) //会将1.js重命名为1.min.js
.pipe(gulp.dest('js'));
});

4.js文件压缩


gulp-uglify插件用来压缩js文件。

1
2
3
4
5
6
7
var gulp = require('gulp'),
uglify = require("gulp-uglify");
gulp.task('minify-js', function () {
gulp.src('src/*.js') // 要压缩的js文件
.pipe(uglify()) //使用uglify进行压缩
.pipe(gulp.dest('dist/js')); //压缩后的路径
});

5.css文件压缩


gulp-minify-css插件用来压缩css文件。

1
2
3
4
5
6
7
var gulp = require('gulp'),
minifyCss = require("gulp-minify-css");
gulp.task('minify-css', function () {
gulp.src('src/*.css') // 要压缩的css文件
.pipe(minifyCss()) //压缩css
.pipe(gulp.dest('dist/css'));
});

6.html文件压缩


gulp-minify-html插件用来压缩html文件。

1
2
3
4
5
6
7
var gulp = require('gulp'),
minifyHtml = require("gulp-minify-html");
gulp.task('minify-html', function () {
gulp.src('src/*.html') // 要压缩的html文件
.pipe(minifyHtml()) //压缩
.pipe(gulp.dest('dist/html'));
});

7.js代码检查


使用gulp-jshint插件,用来检查js代码。

1
2
3
4
5
6
7
var gulp = require('gulp'),
jshint = require("gulp-jshint");
gulp.task('jsLint', function () {
gulp.src('src/*.js')
.pipe(jshint())
.pipe(jshint.reporter()); // 输出检查结果
});

8.文件合并


使用gulp-concat插件,用来把多个文件合并为一个文件,我们可以用它来合并js或css文件等。

1
2
3
4
5
6
7
var gulp = require('gulp'),
concat = require("gulp-concat");
gulp.task('concat', function () {
gulp.src('src/*.js') //要合并的文件
.pipe(concat('all.js')) // 合并匹配到的js文件并命名为 "all.js"
.pipe(gulp.dest('dist/js'));
});

9.图片压缩


可以使用gulp-imagemin插件来压缩jpg、png、gif等图片。

1
2
3
4
5
6
7
8
9
10
11
var gulp = require('gulp');
var imagemin = require('gulp-imagemin');
var pngquant = require('imagemin-pngquant'); //png图片压缩插件
gulp.task('default', function () {
return gulp.src('src/images/*')
.pipe(imagemin({
progressive: true,
use: [pngquant()] //使用pngquant来压缩png图片
}))
.pipe(gulp.dest('dist'));
});

10.自动刷新


使用gulp-livereload插件,当代码变化时,它可以帮我们自动刷新页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
var gulp = require('gulp'),
less = require('gulp-less'),
livereload = require('gulp-livereload');
gulp.task('less', function() {
gulp.src('less/\*.less')
.pipe(less())
.pipe(gulp.dest('css'))
.pipe(livereload());
});
gulp.task('watch', function() {
livereload.listen(); //要在这里调用listen()方法
gulp.watch('less/*.less', ['less']);
});

二.gulp api

1.gulp的工作方式


在介绍gulp API之前,我们首先来说一下gulp.js工作方式。在gulp中,使用的是Nodejs中的stream(流),首先获取到需要的stream,然后可以通过stream的pipe()方法把流导入到你想要的地方,比如gulp的插件中,经过插件处理后的流又可以继续导入到其他插件中,当然也可以把流写入到文件中。所以gulp是以stream为媒介的,它不需要频繁的生成临时文件,这也是我们应用gulp的一个原因。

  gulp的使用流程一般是:首先通过gulp.src()方法获取到想要处理的文件流,然后把文件流通过pipe方法导入到gulp的插件中,最后把经过插件处理后的流再通过pipe方法导入到gulp.dest()中,gulp.dest()方法则把流中的内容写入到文件中。例如:

1
2
3
var gulp = require('gulp');
gulp.src('script/jquery.js') // 获取流的api
.pipe(gulp.dest('dist/foo.js')); // 写放文件的api

2.globs的匹配规则


gulp用到的globs的匹配规则以及一些文件匹配技巧。

  gulp内部使用了node-glob模块来实现其文件匹配功能。我们可以使用下面这些特殊的字符来匹配我们想要的文件:

下面以例子来加深理解

* 能匹配 a.js,x.y,abc,abc/,但不能匹配a/b.js

*.* 能匹配 a.js,style.css,a.b,x.y

*/*/*.js 能匹配 a/b/c.js,x/y/z.js,不能匹配a/b.js,a/b/c/d.js

** 能匹配 abc,a/b.js,a/b/c.js,x/y/z,x/y/z/a.b,能用来匹配所有的目录和文件

**/*.js 能匹配 foo.js,a/foo.js,a/b/foo.js,a/b/c/foo.js

a/**/z 能匹配 a/z,a/b/z,a/b/c/z,a/d/g/h/j/k/z

a/**b/z 能匹配 a/b/z,a/sb/z,但不能匹配a/x/sb/z,因为只有单**单独出现才能匹配多级目录

?.js 能匹配 a.js,b.js,c.js

a?? 能匹配 a.b,abc,但不能匹配ab/,因为它不会匹配路径分隔符

[xyz].js 只能匹配 x.js,y.js,z.js,不会匹配xy.js,xyz.js等,整个中括号只代表一个字符

[^xyz].js 能匹配 a.js,b.js,c.js等,不能匹配x.js,y.js,z.js

3.获取流


src

 gulp.src()方法正是用来获取流的,但要注意这个流里的内容不是原始的文件流,而是一个虚拟文件对象流(Vinyl files),这个虚拟文件对象中存储着原始文件的路径、文件名、内容等信息。其语法为:

1
gulp.src(globs[, options]);

globs参数是文件匹配模式(类似正则表达式),用来匹配文件路径(包括文件名),当然这里也可以直接指定某个具体的文件路径。当有多个匹配模式时,该参数可以为一个数组;类型为String或 Array。

当有多种匹配模式时可以使用数组

1
2
//使用数组的方式来匹配多种文件
gulp.src(['js/\*.js','css/\*.css','*.html'])

options为可选参数。以下为options的选项参数:

options.buffer

类型: Boolean 默认值: true

  如果该项被设置为 false,那么将会以 stream 方式返回 file.contents 而不是文件 buffer 的形式。这在处理一些大文件的时候将会很有用。注意:插件可能并不会实现对 stream 的支持。

options.read

类型: Boolean 默认值: true

如果该项被设置为 false, 那么 file.contents 会返回空值(null),也就是并不会去读取文件。

options.base

类型: String , 设置输出路径以某个路径的某个组成部分为基础向后拼接。

如, 请想像一下在一个路径为 client/js/somedir 的目录中,有一个文件叫 somefile.js :

1
2
3
4
5
6
7
// 匹配 'client/js/somedir/somefile.js'现在 'base' 的值为 'client/js/'
gulp.src('client/js/\*\*/\*.js')
.pipe(minify())
.pipe(gulp.dest('build'));
gulp.src('client/js/\*\*/*.js', { base: 'client' })
.pipe(minify())
.pipe(gulp.dest('build'));

4.写文件


dest

gulp.dest()方法是用来写文件的,其语法为:

1
gulp.dest(path[,options])

path为写入文件的路径;

options为一个可选的参数对象,以下为选项参数:

options.cwd

类型: String 默认值: process.cwd()

输出目录的 cwd 参数,只在所给的输出目录是相对路径时候有效。

options.mode

类型: String 默认值: 0777

八进制权限字符,用以定义所有在输出目录中所创建的目录的权限。

1
2
3
var gulp = require('gulp');
gulp.src('script/jquery.js')  // 获取流
.pipe(gulp.dest('dist/foo.js')); // 写放文件

下面再说说生成的文件路径与我们给gulp.dest()方法传入的路径参数之间的关系。   gulp.dest(path)生成的文件路径是我们传入的path参数后面再加上gulp.src()中有通配符开始出现的那部分路径。例如:

1
2
3
4
var gulp = reruire('gulp'); //有通配符开始出现的那部分路径为 **/*.js
gulp.src('script/**/*.js')
.pipe(gulp.dest('dist')); //最后生成的文件路径为 dist/**/*.js
//如果 **/*.js 匹配到的文件为 jquery/jquery.js ,则生成的文件路径为 dist/jquery/jquery.js

用gulp.dest()把文件流写入文件后,文件流仍然可以继续使用。

5.监视文件


watch

gulp.watch()用来监视文件的变化,当文件发生变化后,我们可以利用它来执行相应的任务,例如文件压缩等。其语法为

gulp.watch(glob[, opts], tasks);

glob 为要监视的文件匹配模式,规则和用法与gulp.src()方法中的glob相同。 opts 为一个可选的配置对象,通常不需要用到。 tasks 为文件变化后要执行的任务,为一个数组。

1
2
3
4
5
6
7
8
9
gulp.task('uglify',function(){
//do something
});
gulp.task('reload',function(){
//do something
});
gulp.watch('js/**/*.js', ['uglify','reload']);
  gulp.watch()还有另外一种使用方式: 
gulp.watch(glob[, opts, cb]);

glob和opts参数与第一种用法相同;

cb参数为一个函数。每当监视的文件发生变化时,就会调用这个函数,并且会给它传入一个对象,该对象包含了文件变化的一些信息,type属性为变化的类型,可以是added,changed,deleted;path属性为发生变化的文件的路径。

gulp.watch(‘js/**/*.js’, function(event){
console.log(event.type); //变化类型 added为新增,deleted为删除,changed为改变
console.log(event.path); //变化的文件的路径
});

6.定义任务


task

gulp.task方法用来定义任务,内部使用的是Orchestrator(用于排序、执行任务和最大并发依赖关系的模块),其语法为:

1
gulp.task(name[, deps], fn)

name 为任务名;

deps 是当前定义的任务需要依赖的其他任务,为一个数组。当前定义的任务会在所有依赖的任务执行完毕后才开始执行。如果没有依赖,则可省略这个参数;

fn 为任务函数,我们把任务要执行的代码都写在里面。该参数也是可选的。

当你定义一个简单的任务时,需要传入任务名字和执行函数两个属性。

1
2
3
gulp.task('greet', function () {
console.log('Hello world!');
});

执行gulp greet的结果就是在控制台上打印出“Hello world”。

你也可以定义一个在gulp开始运行时候默认执行的任务,并将这个任务命名为“default”:

1
2
3
gulp.task('default', function () {
// Your default task
});

前面已经介绍了gulp.task的语法,但是当有多个任务时,需要知道怎么来控制任务的执行顺序。

可以通过任务依赖来实现。例如我想要执行one,two,three这三个任务,那我们就可以定义一个空的任务,然后把那三个任务当做这个空的任务的依赖就行了:

//只要执行default任务,就相当于把one,two,three这三个任务执行了

1
gulp.task('default',['one','two','three']);

  如果任务相互之间没有依赖,任务就会按你书写的顺序来执行,如果有依赖的话则会先执行依赖的任务。但是如果某个任务所依赖的任务是异步的,就要注意了,gulp并不会等待那个所依赖的异步任务完成,而是会接着执行后续的任务。例如:

1
2
3
4
5
6
7
8
9
10
gulp.task('one',function(){
//one是一个异步执行的任务
setTimeout(function(){
console.log('one is done')
},5000);
});
//two任务虽然依赖于one任务,但并不会等到one任务中的异步操作完成后再执行
gulp.task('two',['one'],function(){
console.log('two is done');
});

上面的例子中我们执行two任务时,会先执行one任务,但不会去等待one任务中的异步操作完成后再执行two任务,而是紧接着执行two任务。所以two任务会在one任务中的异步操作完成之前就执行了。

那如果我们想等待异步任务中的异步操作完成后再执行后续的任务,该怎么做呢?

有三种方法可以实现:
第一:在异步操作完成后执行一个回调函数来通知gulp这个异步任务已经完成,这个回调函数就是任务函数的第一个参数。

1
2
3
4
5
6
7
8
9
10
11
gulp.task('one',function(cb){ //cb为任务函数提供的回调,用来通知任务已经完成
//one是一个异步执行的任务
exec(function(){
console.log('one is finish');
cb(); //执行回调,表示这个异步任务已经完成
},5000);
});
//这时two任务会在one任务中的异步操作完成后再执行
gulp.task('two',['one'],function(){
console.log('two is finish');
});

第二:定义任务时返回一个流对象。适用于任务就是操作gulp.src获取到的流的情况。

1
2
3
4
5
6
7
8
9
gulp.task('one',function(cb){
var stream = gulp.src('client/**/*.js')
.pipe(exec()) //exec()中有某些异步操作
.pipe(gulp.dest('build'));
return stream;
});
gulp.task('two',['one'],function(){
console.log('two is done');
});

第三:返回一个promise对象,例如

1
2
3
4
5
6
7
8
9
10
11
var Q = require('q');
gulp.task('one', function() {
var deferred = Q.defer();
setTimeout(function() { // 执行异步的操作
deferred.resolve();
}, 1);
return deferred.promise;
});
gulp.task('two',['one'],function(){
console.log('two is done');
});

7.执行文件


run

 gulp.run()表示要执行的任务。可能会使用单个参数的形式传递多个任务。如下代码:

1
2
3
gulp.task('end',function(){
gulp.run('task1','task3','task2');
});

注意:任务是尽可能多的并行执行的,并且可能不会按照指定的顺序运行。

基础—JavaScript函数参数传递到底是值传递还是引用传递

在传统的观念里,都认为JavaScript函数传递的是引用传递(也称之为指针传递),也有人认为是值传递和引用传递都具备。那么JS的参数传递到底是怎么回事呢?事实上以下的演示也完全可以用于Java

首先来一个比较简单的,基本类型的传递:

1
2
3
4
5
6
7
8
function add(num){
num+=10;
return num;
}
num=10;
console.log(add(num));
console.log(num);
//输出20,10

对于这里的输出20,10,按照JS的官方解释就是在基本类型参数传递的时候,做了一件复制栈帧的拷贝动作,这样外部声明的变量num和函数参数的num,拥有完全相同的值,但拥有完全不同的参数地址,两者谁都不认识谁,在函数调用返回的时候弹出函数参数num栈帧。所以改变函数参数num,对原有的外部变量没有一点影响。

再来看一个较复杂的,对象引用类型的传递:

1
2
3
4
5
6
7
function setName(obj){
obj.name="ted";
}
var obj=new Object();
setName(obj);
console.log(obj.name);
//输出ted

以上代码的运行的实质是:创建了一个object对象,将其引用赋给obj(在C里面就直接是一个内存地址的赋值),然后在传递函数参数的时候,做了一件与前一个方法相同的事情,复制了一个栈帧给函数参数的obj,两者拥有相同的值(不妨将其理解为object对象的地址),然后在setName做改变的时候,事实上是改变了object对象自身的值(在JAVA里称之为可变类),在改变完成之后同样也要弹出函数参数obj对应的栈帧。

所以对应的输出是改变后object对象的值

那么可能有的朋友可能会问,这样也可以理解为一个引用传递(指针传递)呀?不,这里严格的说,在和JAVA类似的语言中,已经没有了指针,在JAVA里将上述过程称之为一个从符号引用到直接引用的解析过程。在C里面,指针就是一个具有固定长度的类型(在大多数的C编译器里是2个字节),但在JAVA类似的语言里,引用也有自己的属性和方法,只是你不能直接去访问和控制它,所以它从某种意义上也是一种对象,这种机制也很大程度的避免了内存泄露,术语称之为内存结构化访问机制。

为了证明上述观点,稍微改造下上述例子:

1
2
3
4
5
6
7
8
9
function setName(obj){
obj.name="zhangsan";
obj=new Object();
obj.name="lisi";
}
var obj=new Object();
setName(obj);
console.log(obj.name);
//输出zhangsan

这个例子与上一个例子的唯一不同是这里将一个新的对象赋给了函数参数obj,这样函数参数obj和原有的引用obj参数,有着完全不同的值和内存地址。

一.gulp简介

1.什么是gulp


gulp是前端开发过程中一种基于流的代码构建工具,是自动化项目的构建利器;她不仅能对网站资源进行优化,而且在开发过程中很多重复的任务能够使用正确的工具自动完成;使用她,不仅可以很愉快的编写代码,而且大大提高我们的工作效率。

  gulp是基于Nodejs的自动任务运行器, 她能自动化地完成 javascript、coffee、sass、less、html/image、css 等文件的测试、检查、合并、压缩、格式化、浏览器自动刷新、部署文件生成,并监听文件在改动后重复指定的这些步骤。在实现上,她借鉴了Unix操作系统的管道(pipe)思想,前一级的输出,直接变成后一级的输入,使得在操作上非常简单。

2.核心


流(stream)

流,简单来说就是建立在面向对象基础上的一种抽象的处理数据的工具。在流中,定义了一些处理数据的基本操作,如读取数据,写入数据等,程序员是对流进行所有操作的,而不用关心流的另一头数据的真正流向。流不但可以处理文件,还可以处理动态内存、网络数据等多种数据形式。

  而gulp正是通过流和代码优于配置的策略来尽量简化任务编写的工作。这看起来有点“像jQuery”的方法,把动作串起来创建构建任务。早在Unix的初期,流就已经存在了。流在Node.js生态系统中也扮演了重要的角色,类似于*nix将几乎所有设备抽象为文件一样,Node将几乎所有IO操作都抽象成了stream的操作。因此用gulp编写任务也可看作是用Node.js编写任务。当使用流时,gulp去除了中间文件,只将最后的输出写入磁盘,整个过程因此变得更快。

3.特点


易于使用

  通过代码优于配置的策略,gulp 让简单的任务简单,复杂的任务可管理。

构建快速

  利用 Node.js 流的威力,你可以快速构建项目并减少频繁的 IO 操作。

易于学习

  通过最少的 API,掌握 gulp 毫不费力,构建工作尽在掌握:如同一系列流管道。

插件高质

  gulp 严格的插件指南确保插件如你期望的那样简洁高质得工作。

4.安装


首先确保你已经正确安装了nodejs环境。然后以全局方式安装gulp:

1
npm install -g gulp

  全局安装gulp后,还需要在每个要使用gulp的项目中都单独安装一次。把目录切换到你的项目文件夹中,然后在命令行中执行:

1
npm install gulp

  如果想在安装的时候把gulp写进项目package.json文件的依赖中,则可以加上–save-dev:

1
npm install --save-dev gulp

  这样就完成了gulp的安装,接下来就可以在项目中应用gulp了。

5.使用


1.建立gulpfile.js文件

  gulp也需要一个文件作为它的主文件,在gulp中这个文件叫做gulpfile.js。新建一个文件名为gulpfile.js的文件,然后放到你的项目目录中。之后要做的事情就是在gulpfile.js文件中定义我们的任务了。下面是一个最简单的gulpfile.js文件内容示例,它定义了一个默认的任务。

1
2
3
4
var gulp = require('gulp');
gulp.task('default',function(){
console.log('hello world');
});

2.运行gulp任务
  要运行gulp任务,只需切换到存放gulpfile.js文件的目录(windows平台请使用cmd或者Power Shell等工具),然后在命令行中执行gulp命令就行了,gulp后面可以加上要执行的任务名,例如gulp task1,如果没有指定任务名,则会执行任务名为default的默认任务。

shell入门(1)

shell简介

Shell本身是一个用C语言编写的程序,它是用户使用Unix/Linux的桥梁,用户的大部分工作都是通过Shell完成的。Shell既是一种命令语言,又是一种程序设计语言。作为命令语言,它交互式地解释和执行用户输入的命令;作为程序设计语言,它定义了各种变量和参数,并提供了许多在高级语言中才具有的控制结构,包括循环和分支。

它虽然不是Unix/Linux系统内核的一部分,但它调用了系统核心的大部分功能来执行程序、建立文件并以并行的方式协调各个程序的运行。因此,对于用户来说,shell是最重要的实用程序,深入了解和熟练掌握shell的特性极其使用方法,是用好Unix/Linux系统的关键。

可以说,shell使用的熟练程度反映了用户对Unix/Linux使用的熟练程度。

Shell有两种执行命令的方式:

  • 交互式(Interactive):解释执行用户的命令,用户输入一条命令,Shell就解释执行一条。
  • 批处理(Batch):用户事先写一个Shell脚本(Script),其中有很多条命令,让Shell一次把这些命令执行完,而不必一条一条地敲命令。

Shell脚本和编程语言很相似,也有变量和流程控制语句,但Shell脚本是解释执行的,不需要编译,Shell程序从脚本中一行一行读取并执行这些命令,相当于一个用户把脚本中的命令一行一行敲到Shell提示符下执行。

Unix/Linux上常见的Shell脚本解释器有bash、sh、csh、ksh等,习惯上把它们称作一种Shell。我们常说有多少种Shell,其实说的是Shell脚本解释器。由于bash是最常见的脚本解释器,如无特殊说明,shell默认都是bash shell。

注意:bash是linux标准的默认shell,bash完全兼容sh,也就是说,用sh写的脚本可以不加修改的在bash中执行。

什么时候使用shell

因为Shell是各UNIX系统之间通用的功能,并且经过了POSIX的标准化。因此,Shell脚本只要“用心写”一次,即可应用到很多系统上。因此,之所以要使用Shell脚本是基于:

  • 简单性:Shell是一个高级语言;通过它,你可以简洁地表达复杂的操作。
  • 可移植性:使用POSIX所定义的功能,可以做到脚本无须修改就可在不同的系统上执行。
  • 开发容易:可以在短时间内完成一个功能强大又实用的脚本。

但是,考虑到Shell脚本的命令限制和效率问题,下列情况一般不使用Shell:

  1. 资源密集型的任务,尤其在需要考虑效率时(比如,排序,hash等等)。
  2. 需要处理大任务的数学操作,尤其是浮点运算,精确运算,或者复杂的算术运算(这种情况一般使用C++或FORTRAN 来处理)。
  3. 有跨平台(操作系统)移植需求(一般使用C 或Java)。
  4. 复杂的应用,在必须使用结构化编程的时候(需要变量的类型检查,函数原型,等等)。
  5. 对于影响系统全局性的关键任务应用。
  6. 对于安全有很高要求的任务,比如你需要一个健壮的系统来防止入侵、破解、恶意破坏等等。
  7. 项目由连串的依赖的各个部分组成。
  8. 需要大规模的文件操作。 需要多维数组的支持。
  9. 需要数据结构的支持,比如链表或数等数据结构。
  10. 需要产生或操作图形化界面 GUI。
  11. 需要直接操作系统硬件。
  12. 需要 I/O 或socket 接口。
  13. 需要使用库或者遗留下来的老代码的接口。
  14. 私人的、闭源的应用(shell 脚本把代码就放在文本文件中,全世界都能看到)。
    如果你的应用符合上边的任意一条,那么就考虑一下更强大的语言吧——或许是Perl、Tcl、Python、Ruby——或者是更高层次的编译语言比如C/C++,或者是Java。即使如此,你会发现,使用shell来原型开发你的应用,在开发步骤中也是非常有用的。

第一个shell脚本

打开文本编辑器vim,新建一个文件test,扩展名为sh(sh代表shell),全名是test.sh。扩展名并不影响脚本执行,见名知意就好,如果你用 php 写shell 脚本,扩展名就用 php 好了。

输入一些代码:

1
2
3
#!/bin/sh
#echo "123456"
echo "Hello World !"

第一行“#!” 是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种Shell。通常/bin/sh是指向/bin/bash的。
但后面的字符 # 表示注释,#后面的内容将被忽略。
echo命令用于向窗口输出文本。

运行Shell脚本有两种方法。

作为可执行程序

将上面的代码保存为test.sh,并 cd 到相应目录:

1
2
3
$ chmod +x ./test.sh  #使脚本具有执行权限
$ ./test.sh #执行脚本
Hello World !

注意,一定要写成./test.sh,而不是test.sh。运行其它二进制的程序也一样,直接写test.sh,linux系统会去PATH里寻找有没有叫test.sh的,而只有/bin, /sbin, /usr/bin,/usr/sbin等在PATH里,你的当前目录通常不在PATH里,所以写成test.sh是会找不到命令的,要用./test.sh告诉系统说,就在当前目录找。
通过这种方式运行bash脚本,第一行一定要写对,好让系统查找到正确的解释器。

###作为解释器参数

这种运行方式是,直接运行解释器,其参数就是shell脚本的文件名,如:

1
2
$ /bin/sh test.sh
$ /bin/php test.php

这种方式运行的脚本,不需要在第一行指定解释器信息,写了也没用。

再看一个例子。下面的脚本使用 read 命令从 stdin 获取输入并赋值给 PERSON 变量,最后在 stdout 上输出:

1
2
3
4
5
#!/bin/bash
# Author : shouliang
echo "What is your name?"
read PERSON
echo "Hello, $PERSON"

运行脚本:

1
2
3
4
5
$ chmod +x ./test.sh
$ ./test.sh
What is your name?
shouliang
Hello, shouliang

七.自定义Git

1.自定义Git


在安装Git一节中,我们已经配置了user.name和user.email,实际上,Git还有很多可配置项。

比如,让Git显示颜色,会让命令输出看起来更醒目:

1
$ git config --global color.ui true

这样,Git会适当地显示不同的颜色,比如git status命令:

文件名就会标上颜色。

2.忽略特殊文件


有些时候我们需要把一些文件例如:保存了数据库密码的配置文件放在Git目录下,但又不提交,那么需要我们在Git工作区的根目录下创建一个特殊的.gitignore文件,然后把要忽略的文件名填进去,Git就会自动忽略这些文件。

  不需要从头写.gitignore文件,GitHub已经为我们准备了各种配置文件,只需要组合一下就可以使用了。所有配置文件可以直接在线浏览:https://github.com/github/gitignore

忽略文件的原则是:

  • 忽略操作系统自动生成的文件,比如缩略图等;
  • 忽略编译生成的中间文件、可执行文件等,也就是如果一个文件是通过另一个文件自动生成的,那自动生成的文件就没必要放进版本库,比如Java编译产生的.class文件;
  • 忽略你自己的带有敏感信息的配置文件,比如存放口令的配置文件。

3.配置别名


配置别名其实就是把命令重新设置简单些,方便输入,例如:如果输入git st就表示git status。

1
$ git config --global alias.st status

现在都用co表示checkout,ci表示commit,br表示branch:

1
2
3
$ git config --global alias.co checkout
$ git config --global alias.ci commit
$ git config --global alias.br branch

–global参数是全局参数,也就是这些命令在这台电脑的所有Git仓库下都有用。

4.配置文件


配置Git的时候,加上–global是针对当前用户起作用的,如果不加,那只针对当前的仓库起作用。

配置文件放哪了?每个仓库的Git配置文件都放在.git/config文件中:

$ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote “origin”]
url = git@github.com:michaelliao/learngit.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch “master”]
remote = origin
merge = refs/heads/master
[alias]
last = log -1

别名就在[alias]后面,要删除别名,直接把对应的行删掉即可。

而当前用户的Git配置文件放在用户主目录下的一个隐藏文件.gitconfig中:

$ cat .gitconfig
[alias]
co = checkout
ci = commit
br = branch
st = status
[user]
name = Your Name
email = your@email.com

配置别名也可以直接修改这个文件,如果改错了,可以删掉文件重新通过命令配置。

5.搭建Git服务器


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

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

第一步,安装git:

$ sudo apt-get install git

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

$ sudo adduser git

第三步,创建证书登录:

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

第四步,初始化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

第五步,禁用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每次一登录就自动退出。

第六步,克隆远程仓库:

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

$ git clone git@server:/srv/sample.git
Cloning into 'sample'...
warning: You appear to have cloned an empty repository.

6.管理公钥和权限


管理公钥

  如果团队很小,把每个人的公钥收集起来放到服务器的/home/git/.ssh/authorized_keys文件里就是可行的。如果团队有几百号人,就没法这么玩了,这时,可以用Gitosis来管理公钥。

管理权限

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

这里我们也不介绍Gitolite了,不要把有限的生命浪费到权限斗争中。

主要两点:

  • 要方便管理公钥,用Gitosis;
  • 要像SVN那样变态地控制权限,用Gitolite。

六.标签管理

1.标签的简介


本节主要记录的Git标签的作用、标签的多种创建方式,以及标签的删除,与推送,和使用GitHub的Fork参与别人的项目。

标签的作用
  发布版本时,通常先在版本库中打一个标签,这样,就唯一确定了打标签时刻的版本。无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来。所以,标签也是版本库的一个快照。Git的标签虽然是版本库的快照,但其实它就是指向某个commit的指针(跟分支很像,但是分支可以移动,标签不能移动),所以,创建和删除标签都是瞬间完成的。   

2.创建标签


在Git中打标签非常简单,首先,切换到需要打标签的分支上:

$ git branch
*dev
master
$ git checkout master
Switched to branch ‘master’
然后,敲命令git tag 就可以打一个新标签:

1
$ git tag v1.0

  默认标签是打在最新提交的commit上的。还可以对历史提交打上标签,只要找到历史提交的commit id,然后打上就可以了,例如要对add merge这次提交打标签,它对应的commit id是6224937,输入命令:

1
$ git tag v0.9 6224937

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

1
$ git tag -a v0.1 -m "version 0.1 released" 3628164

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

1
2
3
4
5
$ git show v0.1
tag v0.1
Tagger: hubwiz <hubwiz@163.com>
Date: Mon Aug 26 07:28:11 2015 +0800
version 0.1 released

  签名采用PGP签名,因此,必须首先安装gpg(GnuPG),如果没有找到gpg,或者没有gpg密钥对,就会报错:

1
2
3
gpg: signing failed: secret key not available
error: gpg failed to sign the data
error: unable to sign the tag

  如果报错,请参考GnuPG帮助文档配置Key。

  • 命令git tag 用于新建一个标签,默认为HEAD,也可以指定一个commit id;
  • git tag -a -m “blablabla…”可以指定标签信息;
  • git tag -s -m “blablabla…”可以用PGP签名标签;
  • 命令git tag可以查看所有标签。

3.操作标签


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

1
2
$ git tag -d v0.1
Deleted tag 'v0.1' (was e078af9)

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

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

1
2
3
4
$ git push origin v1.0
Total 0 (delta 0), reused 0 (delta 0)
To git@github.com:michaelliao/learngit.git
* [new tag] v1.0 -> v1.0

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

1
2
3
4
5
6
7
$ git push origin --tags
Counting objects: 1, done.
Writing objects: 100% (1/1), 554 bytes, done.
Total 1 (delta 0), reused 0 (delta 0)
To git@github.com:michaelliao/learngit.git
* [new tag] v0.2 -> v0.2
* [new tag] v0.9 -> v0.9

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

1
2
$ git tag -d v0.9
Deleted tag 'v0.9' (was 6224937)

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

1
2
3
$ git push origin :refs/tags/v0.9
To git@github.com:michaelliao/learngit.git
- [deleted] v0.9

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

本节知识点主要学习点:

  • git push origin 可以推送一个本地标签;
  • git push origin –tags可以推送全部未推送过的本地标签;
  • git tag -d 可以删除一个本地标签;
  • git push origin :refs/tags/可以删除一个远程标签。

4.GitHub的使用


GitHub不仅是免费的远程仓库,个人的开源项目,可以放到GitHub上,而且GitHub还是一个开源协作社区,通过GitHub,既可以让别人参与你的开源项目,也可以参与别人的开源项目。

  在GitHub上,利用Git极其强大的克隆和分支功能,人们可以自由参与各种开源项目。比如人气极高的bootstrap项目,这是一个非常强大的CSS框架,在它的项目主页,点“Fork”就在自己的账号下克隆了一个bootstrap仓库,然后,从自己的账号下clone。一定要从自己的账号下clone仓库,这样你才能推送修改。如果从bootstrap的作者的仓库地址git@github.com:twbs/bootstrap.git克隆,因为没有权限,你将不能推送修改。

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

如果你希望bootstrap的官方库能接受你的修改,你就可以在GitHub上发起一个pull request。

本节主要内容:

  • 在GitHub上,可以任意Fork开源仓库;
  • 自己拥有Fork后的仓库的读写权限;
  • 可以推送pull request给官方仓库来贡献代码。

五.分支管理

1.分支管理图文详解一


在版本回退里,你已经知道,每次提交,Git都把它们串成一条时间线,这条时间线就是一个分支。截止到目前,只有一条时间线,在Git里,这个分支叫主分支,即master分支。HEAD严格来说不是指向提交,而是指向master,master才是指向提交的,所以,HEAD指向的就是当前分支。

  一开始的时候,master分支是一条线,Git用master指向最新的提交,再用HEAD指向master,就能确定当前分支,以及当前分支的提交点:
  

每次提交,master分支都会向前移动一步,这样,随着你不断提交,master分支的线也越来越长,当我们创建新的分支,例如dev时,Git新建了一个指针叫dev,指向master相同的提交,再把HEAD指向dev,就表示当前分支在dev上:

你看,Git创建一个分支很快,因为除了增加一个dev指针,改改HEAD的指向,工作区的文件都没有任何变化!

2.分支管理图文详解二


接着分支管理图文详解一,现在对工作区的修改和提交就是针对dev分支了,比如新提交一次后,dev指针往前移动一步,而master指针不变:

假如我们在dev上的工作完成了,就可以把dev合并到master上。Git怎么合并呢?最简单的方法,就是直接把master指向dev的当前提交,就完成了合并:

所以Git合并分支也很快!就改改指针,工作区内容也不变!

合并完分支后,甚至可以删除dev分支。删除dev分支就是把dev指针给删掉,删掉后,我们就剩下了一条master分支:

3.创建分支


首先,我们创建dev分支,然后切换到dev分支:

1
2
$ git checkout -b dev
Switched to a new branch 'dev'

git checkout命令加上-b参数表示创建并切换,相当于以下两条命令:

$ git branch dev
$ git checkout dev
Switched to branch ‘dev’

然后,用git branch命令查看当前分支:

$ git branch
*dev
master

git branch命令会列出所有分支,当前分支前面会标一个*号。

然后,我们就可以在dev分支上正常提交,比如对readme.txt做个修改,加上一行:

Creating a new branch is quick.

然后提交:

$ git add readme.txt
$ git commit -m “branch test”
[dev fec145a] branch test
1 file changed, 1 insertion(+)

现在,dev分支的工作完成,我们就可以切换回master分支:

1
2
$ git checkout master
Switched to branch 'master'

切换回master分支后,再查看一个readme.txt文件,刚才添加的内容不见了!因为那个提交是在dev分支上,而master分支此刻的提交点并没有变:

4.合并分支


我们把前面dev分支的工作成果合并到master分支上:

$ git merge dev
Updating d17efd8..fec145a
Fast-forward
readme.txt | 1 +
1 file changed, 1 insertion(+)

  git merge命令用于合并指定分支到当前分支。合并后,再查看readme.txt的内容,就可以看到,和dev分支的最新提交是完全一样的。

  注意到上面的Fast-forward信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master指向dev的当前提交,所以合并速度非常快。

  当然,也不是每次合并都能Fast-forward,我们后面会将其他方式的合并。

5.删除分支


合并完成后,就可以放心地删除dev分支了:

1
2
$ git branch -d dev
Deleted branch dev (was 5659891).

删除后,查看branch,就只剩下master分支了:

1
2
$ git branch
* master

因为创建、合并和删除分支非常快,所以Git鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在master分支上工作效果是一样的,但过程更安全。

前面所讲知识:汇总下这些使用命令:

查看分支:git branch

创建分支:git branch

切换分支:git checkout

创建+切换分支:git checkout -b

合并某分支到当前分支:git merge

删除分支:git branch -d

6.产生冲突


当我们进行合并分支往往会产生冲突。

在准备新的feature1分支,继续我们的新分支开发

1
$ git checkout -b feature1

修改readme.txt最后一行,改为Creating a new branch is quick AND simple.

在feature1分支上提交

1
2
$ git add readme.txt 
$ git commit -m "AND simple"

切换到master分支

1
2
3
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 1 commit.

Git还会自动提示我们当前master分支比远程的master分支要超前1个提交。

在master分支上把readme.txt文件的最后一行改为Creating a new branch is quick & simple.提交:

1
2
$ git add readme.txt 
$ git commit -m "& simple"

7.解决冲突


在上节中master分支和feature1分支各自都分别有新的提交,变成了这样:

这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突,执行git merge feature1,在看readme.txt:

1
2
3
4
5
<<<<<<< HEAD
Creating a new branch is quick & simple.
=======
Creating a new branch is quick AND simple.
>>>>>>> feature1

我们把冲突的内容修改为Creating a new branch is quick and simple.,提交:

1
2
$ git add readme.txt 
$ git commit -m "conflict fixed"

现在,master分支和feature1分支变成了下图所示:

用带参数的git log也可以看到分支的合并情况:

1
$ git log --graph --pretty=oneline --abbrev-commit

最后,删除feature1分支:

1
2
$ git branch -d feature1
Deleted branch feature1.

冲突解决,最后,删除feature1分支 git branch -d feature1。当Git无法自动合并分支时,就必须首先解决冲突。解决冲突后,再提交,合并完成。

用git log –graph命令可以看到分支合并图。

8.Bug分支


如果你有一个bug任务,你想创建一个分支issue-101来修复它,但是你当前正在dev上进行的工作还没有完成而不能提交,bug需要现在修复,所以现在你需要暂停dev上工作,Git提供了一个stash功能,可以把当前工作现场“储藏”起来,等以后恢复现场后继续工作:$ git stash。

  假定需要在master分支上修复,就从master创建临时分支:

1
2
3
4
$ git checkout master
Switched to branch 'master'
$ git checkout -b issue-101
Switched to a new branch 'issue-101'

  现在修复bug,需要把“Git is free software …”改为“Git is a free software …”,然后提交:

1
2
$ git add readme.txt 
$ git commit -m "fix bug 101"

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

1
2
3
4
5
6
$ git checkout master
...
$ git merge --no-ff -m "merged bug fix 101" issue-101
...
$ git branch -d issue-101
Deleted branch issue-101 (...).

  Git把stash内容存在某个地方了,但是需要恢复一下,有两个办法:

  • 一是用git stash apply恢复,但是恢复后,stash内容并不删除,你需要用git stash drop来删除;
  • 另一种方式是用git stash pop,恢复的同时把stash内容也删了:

9.Feature分支


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

  现在新功能开发代号为Vulcan:

1
2
$ git checkout -b feature-vulcan
Switched to a new branch 'feature-vulcan'

  开发完毕,添加并提交:

1
2
$ git add vulcan.py
$ git commit -m "add feature vulcan"

切回dev,准备合并:

1
$ git checkout dev

  一切顺利的话,feature分支和bug分支是类似的,合并,然后删除。

  由于种种原因,此功能又不需要了,现在这个分支需要就地销毁:

1
2
3
$ git branch -d feature-vulcan
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分支还没有被合并,如果删除,将丢失掉修改,如果要强行删除,需要使用命令git branch -D feature-vulcan。

  现在我们强行删除:

1
2
$ git branch -D feature-vulcan
Deleted branch feature-vulcan (was 756d4af).

  注意:开发一个新feature,最好新建一个分支;如果要丢弃一个没有被合并过的分支,可以通过git branch -D 强行删除。

10.推送分支


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

  要查看远程库的信息,用git remote

1
2
$ git remote
origin

  或者,用git remote -v显示更详细的信息:

1
$ git remote -v

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

1
git push origin master

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

1
git push origin dev

  • master分支是主分支,因此要时刻与远程同步;
  • dev分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;
  • bug分支只用于在本地修复bug,就没必要推到远程了,除非老板要看看你每周到底修复了几个bug;
  • feature分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。   

    11.多人协助


多人协作时,大家都会往master和dev分支上推送各自的修改。当你的同事也克隆一份此项目从远程库,默认情况下,只能看到本地的master分支。现在,你的同事要在dev分支上开发,就必须创建远程origin的dev分支到本地,于是他用这个命令创建本地dev分支:

1
$ git checkout -b dev origin/dev

  现在,他就可以在dev上继续修改,然后,时不时地把dev分支push到远程,并且已经向origin/dev分支推送了他的提交,这时你也对同样的文件作了修改,并试图推送,推送失败,因为你的同事的最新提交和你试图推送的提交有冲突,解决办法也很简单,Git已经提示我们,先用git pull把最新的提交从origin/dev抓下来,然后,在本地合并,解决冲突,再推送,git pull也失败了,原因是没有指定本地dev分支与远程origin/dev分支的链接,根据提示,设置dev和origin/dev的链接。

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

1
2
3
$ git commit -m "merge & fix hello.py"
[dev adca45d] merge & fix hello.py
$ git push origin dev

  因此,多人协作的工作模式通常是这样:

  • 首先,可以试图用git push origin branch-name推送自己的修改;
  • 如果推送失败,则因为远程分支比你的本地更新,需要先用git pull试图合并;
  • 如果合并有冲突,则解决冲突,并在本地提交;
  • 没有冲突或者解决掉冲突后,再用git push origin branch-name推送就能成功!
      如果git pull提示“no tracking information”,则说明本地分支和远程分支的链接关系没有创建,用命令git branch –set-upstream branch-name origin/branch-name。

四.远程仓库

1.远程仓库简介


到目前为止,我们已经掌握了如何在Git仓库里对一个文件进行时光穿梭,你再也不用担心文件备份或者丢失的问题了。

  开始介绍Git的远程仓库功能。我们现在借用GitHub神奇的网站,这个网站就是提供Git仓库托管服务的,所以只要注册一个GitHub账号,就可以免费获得Git远程仓库。

  由于你的本地Git仓库和GitHub仓库之间的传输是通过SSH加密的,所以,需要一点设置:

  第1步:创建SSH Key。在当前目录下,看看有没有.ssh目录,如果有,再看看这个目录下有没有id_rsa和id_rsa.pub这两个文件,如果已经有了,可直接跳到下一步。如果没有,打开Shell(Windows下打开Git Bash),创建SSH Key:

$ ssh-keygen -t rsa -C “youremail@example.com
  你需要把邮件地址换成你自己的邮件地址,然后一路回车,使用默认值即可,可以在用户主目录里找到.ssh目录,里面有id_rsa和id_rsa.pub两个文件,这两个就是SSH Key的秘钥对,id_rsa是私钥,不能泄露出去,id_rsa.pub是公钥,可以放心地告诉任何人。

  第2步:登陆GitHub,打开“Account settings”,“SSH Keys”页面:

  然后,点“Add SSH Key”,填上任意Title,在Key文本框里粘贴id_rsa.pub文件的内容:
  

2.GitHub创建新仓库


GitHub创建一个Git仓库,并且本地仓库与此仓库进行远程同步,此仓库既可以作为备份,又可以让其他人通过该仓库来协作。

  首先,登陆GitHub,然后,在右上角找到“Create a new repo”按钮,创建一个新的仓库:
  
  
  
 在Repository name填入learngit,其他保持默认设置,点击“Create repository”按钮,就成功地创建了一个新的Git仓库。

  目前,在GitHub上的这个learngit仓库还是空的,GitHub告诉我们,可以从这个仓库克隆出新的仓库,也可以把一个已有的本地仓库与之关联,然后,把本地仓库的内容推送到GitHub仓库。

  现在,我们根据GitHub的提示,在本地的learngit仓库下运行命令:

1
$ git remote add origin git@github.com:onlyone/learngit.git

  请千万注意,把上面的onlyone替换成你自己的GitHub账户名,否则,你在本地关联的就是我的远程库,关联没有问题,但是你以后推送是推不上去的,因为你的SSH Key公钥不在我的账户列表中。

  添加后,远程库的名字就是origin,这是Git默认的叫法,也可以改成别的,但是origin这个名字一看就知道是远程库。

  注意:要关联一个远程库,使用命令git remote add origin git@server-name:path/repo-name.git。    

3.本地库推送到远程库


本地库的所有内容推送到远程库上:

$ git push -u origin master
  把本地库的内容推送到远程,用git push命令,实际上是把当前分支master推送到远程。

  由于远程库是空的,我们第一次推送master分支时,加上了-u参数,Git不但会把本地的master分支内容推送的远程新的master分支,还会把本地的master分支和远程的master分支关联起来,在以后的推送或者拉取时就可以简化命令。

  推送成功,github内容与当前目录中readme.txt一致:
  
  
  
从现在起,只要本地作了提交,就可以通过命令:

1
$ git push origin master

  把本地master分支的最新修改推送至GitHub,现在,你就拥有了真正的分布式版本库!

  注意:要关联一个远程库,使用命令git remote add origin git@server-name:path/repo-name.git;关联后,使用命令git push -u origin master第一次推送master分支的所有内容;此后,每次本地提交后,只要有必要,就可以使用命令git push origin master推送最新修改;   

4.SSH警告


当你第一次使用Git的clone或者push命令连接GitHub时,会得到一个警告:

1
2
3
The authenticity of host 'github.com (xx.xx.xx.xx)' can't be established.
RSA key fingerprint is xx.xx.xx.xx.xx.
Are you sure you want to continue connecting (yes/no)?

  这是因为Git使用SSH连接,而SSH连接在第一次验证GitHub服务器的Key时,需要你确认GitHub的Key的指纹信息是否真的来自GitHub的服务器,输入yes回车即可。

  Git会输出一个警告,告诉你已经把GitHub的Key添加到本机的一个信任列表里了:

1
Warning: Permanently added 'github.com' (RSA) to the list of known hosts.

  这个警告只会出现一次,后面的操作就不会有任何警告了。

  如果你实在担心有人冒充GitHub服务器,输入yes前可以对照GitHub的RSA Key的指纹信息是否与SSH连接给出的一致。

5.从远程库克隆


从远程库克隆,就需要我们先创建远程库,在github创建一个新的gitskills仓库,我们勾选Initialize this repository with a README,这样GitHub会自动为我们创建一个README.md文件。创建完毕后,可以看到README.md文件:

  现在,远程库已经准备好了,下一步是用命令git clone克隆一个本地库:

1
2
3
4
5
$ git clone git@github.com:michaelliao/gitskills.git

$ cd gitskills
$ ls
README.md

  注意把Git库的地址换成你自己的,然后进入gitskills目录看看,已经有README.md文件了。

  你也许还注意到,GitHub给出的地址不止一个,还可以用github.com/onlyone/gitskills.git这样的地址。实际上,Git支持多种协议,默认的git://使用ssh,但也可以使用https等其他协议。

  注意:要克隆一个仓库,首先必须知道仓库的地址,然后使用git clone命令克隆。

  Git支持多种协议,包括https,但通过ssh支持的原生git协议速度最快。

三.操作Git

1.status命令与diff命令


前面我们已经成功地添加并提交了一个readme.txt文件,修改readme.txt如下:

1
2
echo "Git is a distributed version control system. " > readme.txt
echo "Git is free software." >> readme.txt

  运行git status命令看看结果:

1
2
3
$ git status
...
no changes added to commit (use "git add" and/or "git commit -a")

  git status命令可以让我们时刻掌握仓库当前的状态,上面显示,readme.txt被修改过了,但还没有准备提交的修改。

  git diff这个命令看看:

1
2
3
4
5
$ git diff readme.txt 
...
-Git is version control system.
+Git is a distributed version control system.
Git is free software

  git diff顾名思义就是查看difference,显示的格式正是Unix通用的diff格式,可以从上面的命令输出看到,我们在第一行添加了一个“distributed”单词。

  readme.txt作了什么修改后,再把它提交到仓库,提交修改和提交新文件是一样的两步,git add和git commit:

1
2
$ git add readme.txt
$ git commit -m "add distributed"

注意

  • 要随时掌握工作区的状态,使用git status命令。
  • 如果git status告诉你有文件被修改过,用git diff可以查看修改内容。

2.版本回退


现在,再练习一次,修改readme.txt文件如下:

1
2
echo "Git is a distributed version control system." > readme.txt
echo "Git is free software distributed under the GPL." >> readme.txt

  我们再次提交一次readme.txt

1
2
$ git add readme.txt
$ git commit -m "append GPL"

  我们现在已经提交多次文件,想看看有那些?版本控制系统肯定有某个命令可以告诉我们历史记录,在Git中,我们用git log命令查看:

1
2
3
4
5
6
7
8
9
10
$ git log
commit 3628164fb26d48395383f8f31179f24e0882e1e0
Date: Tue Oct 25 15:11:49 2015 +0000
append GPL
commit ea34578d5496d7dd233c827ed32a8cd576c5ee85
Date: Tue Oct 25 14:53:12 2015 +0000
add distributed
commit cb926e7ea50ad11b8f9e909c05226233bf755030
Date: Mon Oct 24 17:51:55 2015 +0000
wrote a readme file

  git log命令显示从最近到最远的提交日志,我们可以看到3次提交,最近的一次是append GPL,上一次是add distributed,最早的一次是wrote a readme file。commit 36281**2e1e0是commit id(版本号)。如果嫌输出信息太多,可以使用$ git log –pretty=oneline,此时你看到的一大串类似3628164…882e1e0的是commit id(版本号)。

  每提交一个新版本,实际上Git就会把它们自动串成一条时间线。现在准备把readme.txt回退到上一个版本,也就是“add distributed”的那个版本,怎么做呢?

  首先,Git必须知道当前版本是哪个版本,在Git中,用HEAD表示当前版本,也就是最新的提交3628164…882e1e0(注意我的提交ID和你的肯定不一样),上一个版本就是HEAD^,上上一个版本就是HEAD^^,当然往上100个版本写100个^比较容易数不过来,所以写成HEAD~100。

  现在,我们要把当前版本“append GPL”回退到上一个版本“add distributed”,就可以使用git reset命令:

1
2
$ git reset --hard HEAD^
HEAD is now at ea34578 add distributed

3.重新恢复到新版本


接着上节版本回退,还可以继续回退到上一个版本wrote a readme file,不过我们现在看看版本库的状态git log:

1
$ git log

  最新的那个版本append GPL已经看不到了!好比你从21世纪坐时光穿梭机来到了19世纪,想再回去已经回不去了,肿么办?

  只要之前的命令行结果还在,就可以找到那个append GPL的commit id是3628164…,于是就可以指定回到未来的某个版本:

1
2
$ git reset --hard 3628164
HEAD is now at 3628164 append GPL

  版本号没必要写全,前几位就可以了,Git会自动去找。当然也不能只写前一两位,因为Git可能会找到多个版本号,就无法确定是哪一个了。

  可以查看readme.txt的内容$ cat readme.txt.

  Git的版本回退速度非常快,因为Git在内部有个指向当前版本的HEAD指针,当你回退版本的时候,Git仅仅是把HEAD从指向append GPL:


改为指向add distributed:

4.git reflog命令


现在,你回退到了某个版本,当想恢复到新版本怎么办?找不到新版本的commit id怎么办?

  在Git中可以放心下。当你用$ git reset –hard HEAD^回退到add distributed版本时,再想恢复到append GPL,就必须找到append GPL的commit id。Git提供了一个命令git reflog用来记录你的每一次命令:

1
2
3
4
5
$ git reflog
ea34578 HEAD@{0}: reset: moving to HEAD^
3628164 HEAD@{1}: commit: append GPL
ea34578 HEAD@{2}: commit: add distributed
cb926e7 HEAD@{3}: commit (initial): wrote a readme file

  这样可以看到,第二行显示append GPL的commit id是3628164,这样我们就可以重新找到了。

  注意,我们从这两节中可以了解到:

HEAD指向的版本就是当前版本,因此,Git允许我们在版本的历史之间穿梭,使用命令git reset –hard commit_id。
穿梭前,用git log可以查看提交历史,以便确定要回退到哪个版本。
要重返未来,用git reflog查看命令历史,以便确定要回到未来的哪个版本。

5.工作区


工作区:就是你在电脑里能看到的目录,learngit文件夹就是一个工作区,比如我们环境中当前的目录。

6.暂存区


版本库:工作区有一个隐藏目录.git 这个不算工作区,而是Git的版本库。

暂存区:英文叫stage,或index。一般存放在git 目录下的index文件(.git/index)中,所以我们把暂存区时也叫作索引(index).

Git的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master,以及指向master的一个指针叫HEAD。

7.暂存区实践


现在我们对readme.txt做个修改,比如追加一行内容:

1
echo "Git has a mutable index called stage." >> readme.txt

然后,在工作区新增一个LICENSE文本文件

1
echo "LICENSE is a new file." > LICENSE

用git status查看一下状态,Git显示结果,readme.txt被修改了,而LICENSE还从来没有被添加过,所以它的状态是Untracked。

现在,使用两次命令git add,把readme.txt和LICENSE都添加后,用git status再查看一下,通过图可以理解为


所以,git add命令实际上就是把要提交的所有修改放到暂存区(Stage),然后,执行git commit就可以一次性把暂存区的所有修改提交到分支。

1
$ git commit -m "understand how stage works"

一旦提交后,如果你又没有对工作区做任何修改,用git status查看下,没有任何内容,现在版本库变成了这样,暂存区就没有任何内容了:

8.管理修改


Git与其他版本控制系统相比,Git跟踪并管理的是修改,而非文件。

  为什么说Git管理的是修改,而不是文件。接下来做测试,我们对readme.txt修改,追加一行内容:

1
echo "Git tracks changes." >> readme.txt

  然后通过git add添加

1
2
$ git add readme.txt
$ git status

  接下来,我们再次修改readme.txt内容,把最后一行内容修改为:

1
Git tracks changes of files.

  git commit提交

1
2
3
$ git commit -m "git tracks changes"
[master d4f25b6] git tracks changes
1 file changed, 1 insertion(+)

  通过git status查看每次操作的状态,可以看出第二次修改 -> git commit,没有被提交。

  Git管理的是修改,当你用git add命令后,在工作区的第一次修改被放入暂存区,准备提交,但是,在工作区的第二次修改并没有放入暂存区,所以,git commit只负责把暂存区的修改提交了,也就是第一次的修改被提交了,第二次的修改不会被提交。

Git是如何跟踪修改的,每次修改,如果不add到暂存区,那就不会加入到commit中。

9.撤销修改


如果你在readme.txt中加入了一行文件,又感觉不好,你可以删除新加的,恢复到原来的。

Git会告诉你,git checkout – file可以丢弃工作区的修改:

1
$ git checkout -- readme.txt

命令git checkout – readme.txt意思就是,把readme.txt文件在工作区的修改全部撤销,这里有两种情况:

  • 一种是readme.txt自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;
  • 一种是readme.txt已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。
    总之,就是让这个文件回到最近一次git commit或git add时的状态。

现在,看看readme.txt的文件内容:

1
2
3
4
5
$ cat readme.txt
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.

现在来看几种情况,如何撤销修改

1:当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时,用命令git checkout – file。

2:当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改,分两步:

第一步用命令git reset HEAD file,就回到了1;
第二步按1操作。
3:已经提交了不合适的修改到版本库时,想要撤销本次提交,参考版本回退一节,不过前提是没有推送到远程库。

10.删除文件


在Git中,删除也是一个修改操作,先添加一个新文件test.txt到Git并且提交:

1
2
3
4
5
$ git add test.txt
$ git commit -m "add test.txt"
[master 94cdc44] add test.txt
1 file changed, 1 insertion(+)
create mode 100644 test.txt

一般情况下,你通常直接在文件管理器中把没用的文件删了,或者用rm命令删了:

1
$ rm test.txt

这个时候,Git知道你删除了文件,因此,工作区和版本库就不一致了,git status命令会立刻告诉你哪些文件被删除了:

1
2
3
$ git status
...
no changes added to commit (use "git add" and/or "git commit -a")

现在你有两个选择,一是确实要从版本库中删除该文件,那就用命令git rm删掉,并且git commit:

1
2
3
$ git rm test.txt
rm 'test.txt'
$ git commit -m "remove test.txt"

现在,文件就从版本库中被删除了。

另一种情况是删错了,因为版本库里还有呢,所以可以很轻松地把误删的文件恢复到最新版本:

1
$ git checkout -- test.txt

git checkout其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。

  命令git rm用于删除一个文件。如果一个文件已经被提交到版本库,那么你永远不用担心误删,但是要小心,你只能恢复文件到最新版本,你会丢失最近一次提交后你修改的内容。