【优化】记一次通过工具减少 Git 冲突

2020-11-18  本文已影响0人  Gopal

起因

当我们的项目越来越大的时候,Git 冲突是团队协作中令人非常苦恼的事情,不仅仅浪费了我们时间,而且很容易解决冲突的时候出现问题。

我能想到的就是模块细分化,每个人都负责自己相关的模块,这样开发者之间的代码就不会相互影响,也就不会有代码的冲突。但多人协作项目中公共的代码必不可少,比如我们常见的公共变量文件 constant.js,并且大部分的冲突来源于此。

基于此,Leader 提出了另外一个解决方案——控制书写代码的顺序

为什么顺序那么重要

对于这个问题,我们要先清楚,为什么会有代码冲突?

因为我们改了同一个文件中同一行的代码

举个例子,比如我们常量定义中有如下:

export const Employees = {
  Andy: 'I can sing',
  Oliver: 'I can run',
  Ivan: 'I can rap'
};

同事 A 和我都要改到 Employees 这个常量,他添加 Brad: 'I am Brad' , 我添加了 Patrick: 'I am Patrick',我们都加到了该变量的后面,就会导致冲突,类似如下

@@@ -1,4 -1,4 +1,8 @@@
  Andy: 'I can sing',
  Oliver: 'I can run',
  Ivan: 'I can rap',
  <<<<<<< HEAD
 +Brad: 'I am Brad',
  =======
+ Patrick: I am Patrick,
  >>>>>>>

但假如我们两个都是有顺序的会变成怎样呢?

首先调整之后文件的顺序如下:

export const Employees = {
  Andy: 'I can sing',
  Ivan: 'I can rap',
  Oliver: 'I can run'
};

同事 A 添加代码如下:

export const Employees = {
  Andy: 'I can sing',
+ Brad: 'I am Brad',
  Ivan: 'I can rap',
  Oliver: 'I can run'
};

我添加代码如下:

export const Employees = {
  Andy: 'I can sing',
  Ivan: 'I can rap',
  Oliver: 'I can run',
+ Patrick: 'I am Patrick'
};

这个时候,当我们合代码的时候就没有冲突了

通过工具去排序

知道了顺序对于我们避免解决冲突的重要性,那么接下来就是要执行了,如果要求同事们都时刻遵循写代码的顺序,显然不太合理,我们打算用工具去执行。

我用 vue-cli 演示下

具体步骤如下:

时机:在 git commit 之前,可以使用 git hooks 做到

在 package.json 中加入如下代码,意思是在 git commit 之前会去执行 node ./auto-fix/index.js 以及 git add 命令

  "lint-staged": {
    "*.{js}": [
      "node ./auto-fix/index.js",
      "git add"
    ]
  },
  "gitHooks": {
    "pre-commit": "lint-staged"
  },

如果发现没有触发的话,可以看下是不是没有这两个包 lint-stagedyorkie。其中 yorkie 是 尤大大 folk husky,它俩功能是一样的,都是生成一些 git hooks 文件,读取项目中package.json 的相关配置项去执行一些命令,区别是尤大做了一些逻辑和配置上的改动

npm i lint-staged --save-dev
npm i yorkie --save-dev  

读取和排序:寻找指定目录下的文件(下面示例为 src/constant 目录下),匹配出文件中的对象,针对对象排序

这里排序的策略是先针对 value 值进行排序,如果 value 值相同,再针对 key 值排序

写入:将排好序的文件写回原文件

提示:成功或者失败的提示

新建文件 auto-fix/index.js, 代码如下:

const fs = require('fs');
const path = require('path');
const chalk = require('chalk');

function resolve(p) {
  return path.join(__dirname, '..', p);
}

const objRegex = /\{[^}\/\/]*\}/g;

