为什么 git pull 会修改文件权限和所属用户以及用户组?

问题过程

首先 git clone 之后进行改变用户组

image

然后使用 git pull 更新本地仓库,这里明显发现已经发生了变化。

注意:git pull = git fetch + git merge 下面会更多用 git merge 说明。

image

当然如果修改了权限,git pull 之后也会变成默认权限(根据 umask 值确定)

注意:当 git config core.filemode true 的时候,git 只记录文件是否可执行权限。比如说下方的 3.js 用户和用户组可读可写,但都没有执行权限,一旦设置执行权限就会触发 git 修改追踪,不设置则不需要追踪修改记录。一般我会把 core.filemode 设置为 false,这样就不会存在设置权限后还要 commit 的问题。

1
-rw-rw-r-- 1 x x 0 Dec 12 21:15 3.js

当然也不建议这么做,可以看看后面的最佳实践。

为什么会更改

新建一个文件那么用户和用户组就是当前的用户以及所在的用户组。但是如果另一个用户修改文件内容是不会修改文件用户所属的。

那么这里我就猜测 git merge 的时候可能就是新建而不是修改。

这里只要修改内容后再更改用户组,再使用 git merge 的时候如果文件新建的时间变成了最新的时间就是说明是新建而不是修改。

ubuntu 有一个 sudo debugfs -R filename mountdev 的命令可以查看文件新建时间,但是我用了不可以。。不过我用 mac 的时候发现 mac 内的文件是可以查看文件新建时间的。

1
2
3
4
5
6
7
8
9
10
➜  test git init
Initialized empty Git repository in /Users/lishude/Downloads/test/.git/
➜ test git:(master) touch test.txt
➜ test git:(master) ✗ git add .
➜ test git:(master) ✗ git commit -m"1"
[master (root-commit) c5fa95c] 1
1 file changed, 1 insertion(+)
create mode 100644 test.txt
➜ test git:(master) stat test.txt
16777220 1894632 -rw-r--r-- 1 lishude staff 0 2 "Dec 17 18:07:10 2017" "Dec 17 18:06:55 2017" "Dec 17 18:06:55 2017" "Dec 17 18:06:44 2017" 4194304 8 0 test.txt

这里我新建一个git仓库并且commit了一条。

注意,mac 的 stat file 和 ubuntu 下的还不同,这里是有创建时间的,就是最后一个时间,不过格式就比 ubuntu 差了许多。

然后我们再次 commit 然后修改用户组,最后 reset 查看文件信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
➜  test git:(master) git commit -am"2"
[master 773907e] 2
1 file changed, 1 insertion(+)
➜ test git:(master) stat test.txt
16777220 1894632 -rw-r--r-- 1 lishude staff 0 4 "Dec 17 18:07:48 2017" "Dec 17 18:07:38 2017" "Dec 17 18:07:38 2017" "Dec 17 18:06:44 2017" 4194304 8 0 test.txt
➜ test git:(master) sudo chown root test.txt
➜ test git:(master) ll
total 8
-rw-r--r-- 1 root staff 4B 12 17 18:07 test.txt
➜ test git:(master) git reset --hard HEAD~1
HEAD is now at c5fa95c 1
➜ test git:(master) stat test.txt
16777220 1894740 -rw-r--r-- 1 lishude staff 0 2 "Dec 17 18:09:18 2017" "Dec 17 18:09:15 2017" "Dec 17 18:09:15 2017" "Dec 17 18:09:15 2017" 4194304 8 0 test.txt

通过实验确实是这样的,文件新建时间已经变成最新的了。

解决方案

使用特定用户执行

比如说使用文件需要保留 www-data 权限就需要用这个

sudo -u www-data git pull origin master

但是需要注意的这个会修改所有需要更改的所有与当前版本库不同内容的文件的用户和用户组,并且同时也会会更改 .git 文件夹。

git hook

下面在 post 或者 merge 之后会触发钩子命令,也就是自动执行一些操作,这里我们就可以直接为文件更改权限或者修改用户和用户组。

1
2
3
4
5
6
#!/bin/sh
#
# .git/hooks/post-merge

