-
Notifications
You must be signed in to change notification settings - Fork 3
/
lifecycleHelpers.ts
263 lines (240 loc) · 8.7 KB
/
lifecycleHelpers.ts
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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
/*
* Copyright © 2019 Atomist, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as slack from "@atomist/slack-messages";
import * as _ from "lodash";
// This file copied from atomist/lifecycle-automation
/**
* Safely truncate the first line of a commit message to 50 characters
* or less. Only count printable characters, i.e., not link URLs or
* markup.
*/
export function truncateCommitMessage(message: string, repo: any): string {
const title = message.split("\n")[0];
const escapedTitle = slack.escape(title);
const linkedTitle = linkIssues(escapedTitle, repo);
if (linkedTitle.length <= 50) {
return linkedTitle;
}
const splitRegExp = /(&(?:[gl]t|amp);|<.*?\||>)/;
const titleParts = linkedTitle.split(splitRegExp);
let truncatedTitle = "";
let addNext = 1;
let i;
for (i = 0; i < titleParts.length; i++) {
let newTitle = truncatedTitle;
if (i % 2 === 0) {
newTitle += titleParts[i];
} else if (/^&(?:[gl]t|amp);$/.test(titleParts[i])) {
newTitle += "&";
} else if (/^<.*\|$/.test(titleParts[i])) {
addNext = 2;
continue;
} else if (titleParts[i] === ">") {
addNext = 1;
continue;
}
if (newTitle.length > 50) {
const l = 50 - newTitle.length;
titleParts[i] = titleParts[i].slice(0, l) + "...";
break;
}
truncatedTitle = newTitle;
}
return titleParts.slice(0, i + addNext).join("");
}
/**
* Generate GitHub repository "slug", i.e., owner/repo.
*
* @param repo repository with .owner and .name
* @return owner/name string
*/
export function repoSlug(repo: RepoInfo): string {
return `${repo.owner}/${repo.name}`;
}
export function htmlUrl(repo: RepoInfo): string {
if (repo.org && repo.org.provider && repo.org.provider.url) {
let providerUrl = repo.org.provider.url;
if (providerUrl.slice(-1) === "/") {
providerUrl = providerUrl.slice(0, -1);
}
return providerUrl;
} else {
return "https://github.com";
}
}
export const DefaultGitHubApiUrl = "https://api.github.com/";
export function apiUrl(repo: any): string {
if (repo.org && repo.org.provider && repo.org.provider.url) {
let providerUrl = repo.org.provider.apiUrl;
if (providerUrl.slice(-1) === "/") {
providerUrl = providerUrl.slice(0, -1);
}
return providerUrl;
} else {
return DefaultGitHubApiUrl;
}
}
export function userUrl(repo: any, login: string): string {
return `${htmlUrl(repo)}/${login}`;
}
export interface RepoInfo {
owner: string;
name: string;
org?: {
provider: { url?: string },
};
}
export function avatarUrl(repo: any, login: string): string {
if (repo.org !== undefined && repo.org.provider !== undefined && repo.org.provider.url !== undefined) {
return `${htmlUrl(repo)}/avatars/${login}`;
} else {
return `https://avatars.githubusercontent.com/${login}`;
}
}
export function commitUrl(repo: RepoInfo, commit: any): string {
return `${htmlUrl(repo)}/${repoSlug(repo)}/commit/${commit.sha}`;
}
/**
* If the URL is of an image, return a Slack message attachment that
* will render that image. Otherwise return null.
*
* @param url full URL
* @return Slack message attachment for image or null
*/
function urlToImageAttachment(url: string): slack.Attachment {
const imageRegExp = /[^\/]+\.(?:png|jpe?g|gif|bmp)$/i;
const imageMatch = imageRegExp.exec(url);
if (imageMatch) {
const image = imageMatch[0];
return {
text: image,
image_url: url,
fallback: image,
};
} else {
return undefined;
}
}
/**
* Find image URLs in a message body, returning an array of Slack
* message attachments, one for each image. It expects the message to
* be in Slack message markup.
*
* @param body message body
* @return array of Slack message Attachments with the `image_url` set
* to the URL of the image and the `text` and `fallback` set
* to the image name.
*/
export function extractImageUrls(body: string): slack.Attachment[] {
const slackLinkRegExp = /<(https?:\/\/.*?)(?:\|.*?)?>/g;
// inspired by https://stackoverflow.com/a/6927878/5464956
const urlRegExp = /\bhttps?:\/\/[^\s<>\[\]]+[^\s`!()\[\]{};:'".,<>?«»“”‘’]/gi;
const attachments: slack.Attachment[] = [];
const bodyParts = body.split(slackLinkRegExp);
for (let i = 0; i < bodyParts.length; i++) {
if (i % 2 === 0) {
let match: RegExpExecArray;
// tslint:disable-next-line:no-conditional-assignment
while (match = urlRegExp.exec(bodyParts[i])) {
const url = match[0];
const attachment = urlToImageAttachment(url);
if (attachment) {
attachments.push(attachment);
}
}
} else {
const url = bodyParts[i];
const attachment = urlToImageAttachment(url);
if (attachment) {
attachments.push(attachment);
}
}
}
const uniqueAttachments: slack.Attachment[] = [];
attachments.forEach(a => {
if (!uniqueAttachments.some(ua => ua.image_url === a.image_url)) {
uniqueAttachments.push(a);
}
});
return uniqueAttachments;
}
/**
* Find issue mentions in body and replace them with links.
*
* @param body message to modify
* @param repo repository information
* @return string with issue mentions replaced with links
*/
export function linkIssues(body: string, repo: any): string {
if (!body || body.length === 0) {
return body;
}
const splitter = /(\[.+?\](?:\[.*?\]|\(.+?\)|:\s*http.*)|^```.*\n[\S\s]*?^```\s*\n|<.+?>)/m;
const bodyParts = body.split(splitter);
const baseUrl = htmlUrl(repo);
for (let j = 0; j < bodyParts.length; j += 2) {
let newPart = bodyParts[j];
const allIssueMentions = getIssueMentions(newPart);
allIssueMentions.forEach(i => {
const iMatchPrefix = (i.indexOf("#") === 0) ? `^|\\W` : repoIssueMatchPrefix;
const iRegExp = new RegExp(`(${iMatchPrefix})${i}(?!\\w)`, "g");
const iSlug = (i.indexOf("#") === 0) ? `${repo.owner}/${repo.name}${i}` : i;
const iUrlPath = iSlug.replace("#", "/issues/");
const iLink = slack.url(`${baseUrl}/${iUrlPath}`, i);
newPart = newPart.replace(iRegExp, `\$1${iLink}`);
});
bodyParts[j] = newPart;
}
return bodyParts.join("");
}
const gitHubUserMatch = "[a-zA-Z\\d]+(?:-[a-zA-Z\\d]+)*";
/**
* Regular expression to find issue mentions. There are capture
* groups for the issue repository owner, repository name, and issue
* number. The capture groups for repository owner and name are
* optional and therefore may be null, although if one is set, the
* other should be as well.
*
* The rules for preceding characters is different for current repo
* matches, e.g., "#43", and other repo matches, e.g., "some/repo#44".
* Current repo matches allow anything but word characters to precede
* them. Other repo matches only allow a few other characters to
* preceed them.
*/
const repoIssueMatchPrefix = "^|[[\\s:({]";
// tslint:disable-next-line:max-line-length
const issueMentionMatch = `(?:^|(?:${repoIssueMatchPrefix})(${gitHubUserMatch})\/(${gitHubUserMatch})|\\W)#([1-9]\\d*)(?!\\w)`;
const issueMentionRegExp = new RegExp(issueMentionMatch, "g");
/**
* Find all issue mentions and return an array of unique issue
* mentions as "#3" and "owner/repo#5".
*
* @param msg string that may contain mentions
* @return unique list of issue mentions as #N or O/R#N
*/
export function getIssueMentions(msg: string = ""): string[] {
const allMentions: string[] = [];
let matches: string[];
// tslint:disable-next-line:no-conditional-assignment
while (matches = issueMentionRegExp.exec(msg)) {
const owner = matches[1];
const repo = matches[2];
const issue = matches[3];
const slug = (owner && repo) ? `${owner}/${repo}` : "";
allMentions.push(`${slug}#${issue}`);
}
return _.uniq(allMentions);
}