function fileDisplay(filePath) {
  let file = resolve(filePath);
  let fileList = fs.readdirSync(file, 'utf8');

  function compare() {
    return function ([key1, value1],[key2, value2]) {
      key1 = Number.isNaN(Number(key1)) ? key1.toLowerCase() : key1;
      key2 = Number.isNaN(Number(key2)) ? key2.toLowerCase() : key2;
      value1 = Number.isNaN(Number(value1)) ? value1.toLowerCase() : value1;
      value2 = Number.isNaN(Number(value2)) ? value2.toLowerCase() : value2;
      if (value1 === value2) {
        return key1 < key2 ? -1 : key1 > key2 ? 1 : 0; // 升序
      } else {
        return value1 < value2 ? -1 : value1 > value2 ? 1 : 0;
      }
    }
  }

  // 读取文件
  fileList.forEach(filename => {
    let fileDir = path.join(filePath, filename);
    console.log(chalk.cyan(`Auto fix ${fileDir}`))
    let fileContents = fs.readFileSync(fileDir, 'utf8');

    function sortObj(item) {
      item = item.replace(/(\S+):/g,"\"$1\":")
      .replace(/'/g, '"');

      let arr = [];
      item = JSON.parse(item);
      arr = Object.entries(item);
      // 排序
      arr = arr.sort(compare());
      let tempObj = {};
      // 将排序好的数组拼接成对象
      tempObj = arr.reduce((_sortedObj, [key, val]) => ({
        ..._sortedObj,
        [key]: val
      }), {});
      let tempStr = JSON.stringify(tempObj, null, 2);
      // 去掉 key 值的双引号
      tempStr = tempStr.replace(/"/g, "").replace(/\: /g,"\: \'").replace(/\,/g,"\'\,").replace(/\n\}/g,"\'\n\}");
      return tempStr;
    }

    // 匹配对象并排序并替换
    fileContents = fileContents.replace(objRegex, function(match) {
      return sortObj(match)
    });

    // 输出到文件中
    fs.writeFileSync(fileDir, fileContents, 'utf8');
  });

}
try {
  fileDisplay('src/constant');
  console.log(chalk.green(`Auto fix complete`))
} catch (e) {
  console.log(e);
  console.log(chalk.red(`Auto fix Error, You can check the the problem in the auto-fix/index.js directory`))
  throw new Error(`Auto fix Error, You can check the the problem in the auto-fix/index.js directory`)
}

结果

Gif 演示一下

探索:对象属性遍历有顺序么?

我们知道,数组遍历是有顺序的,也经常说对对象遍历是无序的。

但实际上我理解这个“无序”指的只是不会按照属性排列前后的顺序而已,对象属性遍历本身是有自己的一套规则的。

可以实验一下

let obj = { [Symbol()]:0, b:0, 10:0, 2:0, a:0 };
// for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)
// 输出 2 10 b a
for (let key in obj) {
  console.log(key)
}
// Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。
// [ '2', '10', 'b', 'a' ]
console.log(Object.keys(obj));

// Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
// [ '2', '10', 'b', 'a' ]
console.log(Object.getOwnPropertyNames(obj));

// Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。
// [ Symbol() ]
console.log(Object.getOwnPropertySymbols(obj));

// Reflect.ownKeys返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。
// [ '2', '10', 'b', 'a', Symbol() ]
console.log(Reflect.ownKeys(obj));

总结如下:

回顾实现——也存在类似问题

上面的实现中,我是先根据属性的 value 值排序,如果 value 值相同再根据 key 值排序。排序后得到一个有顺序的二维数组,类似如下:

[["Andy","I am Andy"],["Gopal","I am Gopal"],["Ivan","I am Ivan"],["Oliver","I am Oliver"],["Patrick","I am Patrick"]]

然后遍历数组依次写入对象中,这样看起来似乎是没有问题,结果却是也是!但假如 key 值是 number 类型呢?比如类似如下:

export const Employees = {
  Andy: 'I am Andy',
  Gopal: 'I am Gopal',
  Ivan: 'I am Ivan',
  1: 'Z',
  2: 'A',
  3: 'J',
  Oliver: 'I am Oliver',
  Patrick: 'I am Patrick'
};

按照预期,按照 value 排序,应该得到

export const Employees = {
  2: 'A',
  Andy: 'I am Andy',
  Gopal: 'I am Gopal',
  Ivan: 'I am Ivan',
  Oliver: 'I am Oliver',
  Patrick: 'I am Patrick'
  3: 'J',
  1: 'Z'
};

但事实上,我们得到了类似如下的结果:

export const Employees = {
  1: 'Z',
  2: 'A',
  3: 'J',
  Andy: 'I am Andy',
  Gopal: 'I am Gopal',
  Ivan: 'I am Ivan',
  Oliver: 'I am Oliver',
  Patrick: 'I am Patrick'
};

这个实际上就是跟我们上面提到的属性遍历规则有关,因为首先遍历所有数值键,按照数值升序排列。其次遍历所有字符串键,按照加入时间升序排列。

缺点

除了上面所说的属性顺序问题,如果你细读上面的代码,其实是会发现一些问题,也算是一些 TODO 项,如果有朋友解决了,欢迎给我提个 pr

总结

本文记录了一次通过利用 git hooks 在代码提交之前给相关的代码排序,从而减少合代码时候的冲突,也探讨了一下 Js 对象属性遍历属性的问题,希望对大家有所启发。

涉及到的 demo 已放 Github,有更好的想法可以提 PR

原创不易,欢迎点赞评论~

往期优秀文章推荐

参考

上一篇下一篇

猜你喜欢

热点阅读