sudo chmod -R 775 target
sudo chown -R user:group target

这里说阻止其实没有办法阻止,总结一句话就是,git 不记录用户关系和除了执行权限的权限,所有的操作取决于执行git操作的用户。

最佳实践

一般在生产环境最好不要更改文件的用户组或者权限。

比如说 nginx 还有 php-fpm 等就直接使用当前用户(当然一般都是所属 root 和 sudos 用户组的),修改这些设置就不需要再更改用户。在 #1 有 nginx 和 php-fpm 的实践。

那么设置文件权限呢?也是一样,不要直接在生产环境设置,而是再本地设置好(当 git config core.filemode true 时候 git 会跟踪文件权限)再上传到生产环境。

补充 git 指令

git 位于 usr/bin/git ,文件所属 root 以及 root 组,权限为755,所以所有的用户都可以执行。

使用 git 进行 git pull 或者 git merge 的时候会以当前用户更改 .git 文件夹的用户和用户组,谨慎使用不同用户,最好按照最佳实践来做。

在 #5 有用户与用户组的详细说明。

Go 语言延迟调用和错误处理

Go 支持使用关键字 defer 创建函数内延迟语句(进栈),当函数在 return 之前,这些 defer 语句会按照先进后出执行(出栈)。

如下所示,在 test 函数 return 之前所有进入 defer 栈的语句都会先执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
)

func main() {
res := test(1)
fmt.Println(res)
}

func test(a int) string {
fmt.Println("0")
defer fmt.Println("1")
if a == 1 {
return "2"
}
defer fmt.Println("3")
return "3"
}

需要注意的是,编译器在到达 defer 语句的时候要进行确认参数值以及类型,分配堆栈等。下面这段代码输出的是 2 而不是 4,这是因为 i 已经进行了一次计算。REF

1
2
3
4
5
6
7
8
9
10
11
// https://play.golang.org/p/dOUFNj96EIQ
package main

import "fmt"

func main() {
var i int = 1

defer fmt.Println("result =>",func() int { return i * 2 }())
i++
}

不止如此,就算使用 go 关键字也是这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"time"
)

func main() {
var i int = 1

go fmt.Println("result =>",func() int { return i * 2 }())
i++
time.Sleep(3*time.Second)
}

如果要改变这里的方式就要把这里的函数进行改成闭包即可。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
var i int = 1

defer func() {
fmt.Println("result =>", func() int { return i * 2 }())
}()
i++
}

另外这些 defer 语句不受错误的影响,之前入栈的 defer 会照样执行。

如下所示,在发生了除零错误后,之前调用的 fmt.Println() 函数依旧会执行,而后续的没有入栈的就不会调用了。

1
2
3
4
5
6
7
8
9
10
11
12
func test2(x int) {
defer fmt.Println("1")
defer func() {
// 除 0 错误
fmt.Println(100 / x)
}()
defer fmt.Println("2")
}

test2(0)
// 1
// panic: runtime error: integer divide by zero

所以我们可以用来做资源释放和错误处理。

1
2
3
4
5
6
7
8
9
10
11
func test3() error {
// 如果发生错误 f 为空,err 不为空
f, err := os.Create("defer.txt")
if err != nil {
return err
}
// 释放文件句柄
defer f.Close()
f.WriteString("Hello, World!")
return nil
}

Go 没有 C 系语言的 try...throw 的形式,而是使用 panic 和 recover 的形式,而这两个都是内建函数。

panic 用于发出错误(恐慌),而 recover 用于接收 panic 的信息。捕获函数 recover 只有在延迟调⽤内直接调⽤才会终⽌错误,否则总是返回 nil。任何未捕获的错误都会沿调⽤堆栈向外传递。

1
2
3
4
5
6
7
8
func throwsPanic() {
defer func() {
if x := recover(); x != nil {
fmt.Println(x)
}
}()
panic("panic func")
}

以上调用就会输出 panic func。如果是延迟调⽤中引发的错误,可被后续延迟调⽤捕获,但仅最后⼀个错误可被捕获。

