UNPKG

8.9 kBJavaScriptView Raw
1"use strict";
2/*
3 * Copyright © 2018 Atomist, Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17Object.defineProperty(exports, "__esModule", { value: true });
18const slack = require("@atomist/slack-messages/SlackMessages");
19const _ = require("lodash");
20// This file copied from atomist/lifecycle-automation
21/**
22 * Safely truncate the first line of a commit message to 50 characters
23 * or less. Only count printable characters, i.e., not link URLs or
24 * markup.
25 */
26function truncateCommitMessage(message, repo) {
27 const title = message.split("\n")[0];
28 const escapedTitle = slack.escape(title);
29 const linkedTitle = linkIssues(escapedTitle, repo);
30 if (linkedTitle.length <= 50) {
31 return linkedTitle;
32 }
33 const splitRegExp = /(&(?:[gl]t|amp);|<.*?\||>)/;
34 const titleParts = linkedTitle.split(splitRegExp);
35 let truncatedTitle = "";
36 let addNext = 1;
37 let i;
38 for (i = 0; i < titleParts.length; i++) {
39 let newTitle = truncatedTitle;
40 if (i % 2 === 0) {
41 newTitle += titleParts[i];
42 }
43 else if (/^&(?:[gl]t|amp);$/.test(titleParts[i])) {
44 newTitle += "&";
45 }
46 else if (/^<.*\|$/.test(titleParts[i])) {
47 addNext = 2;
48 continue;
49 }
50 else if (titleParts[i] === ">") {
51 addNext = 1;
52 continue;
53 }
54 if (newTitle.length > 50) {
55 const l = 50 - newTitle.length;
56 titleParts[i] = titleParts[i].slice(0, l) + "...";
57 break;
58 }
59 truncatedTitle = newTitle;
60 }
61 return titleParts.slice(0, i + addNext).join("");
62}
63exports.truncateCommitMessage = truncateCommitMessage;
64/**
65 * Generate GitHub repository "slug", i.e., owner/repo.
66 *
67 * @param repo repository with .owner and .name
68 * @return owner/name string
69 */
70function repoSlug(repo) {
71 return `${repo.owner}/${repo.name}`;
72}
73exports.repoSlug = repoSlug;
74function htmlUrl(repo) {
75 if (repo.org && repo.org.provider && repo.org.provider.url) {
76 let providerUrl = repo.org.provider.url;
77 if (providerUrl.slice(-1) === "/") {
78 providerUrl = providerUrl.slice(0, -1);
79 }
80 return providerUrl;
81 }
82 else {
83 return "https://github.com";
84 }
85}
86exports.htmlUrl = htmlUrl;
87exports.DefaultGitHubApiUrl = "https://api.github.com/";
88function apiUrl(repo) {
89 if (repo.org && repo.org.provider && repo.org.provider.url) {
90 let providerUrl = repo.org.provider.apiUrl;
91 if (providerUrl.slice(-1) === "/") {
92 providerUrl = providerUrl.slice(0, -1);
93 }
94 return providerUrl;
95 }
96 else {
97 return exports.DefaultGitHubApiUrl;
98 }
99}
100exports.apiUrl = apiUrl;
101function userUrl(repo, login) {
102 return `${htmlUrl(repo)}/${login}`;
103}
104exports.userUrl = userUrl;
105function avatarUrl(repo, login) {
106 if (repo.org != null && repo.org.provider != null && repo.org.provider.url != null) {
107 return `${htmlUrl(repo)}/avatars/${login}`;
108 }
109 else {
110 return `https://avatars.githubusercontent.com/${login}`;
111 }
112}
113exports.avatarUrl = avatarUrl;
114function commitUrl(repo, commit) {
115 return `${htmlUrl(repo)}/${repoSlug(repo)}/commit/${commit.sha}`;
116}
117exports.commitUrl = commitUrl;
118/**
119 * If the URL is of an image, return a Slack message attachment that
120 * will render that image. Otherwise return null.
121 *
122 * @param url full URL
123 * @return Slack message attachment for image or null
124 */
125function urlToImageAttachment(url) {
126 const imageRegExp = /[^\/]+\.(?:png|jpe?g|gif|bmp)$/i;
127 const imageMatch = imageRegExp.exec(url);
128 if (imageMatch) {
129 const image = imageMatch[0];
130 return {
131 text: image,
132 image_url: url,
133 fallback: image,
134 };
135 }
136 else {
137 return null;
138 }
139}
140/**
141 * Find image URLs in a message body, returning an array of Slack
142 * message attachments, one for each image. It expects the message to
143 * be in Slack message markup.
144 *
145 * @param body message body
146 * @return array of Slack message Attachments with the `image_url` set
147 * to the URL of the image and the `text` and `fallback` set
148 * to the image name.
149 */
150function extractImageUrls(body) {
151 const slackLinkRegExp = /<(https?:\/\/.*?)(?:\|.*?)?>/g;
152 // inspired by https://stackoverflow.com/a/6927878/5464956
153 const urlRegExp = /\bhttps?:\/\/[^\s<>\[\]]+[^\s`!()\[\]{};:'".,<>?«»“”‘’]/gi;
154 const attachments = [];
155 const bodyParts = body.split(slackLinkRegExp);
156 for (let i = 0; i < bodyParts.length; i++) {
157 if (i % 2 === 0) {
158 let match;
159 // tslint:disable-next-line:no-conditional-assignment
160 while (match = urlRegExp.exec(bodyParts[i])) {
161 const url = match[0];
162 const attachment = urlToImageAttachment(url);
163 if (attachment) {
164 attachments.push(attachment);
165 }
166 }
167 }
168 else {
169 const url = bodyParts[i];
170 const attachment = urlToImageAttachment(url);
171 if (attachment) {
172 attachments.push(attachment);
173 }
174 }
175 }
176 const uniqueAttachments = [];
177 attachments.forEach(a => {
178 if (!uniqueAttachments.some(ua => ua.image_url === a.image_url)) {
179 uniqueAttachments.push(a);
180 }
181 });
182 return uniqueAttachments;
183}
184exports.extractImageUrls = extractImageUrls;
185/**
186 * Find issue mentions in body and replace them with links.
187 *
188 * @param body message to modify
189 * @param repo repository information
190 * @return string with issue mentions replaced with links
191 */
192function linkIssues(body, repo) {
193 if (!body || body.length === 0) {
194 return body;
195 }
196 const splitter = /(\[.+?\](?:\[.*?\]|\(.+?\)|:\s*http.*)|^```.*\n[\S\s]*?^```\s*\n|<.+?>)/m;
197 const bodyParts = body.split(splitter);
198 const baseUrl = htmlUrl(repo);
199 for (let j = 0; j < bodyParts.length; j += 2) {
200 let newPart = bodyParts[j];
201 const allIssueMentions = getIssueMentions(newPart);
202 allIssueMentions.forEach(i => {
203 const iMatchPrefix = (i.indexOf("#") === 0) ? `^|\\W` : repoIssueMatchPrefix;
204 const iRegExp = new RegExp(`(${iMatchPrefix})${i}(?!\\w)`, "g");
205 const iSlug = (i.indexOf("#") === 0) ? `${repo.owner}/${repo.name}${i}` : i;
206 const iUrlPath = iSlug.replace("#", "/issues/");
207 const iLink = slack.url(`${baseUrl}/${iUrlPath}`, i);
208 newPart = newPart.replace(iRegExp, `\$1${iLink}`);
209 });
210 bodyParts[j] = newPart;
211 }
212 return bodyParts.join("");
213}
214exports.linkIssues = linkIssues;
215const gitHubUserMatch = "[a-zA-Z\\d]+(?:-[a-zA-Z\\d]+)*";
216/**
217 * Regular expression to find issue mentions. There are capture
218 * groups for the issue repository owner, repository name, and issue
219 * number. The capture groups for repository owner and name are
220 * optional and therefore may be null, although if one is set, the
221 * other should be as well.
222 *
223 * The rules for preceding characters is different for current repo
224 * matches, e.g., "#43", and other repo matches, e.g., "some/repo#44".
225 * Current repo matches allow anything but word characters to precede
226 * them. Other repo matches only allow a few other characters to
227 * preceed them.
228 */
229const repoIssueMatchPrefix = "^|[[\\s:({]";
230// tslint:disable-next-line:max-line-length
231const issueMentionMatch = `(?:^|(?:${repoIssueMatchPrefix})(${gitHubUserMatch})\/(${gitHubUserMatch})|\\W)#([1-9]\\d*)(?!\\w)`;
232const issueMentionRegExp = new RegExp(issueMentionMatch, "g");
233/**
234 * Find all issue mentions and return an array of unique issue
235 * mentions as "#3" and "owner/repo#5".
236 *
237 * @param msg string that may contain mentions
238 * @return unique list of issue mentions as #N or O/R#N
239 */
240function getIssueMentions(msg = "") {
241 const allMentions = [];
242 let matches;
243 // tslint:disable-next-line:no-conditional-assignment
244 while (matches = issueMentionRegExp.exec(msg)) {
245 const owner = matches[1];
246 const repo = matches[2];
247 const issue = matches[3];
248 const slug = (owner && repo) ? `${owner}/${repo}` : "";
249 allMentions.push(`${slug}#${issue}`);
250 }
251 return _.uniq(allMentions);
252}
253exports.getIssueMentions = getIssueMentions;
254//# sourceMappingURL=lifecycleHelpers.js.map
\No newline at end of file