1 // Copyright 2022 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
7 const process = require('child_process');
8 const https = require('https');
16 this.name_ = json.displayName;
18 this.email_ = json.email;
34 this.number_ = json.name;
35 this.reporter_id_ = json.reporter;
36 this.owner_id_ = json.owner ? json.owner.user : undefined;
37 this.last_update_ = json.modifyTime;
38 this.close_ = json.closeTime ? new Date(json.closeTime) : undefined;
40 this.url_ = undefined;
41 const parts = this.number_.split('/');
42 if (parts[0] === 'projects' && parts[2] === 'issues') {
43 const project = parts[1];
46 `https://bugs.chromium.org/p/${project}/issues/detail?id=${num}`;
54 return this.owner_id_;
57 return this.reporter_id_;
66 this.user_id_ = json.commenter;
67 this.timestamp_ = new Date(json.createTime);
68 this.timestamp_.setSeconds(0);
69 this.content_ = json.content;
70 this.fields_ = json.amendments ?
71 json.amendments.map(m => m.fieldName.toLowerCase()) :
74 this.json_ = JSON.stringify(json);
81 return this.timestamp_;
94 const fields = this.updatedFields;
96 // If bug A gets merged into bug B, then ignore the update for bug A. There
97 // will also be an update for bug B, and that will be counted instead.
98 if (fields && fields.indexOf('mergedinto') >= 0) {
102 // If bug A is marked as blocked on bug B, then that triggers updates for
103 // both bugs. So only count 'blockedon', and ignore 'blocking'.
104 const allowedFields = [
105 'blockedon', 'cc', 'components', 'label', 'owner', 'priority', 'status',
108 if (fields && fields.some(f => allowedFields.indexOf(f) >= 0)) {
116 constructor(project) {
117 this.token_ = this.getAuthToken_();
118 this.project_ = project;
122 const scope = 'https://www.googleapis.com/auth/userinfo.email';
124 'luci-auth', 'token', '-use-id-token', '-audience',
125 'https://monorail-prod.appspot.com', '-scopes', scope, '-json-output', '-'
127 const stdout = process.execSync(args.join(' ')).toString().trim();
128 const json = JSON.parse(stdout);
132 async fetchFromServer_(path, message) {
133 const hostname = 'api-dot-monorail-prod.appspot.com';
134 return new Promise((resolve, reject) => {
135 const postData = JSON.stringify(message);
141 'Content-Type': 'application/json',
142 'Accept': 'application/json',
143 'Authorization': `Bearer ${this.token_}`,
148 const req = https.request(options, (res) => {
149 log(`STATUS: ${res.statusCode}`);
150 log(`HEADERS: ${JSON.stringify(res.headers)}`);
152 res.setEncoding('utf8');
153 res.on('data', (chunk) => {
154 log(`BODY: ${chunk}`);
157 res.on('end', () => {
158 if (data.startsWith(')]}\'')) {
159 resolve(JSON.parse(data.substr(4)));
166 req.on('error', (e) => {
167 console.error(`problem with request: ${e.message}`);
171 // Write data to request body
172 log(`Writing ${postData}`);
179 * Calls SearchIssues with the given parameters.
181 * @param {string} query The query to use to search.
182 * @param {Number} pageSize The maximum issues to return.
183 * @param {string} pageToken The page token from the previous call.
187 async searchIssuesPagination_(query, pageSize, pageToken) {
189 'projects': [this.project_],
191 'pageToken': pageToken,
194 message['pageSize'] = pageSize;
196 const url = '/prpc/monorail.v3.Issues/SearchIssues';
197 return this.fetchFromServer_(url, message);
201 * Searches Monorail for issues using the given query.
202 * TODO(crbug.com/monorail/7143): SearchIssues only accepts one project.
204 * @param {string} query The query to use to search.
206 * @return {Array<CrBugIssue>}
208 async search(query) {
209 const pageSize = 100;
214 await this.searchIssuesPagination_(query, pageSize, pageToken);
216 issues = issues.concat(resp.issues.map(i => new CrBugIssue(i)));
218 pageToken = resp.nextPageToken;
224 * Calls ListComments with the given parameters.
226 * @param {string} issueName Resource name of the issue.
227 * @param {string} filter The approval filter query.
228 * @param {Number} pageSize The maximum number of comments to return.
229 * @param {string} pageToken The page token from the previous request.
233 async listCommentsPagination_(issueName, pageToken, pageSize) {
236 'pageToken': pageToken,
240 message['pageSize'] = pageSize;
242 const url = '/prpc/monorail.v3.Issues/ListComments';
243 return this.fetchFromServer_(url, message);
247 * Returns all comments and previous/current descriptions of an issue.
249 * @param {CrBugIssue} issue The CrBugIssue instance.
251 * @return {Array<CrBugComment>}
253 async getComments(issue) {
257 const resp = await this.listCommentsPagination_(issue.number, pageToken);
259 comments = comments.concat(resp.comments.map(c => new CrBugComment(c)));
261 pageToken = resp.nextPageToken;
267 * Returns the user associated with 'username'.
269 * @param {string} username The username (e.g. linus@chromium.org).
271 * @return {CrBugUser}
273 async getUser(username) {
274 const url = '/prpc/monorail.v3.Users/GetUser';
276 name: `users/${username}`,
278 return new CrBugUser(await this.fetchFromServer_(url, message));