1
2
3
4
5
6
7
8
9
10
func test() {
defer func() {
fmt.Println(recover())
}()
defer func() {
panic("defer panic")
}()
panic("test panic")
}
// defer panic

补充错误处理

Panic 是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。当函数F调用panic,函数F的执行被中断,但是F中的延迟函数会正常执行,然后F返回到调用它的地方。在调用的地方,F的行为就像调用了panic。这一过程继续向上,直到发生panic的goroutine中所有调用的函数返回,此时程序退出。恐慌可以直接调用panic产生。也可以由运行时错误产生,例如访问越界的数组。

Recover 是一个内建的函数,可以让进入令人恐慌的流程中的goroutine恢复过来。recover仅在延迟函数中有效。在正常的执行过程中,调用recover会返回nil,并且没有其它任何效果。如果当前的goroutine陷入恐慌,调用recover可以捕获到panic的输入值,并且恢复正常的执行。

一定要记住,你应当把它作为最后的手段来使用,也就是说,你的代码中应当没有,或者很少有panic的东西。这是个强大的工具,请明智地使用它。

不常见的 NPM 包管理技巧

除了使用 npmjs.com 这个集中包托管网站,npm 还可以使用 Git 和本地包来安装。

和正常的 npm install package-name 语法一样,只不过下面的 package-name 全部换成了 url。

使用 Git 包

官方在文档里定义的 url 的格式是 <protocol>://[<user>[:<password>]@]<hostname>[:<port>][:][/]<path>[#<commit-ish> | #semver:<semver>]

其中 <protocol> 可以是 gitgit+sshgit+httpgit+https,或者 git+file。而 #<commit-ish> 可以选择 commit 的点,#semver:<semver> 是选择 tag 并且支持语义化版本,如此以来我们不用发布到 npm 也能使用包了!在 Golang 中就是这样这样进行包管理,不过知道 Go 1.10 还没有确定的版本管理方案。这里官方文档并没有说明可以使用 branch,其实是可以的,具体可以参考下述的格式示例。

如果没有使用 commit-ish 或者 semver 会直接使用 master 分支,最后使用 npm install git-url 安装即可, package.json 新增了一些包名称字段,这些名称就是相应包中 package.json 定义的 name 字段:

各种格式示例如下:

1
2
3
4
5
6
7
8
9
10
"dependencies": {
// semver(by tag)
"random.ts": "git+https://github.com/isLishude/random.ts.git#semver:^2.0.0",
// branch
"random.ts": "git+ssh://git@github.com/isLishude/random.ts.git#dev",
// master
"random.ts": "git+ssh://git@github.com/isLishude/random.ts.git",
// commit-ish
"random.ts": "git+ssh://git@github.com/isLishude/random.ts.git#9d22109491"
}

如果是 GitHub 的话更简单了,安装命令直接使用 npm install username/repository 就可以了,也可以使用 branch 以及 semver tag。

使用本地包

require 引用当前项目的其它文件需要使用相对路径的地址,如果层级关系太多,就会写很多 ../,有一种本地包的方式可以很好的解决。

配置很简单,类似当前项目一样,新建一个用于当前项目的包目录并且包括 package.json 文件,如本项目所示,在本项目 helper 目录下定义 local-helper 包。

1
2
3
4
5
6
7
{
"name": "local-helper",
"version": "1.0.0",
"description": "local heleper functions",
"main": "index.js",
"license": "ISC"
}

然后在项目使用 npm install file:package-path 就可以了,例如此项目就使用 npm install file:./helper。最后就可以看到在 package.json 文件 dependencies 字段新增了 "local-helper": "file:helper"

1
2
3
"dependencies": {
"local-helper": "file:helper"
}

示例

参考

JS BigInt 尝鲜

JavaScript 所有数字内部都是 float64 类型,所以数值的精度最多只能到 53 个二进制位,大于这个范围的整数是无法精确表示的。

1
2
3
4
2 ** 53;
// 9007199254740992
2 ** 53 + 1;
// 9007199254740992

在很多金融场景如果使用 JS 的话就得使用一些 BigNumber 库。其中以太坊 web3.js 使用的是最为流行的是 bignumber.js,并且包含 .d.ts 类型提示,推荐在生产环境使用。

