forked from xpl/git-slack-notify
-
Notifications
You must be signed in to change notification settings - Fork 0
/
git-slack-notify.js
196 lines (139 loc) · 8.04 KB
/
git-slack-notify.js
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
"use strict";
/* ------------------------------------------------------------------------ */
const log = require ('ololog'),
ansi = require ('ansicolor').nice,
fs = require ('fs'),
path = require ('path'),
testMode = process.argv.includes ('--test')
/* ------------------------------------------------------------------------ */
const fatal = (...args) => (log.bright.red.error ('\n', ...args, '\n'), process.exit (0))
/* ------------------------------------------------------------------------ */
const prettyPrintJSON = x => JSON.stringify (x, null, 4)
/* ------------------------------------------------------------------------ */
const [processPath, , configFile = './config.json'] = process.argv.filter (x => x !== 'index.js') // nodemon incorrectly passes index.js occasionally
if (!fs.existsSync (configFile)) {
log.green ('No', configFile.white.bright, 'found, so we filled it with the example data.', 'Check it out, edit and re-start.\n'.bright)
fs.writeFileSync (configFile, prettyPrintJSON ({
accessToken: '<your Slack OAuth access token here>',
fetchFrequency: 30000, // check for new commits each 30s
repos: [
{
name: 'Git Slack Notify', // display name
dir: process.cwd (), // local git repo path
channel: 'general' // where to post
}
]
}))
process.exit ()
}
log.cyan ('Reading config from', configFile.bright)
const config = JSON.parse (fs.readFileSync (configFile, { encoding: 'utf-8' }))
if (!config.accessToken || config.accessToken === '<your Slack OAuth access token here>') {
fatal (`You should specify a valid OAuth ${'accessToken'.white} — get one at ${'https://api.slack.com/'.cyan}`)
}
for (const repo of config.repos) {
if (!repo.dir) {
fatal (`You should specify a ${'dir'.white} for your repository: ${JSON.stringify (repo).yellow}`)
}
if (!repo.name) {
repo.name = path.basename (repo.dir)
}
if (process.argv.includes ('--reset')) {
log.yellow (`Reseting "since" for ${repo.name.bright}`)
delete repo.since
}
}
const saveConfig = () => (!testMode ? fs.writeFileSync (configFile, prettyPrintJSON (config), { encoding: 'utf-8' }) : undefined)
saveConfig ()
/* ------------------------------------------------------------------------ */
const exec = cmd => new Promise (resolve => {
//log.dim.cyan ('> '.bright + cmd)
require ('child_process').exec (cmd, {maxBuffer: 1024 * 1024}, (err, stdout, stderr) => {
if (err) { fatal (`${cmd} failed: ${err}, stderr output: ${stderr}`) }
else { resolve (stdout) }
})
})
/* ------------------------------------------------------------------------ */
const parseGitLog = stdout => {
return stdout
.split ('\u0000')
.map (text => text
.split ('\n')
.reduce ((commit, line, i) => {
if (i === 0) commit.hash = line
else if (line.startsWith ('author')) {
try {
const [,author,timestamp,timezone] = line.match (/author (.+) \<.+\> (\d+)( [+-]\d+)/)
commit.author = author
commit.authorTimestamp = Number (timestamp)
} catch (e) {
log.bright.red.error ('Failed to parse:', line)
throw e
}
}
else if (line.startsWith ('committer')) {
try {
const [,committer,timestamp,timezone] = line.match (/committer (.+) \<.+\> (\d+)( [+-]\d+)/)
commit.committer = committer
commit.committerTimestamp = Number (timestamp)
} catch (e) {
log.bright.red.error ('Failed to parse:', line)
throw e
}
}
else if (line.startsWith (' ')) commit.message.push (line.trim ())
return commit
}, { message: [] })
)
.filter (c => c.hash)
.map (c => ({ ...c,
message: c.message.filter (s => s).join ('\n'),
timestamp: Math.max (c.authorTimestamp || 0, c.committerTimestamp || 0) + 1
}))
}
/* ------------------------------------------------------------------------ */
const sleep = ms => new Promise (resolve => setTimeout (resolve, ms))
/* ------------------------------------------------------------------------ */
async function* newCommits (repo) {
const { dir } = repo
log.cyan ('Watching for new commits in', dir.bright, 'starting from', (repo.since || 'now').bright)
while (true /* this is OK due to the asynchronous nature of this function */) {
const opts = repo.since ? `--since "${repo.since}"` : '-1'
, cmd = `cd ${dir.replace (/^\\\s/g, '\\ ')} && git fetch && git rev-list --topo-order --header --reverse --all ${opts}`
, commits = parseGitLog (await exec (cmd))
if (commits.length) {
log (`new commits since ${repo.since}, fetched with ${cmd}`)
if (repo.since) { // DO NOT report if launched first time
for (const commit of commits) yield commit
}
repo.since = commits[commits.length - 1].timestamp
saveConfig ()
}
await sleep (config.fetchFrequency)
}
}
/* ------------------------------------------------------------------------ */
const slack = new (require ('@slack/client').WebClient) (config.accessToken);
/* ------------------------------------------------------------------------ */
const muted = ({ message }) => message.match (/^\d+\.\d+\.\d+/) || // NPM version numbers
message.match (/^Update (.+)\.md$/i) || // GitHub online editor's default message
message.startsWith ('Merge branch') // auto-generated merge commits
/* ------------------------------------------------------------------------ */
async function watch (repo) {
const { name, dir, channel = 'general', since = '' } = repo
for await (let commit of newCommits (repo)) {
if (muted (commit)) { // filters out automatically generated garbage and other non-informative stuff
log.dim.green (commit, '\n')
} else {
log.bright.green (commit, '\n')
if (!testMode) {
await slack.chat.postMessage ({ channel,
parse: 'full',
text: `:loudspeaker: [${name}] new commit by \`${commit.author}\`: *${commit.message.replace (/\n/g, ' \ ')}*` })
}
}
}
};
/* ------------------------------------------------------------------------ */
config.repos.forEach (repo => watch (repo).catch (fatal))
/* ------------------------------------------------------------------------ */