@netlify/git-utils 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +86 -0
- package/package.json +58 -0
- package/src/commits.js +37 -0
- package/src/diff.js +40 -0
- package/src/exec.js +48 -0
- package/src/main.js +20 -0
- package/src/match.js +18 -0
- package/src/refs.js +67 -0
- package/src/stats.js +27 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Netlify <team@netlify.com>
|
|
4
|
+
Portions Copyright (c) 2016-2019 Orta Therox
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
[](https://codecov.io/gh/netlify/build)
|
|
2
|
+
[](https://github.com/netlify/build/actions)
|
|
3
|
+
|
|
4
|
+
# Git Utility
|
|
5
|
+
|
|
6
|
+
Utility for dealing with modified, created, deleted files since a git commit.
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
/* Export the Netlify Plugin */
|
|
12
|
+
module.exports = {
|
|
13
|
+
onPreBuild: ({ utils }) => {
|
|
14
|
+
const { git } = utils
|
|
15
|
+
|
|
16
|
+
/* Do stuff if files modified */
|
|
17
|
+
if (git.modifiedFiles.length !== 0) {
|
|
18
|
+
console.log('Modified files:', git.modifiedFiles)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* Do stuff only if html code edited */
|
|
22
|
+
const htmlFiles = git.fileMatch('**/*.html')
|
|
23
|
+
console.log('html files git info:', htmlFiles)
|
|
24
|
+
|
|
25
|
+
if (htmlFiles.edited.length !== 0) {
|
|
26
|
+
console.log('>> Run thing because HTML has changed\n')
|
|
27
|
+
}
|
|
28
|
+
//
|
|
29
|
+
/* Do stuff only if markdown files edited */
|
|
30
|
+
const markdownFiles = git.fileMatch('**/*.md')
|
|
31
|
+
console.log('markdown files git info:', markdownFiles)
|
|
32
|
+
|
|
33
|
+
if (markdownFiles.modified.length !== 0) {
|
|
34
|
+
console.log('>> Run thing because Markdown files have been created/changed/deleted\n')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Do stuff only if css files edited */
|
|
38
|
+
const cssFiles = git.fileMatch('**/*.css')
|
|
39
|
+
if (cssFiles.deleted.length !== 0) {
|
|
40
|
+
console.log('>> Run thing because css files have been deleted\n')
|
|
41
|
+
console.log(cssFiles)
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
The `git` util includes the following signature.
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
module.exports = {
|
|
53
|
+
onPreBuild: ({ utils }) => {
|
|
54
|
+
console.log(utils.git)
|
|
55
|
+
// {
|
|
56
|
+
// fileMatch: [Function], <-- Lookup function. See below
|
|
57
|
+
// modifiedFiles: [ Array of all modified files ],
|
|
58
|
+
// createdFiles: [ Array of all created files ],
|
|
59
|
+
// deletedFiles: [ Array of all deleted files ],
|
|
60
|
+
// commits: [ Array of commits with details ],
|
|
61
|
+
// linesOfCode: [AsyncFunction: linesOfCode] <-- how many lines of code have changed
|
|
62
|
+
// }
|
|
63
|
+
//
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`git.fileMatch()` is a glob matcher function to detect the git status of a pattern of files.
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
const cssFiles = git.fileMatch('**/*.css')
|
|
74
|
+
console.log('cssFiles', cssFiles)
|
|
75
|
+
// {
|
|
76
|
+
// modified: [ 'just-changed.css', 'just-changed-two.css' ],
|
|
77
|
+
// created: [ 'just-added.css' ],
|
|
78
|
+
// deleted: [ 'just-deleted.css' ],
|
|
79
|
+
// edited: [ 'just-changed.css', 'just-changed-two.css', 'just-added.css', 'just-deleted.css' ]
|
|
80
|
+
// }
|
|
81
|
+
//
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Prior art
|
|
85
|
+
|
|
86
|
+
This was originally found in [danger.js](https://danger.systems/js/) and extracted into this utility
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@netlify/git-utils",
|
|
3
|
+
"version": "2.0.2",
|
|
4
|
+
"description": "Utility for dealing with modified, created, deleted files since a git commit",
|
|
5
|
+
"main": "src/main.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"src/**/*.js"
|
|
8
|
+
],
|
|
9
|
+
"author": "Netlify Inc.",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"prepublishOnly": "cd ../../ && npm run prepublishOnly"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"nodejs",
|
|
15
|
+
"javascript",
|
|
16
|
+
"windows",
|
|
17
|
+
"macos",
|
|
18
|
+
"linux",
|
|
19
|
+
"shell",
|
|
20
|
+
"bash",
|
|
21
|
+
"build",
|
|
22
|
+
"terminal",
|
|
23
|
+
"deployment",
|
|
24
|
+
"es6",
|
|
25
|
+
"serverless",
|
|
26
|
+
"continuous-integration",
|
|
27
|
+
"continuous-delivery",
|
|
28
|
+
"ci",
|
|
29
|
+
"continuous-deployment",
|
|
30
|
+
"plugins",
|
|
31
|
+
"continuous-testing",
|
|
32
|
+
"netlify-plugin",
|
|
33
|
+
"netlify"
|
|
34
|
+
],
|
|
35
|
+
"homepage": "https://github.com/netlify/build",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/netlify/build.git",
|
|
39
|
+
"directory": "packages/git-utils"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/netlify/build/issues"
|
|
43
|
+
},
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"execa": "^5.1.1",
|
|
47
|
+
"map-obj": "^4.0.0",
|
|
48
|
+
"micromatch": "^4.0.2",
|
|
49
|
+
"moize": "^6.0.0",
|
|
50
|
+
"path-exists": "^4.0.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"ava": "^3.15.0"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=10.18.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/commits.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { git } = require('./exec')
|
|
4
|
+
|
|
5
|
+
// Return information on each commit since the `base` commit, such as SHA,
|
|
6
|
+
// parent commits, author, committer and commit message
|
|
7
|
+
const getCommits = function (base, head, cwd) {
|
|
8
|
+
const stdout = git(['log', `--pretty=${GIT_PRETTY_FORMAT}`, `${base}...${head}`], cwd)
|
|
9
|
+
const commits = stdout.split('\n').map(getCommit)
|
|
10
|
+
return commits
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Parse the commit output from a string to a JavaScript object
|
|
14
|
+
const getCommit = function (line) {
|
|
15
|
+
const [sha, parents, authorName, authorEmail, authorDate, committerName, committerEmail, committerDate, message] =
|
|
16
|
+
line.split(GIT_PRETTY_SEPARATOR)
|
|
17
|
+
return {
|
|
18
|
+
sha,
|
|
19
|
+
parents,
|
|
20
|
+
author: { name: authorName, email: authorEmail, date: authorDate },
|
|
21
|
+
committer: { name: committerName, email: committerEmail, date: committerDate },
|
|
22
|
+
message,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// `git log --pretty` does not have any way of separating tokens, except for
|
|
27
|
+
// commits being separated by newlines. Since some tokens (like the commit
|
|
28
|
+
// message or the committer name) might contain a wide range of characters, we
|
|
29
|
+
// need a specific separator.
|
|
30
|
+
// We choose RS (Record separator) which is a rarely used control character
|
|
31
|
+
// intended for this very purpose: separating records. It is used by some
|
|
32
|
+
// formats such as JSON text sequences (RFC 7464).
|
|
33
|
+
const GIT_PRETTY_SEPARATOR = '\u001E'
|
|
34
|
+
// List of commit fields we want to retrieve
|
|
35
|
+
const GIT_PRETTY_FORMAT = ['%H', '%p', '%an', '%ae', '%ai', '%cn', '%ce', '%ci', '%f'].join(GIT_PRETTY_SEPARATOR)
|
|
36
|
+
|
|
37
|
+
module.exports = { getCommits }
|
package/src/diff.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { git } = require('./exec')
|
|
4
|
+
|
|
5
|
+
// Return the list of modified|created|deleted files according to git, between
|
|
6
|
+
// the `base` commit and the `HEAD`
|
|
7
|
+
const getDiffFiles = function (base, head, cwd) {
|
|
8
|
+
const stdout = git(['diff', '--name-status', '--no-renames', `${base}...${head}`], cwd)
|
|
9
|
+
const files = stdout.split('\n').map(getDiffFile).filter(Boolean)
|
|
10
|
+
|
|
11
|
+
const modifiedFiles = getFilesByType(files, 'M')
|
|
12
|
+
const createdFiles = getFilesByType(files, 'A')
|
|
13
|
+
const deletedFiles = getFilesByType(files, 'D')
|
|
14
|
+
return { modifiedFiles, createdFiles, deletedFiles }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Parse each `git diff` line
|
|
18
|
+
const getDiffFile = function (line) {
|
|
19
|
+
const result = DIFF_FILE_REGEXP.exec(line)
|
|
20
|
+
|
|
21
|
+
// Happens for example when `base` is invalid
|
|
22
|
+
if (result === null) {
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const [, type, filepath] = result
|
|
27
|
+
return { type, filepath }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DIFF_FILE_REGEXP = /([ADM])\s+(.*)/
|
|
31
|
+
|
|
32
|
+
const getFilesByType = function (files, type) {
|
|
33
|
+
return files.filter((file) => file.type === type).map(getFilepath)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const getFilepath = function ({ filepath }) {
|
|
37
|
+
return filepath
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { getDiffFiles }
|
package/src/exec.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const process = require('process')
|
|
4
|
+
|
|
5
|
+
const execa = require('execa')
|
|
6
|
+
const moize = require('moize')
|
|
7
|
+
const pathExists = require('path-exists')
|
|
8
|
+
|
|
9
|
+
// Fires the `git` binary. Memoized.
|
|
10
|
+
const mGit = function (args, cwd) {
|
|
11
|
+
const cwdA = safeGetCwd(cwd)
|
|
12
|
+
try {
|
|
13
|
+
const { stdout } = execa.sync('git', args, { cwd: cwdA })
|
|
14
|
+
return stdout
|
|
15
|
+
} catch (error) {
|
|
16
|
+
// The child process `error.message` includes stderr and stdout output which most of the times contains duplicate
|
|
17
|
+
// information. We rely on `error.shortMessage` instead.
|
|
18
|
+
error.message = error.shortMessage
|
|
19
|
+
throw error
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// eslint-disable-next-line no-magic-numbers
|
|
24
|
+
const git = moize(mGit, { isDeepEqual: true, maxSize: 1e3 })
|
|
25
|
+
|
|
26
|
+
const safeGetCwd = function (cwd) {
|
|
27
|
+
const cwdA = getCwdValue(cwd)
|
|
28
|
+
|
|
29
|
+
if (!pathExists.sync(cwdA)) {
|
|
30
|
+
throw new Error(`Current directory does not exist: ${cwdA}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return cwdA
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const getCwdValue = function (cwd) {
|
|
37
|
+
if (cwd !== undefined) {
|
|
38
|
+
return cwd
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
return process.cwd()
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new Error('Current directory does not exist')
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { git }
|
package/src/main.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { getCommits } = require('./commits')
|
|
4
|
+
const { getDiffFiles } = require('./diff')
|
|
5
|
+
const { fileMatch } = require('./match')
|
|
6
|
+
const { getBase, getHead } = require('./refs')
|
|
7
|
+
const { getLinesOfCode } = require('./stats')
|
|
8
|
+
|
|
9
|
+
// Main entry point to the git utilities
|
|
10
|
+
const getGitUtils = function ({ base, head, cwd } = {}) {
|
|
11
|
+
const headA = getHead(cwd, head)
|
|
12
|
+
const baseA = getBase(base, headA, cwd)
|
|
13
|
+
const { modifiedFiles, createdFiles, deletedFiles } = getDiffFiles(baseA, headA, cwd)
|
|
14
|
+
const commits = getCommits(baseA, headA, cwd)
|
|
15
|
+
const linesOfCode = getLinesOfCode(baseA, headA, cwd)
|
|
16
|
+
const fileMatchA = fileMatch.bind(null, { modifiedFiles, createdFiles, deletedFiles })
|
|
17
|
+
return { modifiedFiles, createdFiles, deletedFiles, commits, linesOfCode, fileMatch: fileMatchA }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = getGitUtils
|
package/src/match.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const mapObj = require('map-obj')
|
|
4
|
+
const micromatch = require('micromatch')
|
|
5
|
+
|
|
6
|
+
// Return functions that return modified|created|deleted files filtered by a
|
|
7
|
+
// globbing pattern
|
|
8
|
+
const fileMatch = function ({ modifiedFiles, createdFiles, deletedFiles }, ...patterns) {
|
|
9
|
+
const matchFiles = {
|
|
10
|
+
modified: modifiedFiles,
|
|
11
|
+
created: createdFiles,
|
|
12
|
+
deleted: deletedFiles,
|
|
13
|
+
edited: [...modifiedFiles, ...createdFiles],
|
|
14
|
+
}
|
|
15
|
+
return mapObj(matchFiles, (key, paths) => [key, micromatch(paths, patterns)])
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { fileMatch }
|
package/src/refs.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { env } = require('process')
|
|
4
|
+
|
|
5
|
+
const { git } = require('./exec')
|
|
6
|
+
|
|
7
|
+
// Retrieve the `head` commit
|
|
8
|
+
const getHead = function (cwd, head = 'HEAD') {
|
|
9
|
+
const result = checkRef(head, cwd)
|
|
10
|
+
if (result.error !== undefined) {
|
|
11
|
+
throwError('head', result)
|
|
12
|
+
}
|
|
13
|
+
return result.ref
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Retrieve the `base` commit
|
|
17
|
+
const getBase = function (base, head, cwd) {
|
|
18
|
+
const refs = getBaseRefs(base, head)
|
|
19
|
+
const { ref } = findRef(refs, cwd)
|
|
20
|
+
return ref
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const getBaseRefs = function (base, head) {
|
|
24
|
+
if (base !== undefined) {
|
|
25
|
+
return [base]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (env.CACHED_COMMIT_REF) {
|
|
29
|
+
return [env.CACHED_COMMIT_REF]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Some git repositories are missing `master` or `main` branches, so we also try HEAD^.
|
|
33
|
+
// We end with HEAD as a failsafe.
|
|
34
|
+
return ['main', 'master', `${head}^`, head]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Use the first commit that exists
|
|
38
|
+
const findRef = function (refs, cwd) {
|
|
39
|
+
const results = refs.map((ref) => checkRef(ref, cwd))
|
|
40
|
+
const result = results.find(refExists)
|
|
41
|
+
if (result === undefined) {
|
|
42
|
+
throwError('base', results[0])
|
|
43
|
+
}
|
|
44
|
+
return result
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check if a commit exists
|
|
48
|
+
const checkRef = function (ref, cwd) {
|
|
49
|
+
try {
|
|
50
|
+
git(['rev-parse', ref], cwd)
|
|
51
|
+
return { ref }
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return { ref, error }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const refExists = function ({ error }) {
|
|
58
|
+
return error === undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const throwError = function (name, { ref, error: { message, stderr } }) {
|
|
62
|
+
const messages = [message, stderr].filter(Boolean).join('\n')
|
|
63
|
+
const messageA = `Invalid ${name} commit ${ref}\n${messages}`
|
|
64
|
+
throw new Error(messageA)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { getBase, getHead }
|
package/src/stats.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { git } = require('./exec')
|
|
4
|
+
|
|
5
|
+
// Returns the number of lines of code added, removed or modified since the
|
|
6
|
+
// `base` commit
|
|
7
|
+
const getLinesOfCode = function (base, head, cwd) {
|
|
8
|
+
const stdout = git(['diff', '--shortstat', `${base}...${head}`], cwd)
|
|
9
|
+
const insertions = parseStdout(stdout, INSERTION_REGEXP)
|
|
10
|
+
const deletions = parseStdout(stdout, DELETION_REGEXP)
|
|
11
|
+
return insertions + deletions
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const parseStdout = function (stdout, regexp) {
|
|
15
|
+
const result = regexp.exec(stdout)
|
|
16
|
+
|
|
17
|
+
if (result === null) {
|
|
18
|
+
return 0
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return Number(result[1])
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const INSERTION_REGEXP = /(\d+) insertion/
|
|
25
|
+
const DELETION_REGEXP = /(\d+) deletion/
|
|
26
|
+
|
|
27
|
+
module.exports = { getLinesOfCode }
|