因为这些库表示大数的方式是以 16 进制字符串表示的,通常在实践中还需要使用 Buffer.from() 转换成二进制对象,还是有很多不方便,需要写很多辅助函数。不过以后我们就可以使用官方标准库中的 BigInt 了。

目前(2018 年 5 月 1 日)BigInt 提案已经进入 TC39 stage 3,不过还是被 V8 引擎提前实现,本文所有代码示例基于 Chrome Canary 68.0.3415.0,如下图所示。

1
2
3
4
5
6
typeof 123;
// "bigint"
2n ** 53n;
// 9007199254740992n
2n ** 53n + 1n;
// 9007199254740993n

BigInt 表示没有精度和大小限制的整数,为了兼容性考虑,在数字后面添加后缀n 和普通数字类型区分,使用二进制八进制和十六进制也可以表示。

1
2
3
4
5
6
7
8
123n;
// 123n;
0b101n;
// 5n;
0o123n;
// 83n;
0xabcn;
// 2748n;

数字的字符串形式可以类似于 Number() 使用 BigInt() 直接转换为 BigInt,需要注意的是参数检查和 Number() 是一致的,是不能使用 123n 字符串形式的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
123n;
//123n
0b101n;
// 5n
0o123n;
//83n
0xabcn;
//2748n

BigInt("123");
// 123n
BigInt("0xa");
// 10n
BigInt("0b1");
// 1n
BigInt("0o1");
// 1n

new BigInt();
// Thrown:
// TypeError: BigInt is not a constructor
// at new BigInt (<anonymous>)
BigInt(undefined);
// Thrown:
// TypeError: Cannot convert undefined to a BigInt
// at BigInt (<anonymous>)
BigInt(null);
// Thrown:
// TypeError: Cannot convert null to a BigInt
// at BigInt (<anonymous>)

BitInt 除了不能和 number 类型直接运算之外,其它方面和普通的数值运算没有多少区别,除法运算始终返回整数形式。

1
2
3
4
5
5n / 2n;
// 2n
5n + 2;
// Thrown:
// TypeError: Cannot mix BigInt and other types, use explicit conversions

BigInt 也存在隐式转换,在相等运算符==、不同类型运算以及强制类型转化函数,都还存在 JS 远古传统。

1
2
3
4
5
6
7
8
9
10
11
12
2n == 2;
// true
2n === 2;
// false
0n == "";
// true
0n == 0;
// true
0n == false;
// true
"" + 123n;
// '123'

更多内容可以参考 BigInt 提案

TypeScript 导入 JSON Module

这是我在知乎问题 Typescript有什么冷门但是很好用的特性? 的回答。

Node.js 模块是允许直接导入 JSON 文件的,但是 ES Module 现在还不支持,TS 在 2.9 (18年5月17日还未发布)之前也不支持。日常中使用 JSON 最多的场景就是 配置文件了,如果要使用的话需要使用下面一些 trick 来支持。

这里有个 JSON 文件

1
2
3
4
5
{
"//": "student.json",
"name": "test",
"age": 23
}

如果需要导入的话,需要先定义类型。

1
2
3
4
5
6
7
8
// student.d.ts
declare module "*student.json" {
export interface IStudent {
name: string;
age: number;
}
export const student: IStudent
}

这里使用 *student.json 是因为导入的时候有路径符号,这里要用 * 匹配。

最后在 tsconfig.json 文件中包含这些类型定义文件。

1
2
3
4
5
{
"//": "tsconfig.json",
"include": ["src/**/*", "./myTypes/*.d.ts"],
"exclude": ["node_modules"]
}

现在我们就可以直接在文件中引入了,TS 会智能的提示类型。

1
2
3
4
5
6
7
import { log } from "console";
// 路径为 `./student.json`
// 所以上面声明模块时用了通配符 `declare module "*student.json"`
import { student } from "./student.json";

log(student.age);
log(student.name);

TypeScript 2.9 即将发布,到时候就可以直接使用 JsonModule 了。