不会写shell的程序员照样是好前端——用Node.JS实现git hooks

PYABess 8年前
   <p>来自: <a href="/misc/goto?guid=4959670312825858603" rel="nofollow">http://hao.jser.com/archive/10098/</a></p>    <h2>前言</h2>    <p>git hooks想必很多攻城狮都不陌生,官方对于hooks有详细的 <a href="/misc/goto?guid=4959670312911586141" rel="nofollow,noindex">文档</a> ,也有站内网友的文章 <a href="/misc/goto?guid=4959670313006157058" rel="nofollow,noindex">Git Hooks (1):介绍</a> , <a href="/misc/goto?guid=4959670313087976496" rel="nofollow,noindex">GIt Hooks (2):脚本分类</a> ,说的非常详细了,这里就不多做介绍,这里主要介绍一下如何写一个hook。</p>    <h2>一个基本的git hook长什么样?</h2>    <p>对git-hooks有一个入门认识的朋友都知道,hooks存放在git仓库的 .git/hooks 目录下,其中包括很多hooks,这些是在git 仓库创建的时候自动生成的,后缀名统一都是 .sample ,表示这些hooks都是默认不启用的,当把后缀名去掉之后,就变成了可以使用的hook。</p>    <h3>举个栗子</h3>    <p>pre-commit这个hook是在 git commit 的时候触发的hook,这个hook里面写了什么呢?代码我就不贴了,没啥劲,主要的几点就是:</p>    <ol>     <li> <p>这是一个shell脚本</p> </li>     <li> <p>这个脚本运行了一些东西然后退出了</p> </li>     <li> <p>退出的时候退出的错误码不是确定的</p> </li>    </ol>    <p>这就是一个hook的最基本的组成:在命令行执行git操作的时候,自动执行hooks目录下相应的可执行脚本,然后根据脚本的退出状态决定此次操作是否成功。当退出的错误码不为0的时候,表示失败,操作终止,否则操作继续。</p>    <h3>模拟场景</h3>    <p>如果现在有这样一个场景,在你的git仓库里,要求不允许提交 dist 目录,并且通过 mocha 的测试,否则不允许提交,用git hook 怎么做呢?</p>    <p>首先,这是在提交的时候的一个限制,所以应该考虑使用 pre-commit 这个hook,代码就不写了(不会写shell… Orz),整个过程如下:</p>    <ol>     <li> <p>检查是否有 dist 目录,如果没有的话下一步,否则退出,错误码置为1。</p> </li>     <li> <p>执行mocha命令进行测试,如果测试全部通过的话,退出,错误码为0,否则错误码为1,同样退出。</p> </li>    </ol>    <p>这样,当上述任何一步没有通过的时候,这个hook就会被终止,git-commit就无法通过,也就达到了限制提交的目的。</p>    <h2>shell脚本的局限性——不会写</h2>    <p>作为一名普通的前端,兼,一名不太合格的工程师,我对于shell脚本实在是不熟悉,连Linux命令都玩不转,别说写出666的shell脚本了,囧~ 所以要另辟巧径做这件事。</p>    <p>前端仔们对js应该是非常熟练的,所以如果能用js写hooks,那不就爽了?而Node.JS正好给了我们希望,感激涕零的话就不多说了,绝对感动到哭!</p>    <p>Node.js写起脚本来也非常简单,比如一个最简单的脚本</p>    <pre>  #!/usr/bin/env node    console.log('Hello World!');</pre>    <p>给脚本赋予可执行权限之后就完全可以当做shell脚本来跑了,麻麻再也不用担心我不会shell了。同样的,在hooks中我们也可以这样用。再举个栗子</p>    <p>还是刚才的场景,不允许有 dist 目录,同时通过所有mocha测试,用Node就可以这样写(这次我能show出代码了)</p>    <pre>  #!/usr/bin/env node  var fs = require('fs'),      spawnSync = require('child_process').spawnSync;  if(fs.existsSync('./dist')){      console.log('Commit Abort!Please remove dist directory.');      process.exit(1);  }  // 使用同步方法spawnSync执行mocha,测试的结果在result.status中,通过为0,不通过为1  var result = spawnSync('./node_modules/.bin/mocha',['test']);   if(result.status){      console.log('Commit Abort!Test failure.');  }  process.exit(result.status);</pre>    <p>这就是一个用Node.JS实现的基本的git-hook。</p>    <h2>Node.JS的局限性——不能动</h2>    <p>client-side hook的一个问题就是没法在随着仓库变动,如果项目成员多的话,每个人都需要在自己本地添加一次,hooks有变动了更新也比较麻烦。</p>    <p>解决方案</p>    <p>我个人对这个问题有一个简单解决方案,我做了一个仓库 <a href="/misc/goto?guid=4959670313176528450" rel="nofollow,noindex">git-hooks-node</a> ,每次写好git hooks之后通过自己写的工具进行build,生成一个类似于安装器的文件,然后提交到远程仓库,如 <a href="/misc/goto?guid=4959670313253994464" rel="nofollow,noindex">pre-commit.js</a> 是hook具体的内容, <a href="/misc/goto?guid=4959670313332005984" rel="nofollow,noindex">pre-commit.installer.js</a> 是生成的安装文件,也是一个脚本,github上的每一个文件都有相应的raw地址,如这个安装文件的地址为 <a href="/misc/goto?guid=4959670313417336388" rel="nofollow,noindex">raw pre-commit.installer.js</a> ,然后mac OS下的用户就可以使用 curl 获取脚本并运行,如下:</p>    <pre>  curl https://raw.githubusercontent.com/y8n/git-hooks-node/master/xgfe-ma/pre-commit.installer.js | node</pre>    <p>安装效果如下</p>    <p>这样只要写好一个hook并发布,项目成员只要知道地址就可以一键安转(想想还有点小激动呢)。这样虽然没有解决hook不会随着仓库移动的问题,但也提供了一种在项目组里通用一套hook的方案。</p>    <p>其他解决方法</p>    <p><a href="/misc/goto?guid=4959670313501756128" rel="nofollow,noindex">husky</a> 是GitHub上一个开源项目,它的做法是在 npm install 这个模块的时候自动在 .git/hooks 目录下创建很多hooks,然后再在package.json中指定每一个hook的执行脚本,如下</p>    <pre>  "scripts": {      "precommit": "npm test",      "prepush": "npm test",      "commit-msg": "./validate-commit-msg.js",      "...": "..."    }</pre>    <p>这样就可以把hooks随着项目变动,真正做到项目成员共用一个git hook,但问题就是必须在项目中依赖husky,不过想想这样的方法也比上面我的方法高明许多 -.-!</p>