xref: /aosp_15_r20/external/pigweed/docs/_static/js/changelog.js (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1*61c4878aSAndroid Build Coastguard Worker// Copyright 2023 The Pigweed Authors
2*61c4878aSAndroid Build Coastguard Worker//
3*61c4878aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License"); you may not
4*61c4878aSAndroid Build Coastguard Worker// use this file except in compliance with the License. You may obtain a copy of
5*61c4878aSAndroid Build Coastguard Worker// the License at
6*61c4878aSAndroid Build Coastguard Worker//
7*61c4878aSAndroid Build Coastguard Worker//     https://www.apache.org/licenses/LICENSE-2.0
8*61c4878aSAndroid Build Coastguard Worker//
9*61c4878aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software
10*61c4878aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11*61c4878aSAndroid Build Coastguard Worker// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12*61c4878aSAndroid Build Coastguard Worker// License for the specific language governing permissions and limitations under
13*61c4878aSAndroid Build Coastguard Worker// the License.
14*61c4878aSAndroid Build Coastguard Worker
15*61c4878aSAndroid Build Coastguard Worker// This file powers the changelog tool in //docs/contributing/changelog.rst.
16*61c4878aSAndroid Build Coastguard Worker// We use this tool to speed up the generation of bi-weekly changelog
17*61c4878aSAndroid Build Coastguard Worker// updates. It fetches the commits over a user-specified timeframe, derives
18*61c4878aSAndroid Build Coastguard Worker// a little metadata about each commit, organizes the commits, and renders
19*61c4878aSAndroid Build Coastguard Worker// the data as reStructuredText. It doesn't completely automate the changelog
20*61c4878aSAndroid Build Coastguard Worker// update process (a contributor still needs to manually write the summaries)
21*61c4878aSAndroid Build Coastguard Worker// but it does reduce a lot of the toil.
22*61c4878aSAndroid Build Coastguard Worker
23*61c4878aSAndroid Build Coastguard Worker// Get the commits from the user-specified timeframe.
24*61c4878aSAndroid Build Coastguard Workerasync function get() {
25*61c4878aSAndroid Build Coastguard Worker  const start = `${document.querySelector('#start').value}T00:00:00Z`;
26*61c4878aSAndroid Build Coastguard Worker  const end = `${document.querySelector('#end').value}T23:59:59Z`;
27*61c4878aSAndroid Build Coastguard Worker  document.querySelector('#status').textContent = `Getting commit data...`;
28*61c4878aSAndroid Build Coastguard Worker  let page = 1;
29*61c4878aSAndroid Build Coastguard Worker  let done = false;
30*61c4878aSAndroid Build Coastguard Worker  let commits = [];
31*61c4878aSAndroid Build Coastguard Worker  while (!done) {
32*61c4878aSAndroid Build Coastguard Worker    // The commits are pulled from the Pigweed mirror on GitHub because
33*61c4878aSAndroid Build Coastguard Worker    // GitHub provides a better API than Gerrit for this task.
34*61c4878aSAndroid Build Coastguard Worker    let url = new URL(`https://api.github.com/repos/google/pigweed/commits`);
35*61c4878aSAndroid Build Coastguard Worker    const params = { since: start, until: end, per_page: 100, page };
36*61c4878aSAndroid Build Coastguard Worker    Object.keys(params).forEach((key) =>
37*61c4878aSAndroid Build Coastguard Worker      url.searchParams.append(key, params[key]),
38*61c4878aSAndroid Build Coastguard Worker    );
39*61c4878aSAndroid Build Coastguard Worker    const headers = {
40*61c4878aSAndroid Build Coastguard Worker      Accept: 'application/vnd.github+json',
41*61c4878aSAndroid Build Coastguard Worker      'X-GitHub-Api-Version': '2022-11-28',
42*61c4878aSAndroid Build Coastguard Worker    };
43*61c4878aSAndroid Build Coastguard Worker    const response = await fetch(url.href, { method: 'GET', headers });
44*61c4878aSAndroid Build Coastguard Worker    if (!response.ok) {
45*61c4878aSAndroid Build Coastguard Worker      document.querySelector('#status').textContent =
46*61c4878aSAndroid Build Coastguard Worker        'An error occurred while fetching the commit data.';
47*61c4878aSAndroid Build Coastguard Worker      console.error(response);
48*61c4878aSAndroid Build Coastguard Worker      return;
49*61c4878aSAndroid Build Coastguard Worker    }
50*61c4878aSAndroid Build Coastguard Worker    const data = await response.json();
51*61c4878aSAndroid Build Coastguard Worker    if (data.length === 0) {
52*61c4878aSAndroid Build Coastguard Worker      done = true;
53*61c4878aSAndroid Build Coastguard Worker      continue;
54*61c4878aSAndroid Build Coastguard Worker    }
55*61c4878aSAndroid Build Coastguard Worker    commits = commits.concat(data);
56*61c4878aSAndroid Build Coastguard Worker    page += 1;
57*61c4878aSAndroid Build Coastguard Worker  }
58*61c4878aSAndroid Build Coastguard Worker  return commits;
59*61c4878aSAndroid Build Coastguard Worker}
60*61c4878aSAndroid Build Coastguard Worker
61*61c4878aSAndroid Build Coastguard Worker// Weed out all the data that GitHub provides that we don't need.
62*61c4878aSAndroid Build Coastguard Worker// Also, parse the "subject" of the commit, which is the first line
63*61c4878aSAndroid Build Coastguard Worker// of the commit message.
64*61c4878aSAndroid Build Coastguard Workerasync function normalize(commits) {
65*61c4878aSAndroid Build Coastguard Worker  function parseSubject(message) {
66*61c4878aSAndroid Build Coastguard Worker    const end = message.indexOf('\n\n');
67*61c4878aSAndroid Build Coastguard Worker    return message.substring(0, end);
68*61c4878aSAndroid Build Coastguard Worker  }
69*61c4878aSAndroid Build Coastguard Worker
70*61c4878aSAndroid Build Coastguard Worker  document.querySelector('#status').textContent = 'Normalizing data...';
71*61c4878aSAndroid Build Coastguard Worker  let normalizedCommits = [];
72*61c4878aSAndroid Build Coastguard Worker  commits.forEach((commit) => {
73*61c4878aSAndroid Build Coastguard Worker    normalizedCommits.push({
74*61c4878aSAndroid Build Coastguard Worker      sha: commit.sha,
75*61c4878aSAndroid Build Coastguard Worker      message: commit.commit.message,
76*61c4878aSAndroid Build Coastguard Worker      date: commit.commit.committer.date,
77*61c4878aSAndroid Build Coastguard Worker      subject: parseSubject(commit.commit.message),
78*61c4878aSAndroid Build Coastguard Worker    });
79*61c4878aSAndroid Build Coastguard Worker  });
80*61c4878aSAndroid Build Coastguard Worker  return normalizedCommits;
81*61c4878aSAndroid Build Coastguard Worker}
82*61c4878aSAndroid Build Coastguard Worker
83*61c4878aSAndroid Build Coastguard Worker// Derive Pigweed-specific metadata from each commit.
84*61c4878aSAndroid Build Coastguard Workerasync function annotate(commits) {
85*61c4878aSAndroid Build Coastguard Worker  function categorize(preamble) {
86*61c4878aSAndroid Build Coastguard Worker    if (preamble.startsWith('third_party')) {
87*61c4878aSAndroid Build Coastguard Worker      return 'Third party';
88*61c4878aSAndroid Build Coastguard Worker    } else if (preamble.startsWith('pw_')) {
89*61c4878aSAndroid Build Coastguard Worker      return 'Modules';
90*61c4878aSAndroid Build Coastguard Worker    } else if (preamble.startsWith('targets')) {
91*61c4878aSAndroid Build Coastguard Worker      return 'Targets';
92*61c4878aSAndroid Build Coastguard Worker    } else if (['build', 'bazel', 'cmake'].includes(preamble)) {
93*61c4878aSAndroid Build Coastguard Worker      return 'Build';
94*61c4878aSAndroid Build Coastguard Worker    } else if (['rust', 'python'].includes(preamble)) {
95*61c4878aSAndroid Build Coastguard Worker      return 'Language support';
96*61c4878aSAndroid Build Coastguard Worker    } else if (['zephyr', 'freertos'].includes(preamble)) {
97*61c4878aSAndroid Build Coastguard Worker      return 'OS support';
98*61c4878aSAndroid Build Coastguard Worker    } else if (preamble.startsWith('SEED')) {
99*61c4878aSAndroid Build Coastguard Worker      return 'SEEDs';
100*61c4878aSAndroid Build Coastguard Worker    } else if (preamble === 'docs') {
101*61c4878aSAndroid Build Coastguard Worker      return 'Docs';
102*61c4878aSAndroid Build Coastguard Worker    } else {
103*61c4878aSAndroid Build Coastguard Worker      return 'Miscellaneous';
104*61c4878aSAndroid Build Coastguard Worker    }
105*61c4878aSAndroid Build Coastguard Worker  }
106*61c4878aSAndroid Build Coastguard Worker
107*61c4878aSAndroid Build Coastguard Worker  function parseTitle(message) {
108*61c4878aSAndroid Build Coastguard Worker    const start = message.indexOf(':') + 1;
109*61c4878aSAndroid Build Coastguard Worker    const tmp = message.substring(start);
110*61c4878aSAndroid Build Coastguard Worker    const end = tmp.indexOf('\n');
111*61c4878aSAndroid Build Coastguard Worker    return tmp.substring(0, end).trim();
112*61c4878aSAndroid Build Coastguard Worker  }
113*61c4878aSAndroid Build Coastguard Worker
114*61c4878aSAndroid Build Coastguard Worker  function parseBugUrl(message, bugLabel) {
115*61c4878aSAndroid Build Coastguard Worker    const start = message.indexOf(bugLabel);
116*61c4878aSAndroid Build Coastguard Worker    const tmp = message.substring(start);
117*61c4878aSAndroid Build Coastguard Worker    const end = tmp.indexOf('\n');
118*61c4878aSAndroid Build Coastguard Worker    let bug = tmp.substring(bugLabel.length, end).trim();
119*61c4878aSAndroid Build Coastguard Worker    if (bug.startsWith('b/')) bug = bug.replace('b/', '');
120*61c4878aSAndroid Build Coastguard Worker    return `https://issues.pigweed.dev/issues/${bug}`;
121*61c4878aSAndroid Build Coastguard Worker  }
122*61c4878aSAndroid Build Coastguard Worker
123*61c4878aSAndroid Build Coastguard Worker  function parseChangeUrl(message) {
124*61c4878aSAndroid Build Coastguard Worker    const label = 'Reviewed-on:';
125*61c4878aSAndroid Build Coastguard Worker    const start = message.indexOf(label);
126*61c4878aSAndroid Build Coastguard Worker    const tmp = message.substring(start);
127*61c4878aSAndroid Build Coastguard Worker    const end = tmp.indexOf('\n');
128*61c4878aSAndroid Build Coastguard Worker    const change = tmp.substring(label.length, end).trim();
129*61c4878aSAndroid Build Coastguard Worker    return change;
130*61c4878aSAndroid Build Coastguard Worker  }
131*61c4878aSAndroid Build Coastguard Worker
132*61c4878aSAndroid Build Coastguard Worker  for (let i = 0; i < commits.length; i++) {
133*61c4878aSAndroid Build Coastguard Worker    let commit = commits[i];
134*61c4878aSAndroid Build Coastguard Worker    const { message, sha } = commit;
135*61c4878aSAndroid Build Coastguard Worker    commit.url = `https://cs.opensource.google/pigweed/pigweed/+/${sha}`;
136*61c4878aSAndroid Build Coastguard Worker    commit.change = parseChangeUrl(message);
137*61c4878aSAndroid Build Coastguard Worker    commit.summary = message.substring(0, message.indexOf('\n'));
138*61c4878aSAndroid Build Coastguard Worker    commit.preamble = message.substring(0, message.indexOf(':'));
139*61c4878aSAndroid Build Coastguard Worker    commit.category = categorize(commit.preamble);
140*61c4878aSAndroid Build Coastguard Worker    commit.title = parseTitle(message);
141*61c4878aSAndroid Build Coastguard Worker    // We use syntax like "pw_{tokenizer,string}" to indicate that a commit
142*61c4878aSAndroid Build Coastguard Worker    // affects both pw_tokenizer and pw_string. The next logic detects this
143*61c4878aSAndroid Build Coastguard Worker    // situation. The same commit gets duplicated to each module's section.
144*61c4878aSAndroid Build Coastguard Worker    // The rationale for the duplication is that someone might only care about
145*61c4878aSAndroid Build Coastguard Worker    // pw_tokenizer and they should be able to see all commits that affected
146*61c4878aSAndroid Build Coastguard Worker    // in a single place.
147*61c4878aSAndroid Build Coastguard Worker    if (commit.preamble.indexOf('{') > -1) {
148*61c4878aSAndroid Build Coastguard Worker      commit.topics = [];
149*61c4878aSAndroid Build Coastguard Worker      const topics = commit.preamble
150*61c4878aSAndroid Build Coastguard Worker        .substring(
151*61c4878aSAndroid Build Coastguard Worker          commit.preamble.indexOf('{') + 1,
152*61c4878aSAndroid Build Coastguard Worker          commit.preamble.indexOf('}'),
153*61c4878aSAndroid Build Coastguard Worker        )
154*61c4878aSAndroid Build Coastguard Worker        .split(',');
155*61c4878aSAndroid Build Coastguard Worker      topics.forEach((topic) => commit.topics.push(`pw_${topic}`));
156*61c4878aSAndroid Build Coastguard Worker    } else {
157*61c4878aSAndroid Build Coastguard Worker      commit.topics = [commit.preamble];
158*61c4878aSAndroid Build Coastguard Worker    }
159*61c4878aSAndroid Build Coastguard Worker    const bugLabels = ['Bug:', 'Fixes:', 'Fixed:'];
160*61c4878aSAndroid Build Coastguard Worker    for (let i = 0; i < bugLabels.length; i++) {
161*61c4878aSAndroid Build Coastguard Worker      const bugLabel = bugLabels[i];
162*61c4878aSAndroid Build Coastguard Worker      if (message.indexOf(bugLabel) > -1) {
163*61c4878aSAndroid Build Coastguard Worker        const bugUrl = parseBugUrl(message, bugLabel);
164*61c4878aSAndroid Build Coastguard Worker        const bugId = bugUrl.substring(bugUrl.lastIndexOf('/') + 1);
165*61c4878aSAndroid Build Coastguard Worker        commit.issue = { id: bugId, url: bugUrl };
166*61c4878aSAndroid Build Coastguard Worker        break;
167*61c4878aSAndroid Build Coastguard Worker      }
168*61c4878aSAndroid Build Coastguard Worker    }
169*61c4878aSAndroid Build Coastguard Worker  }
170*61c4878aSAndroid Build Coastguard Worker  return commits;
171*61c4878aSAndroid Build Coastguard Worker}
172*61c4878aSAndroid Build Coastguard Worker
173*61c4878aSAndroid Build Coastguard Worker// If there are any categories of commits that we don't want to surface
174*61c4878aSAndroid Build Coastguard Worker// in the changelog, this function is where we drop them.
175*61c4878aSAndroid Build Coastguard Workerasync function filter(commits) {
176*61c4878aSAndroid Build Coastguard Worker  const filteredCommits = commits.filter((commit) => {
177*61c4878aSAndroid Build Coastguard Worker    if (commit.preamble === 'roll') return false;
178*61c4878aSAndroid Build Coastguard Worker    return true;
179*61c4878aSAndroid Build Coastguard Worker  });
180*61c4878aSAndroid Build Coastguard Worker  return filteredCommits;
181*61c4878aSAndroid Build Coastguard Worker}
182*61c4878aSAndroid Build Coastguard Worker
183*61c4878aSAndroid Build Coastguard Worker// Render the commit data as reStructuredText.
184*61c4878aSAndroid Build Coastguard Workerasync function render(commits) {
185*61c4878aSAndroid Build Coastguard Worker  function organizeByCategoryAndTopic(commits) {
186*61c4878aSAndroid Build Coastguard Worker    let categories = {};
187*61c4878aSAndroid Build Coastguard Worker    commits.forEach((commit) => {
188*61c4878aSAndroid Build Coastguard Worker      const { category } = commit;
189*61c4878aSAndroid Build Coastguard Worker      if (!(category in categories)) categories[category] = {};
190*61c4878aSAndroid Build Coastguard Worker      commit.topics.forEach((topic) => {
191*61c4878aSAndroid Build Coastguard Worker        topic in categories[category]
192*61c4878aSAndroid Build Coastguard Worker          ? categories[category][topic].push(commit)
193*61c4878aSAndroid Build Coastguard Worker          : (categories[category][topic] = [commit]);
194*61c4878aSAndroid Build Coastguard Worker      });
195*61c4878aSAndroid Build Coastguard Worker    });
196*61c4878aSAndroid Build Coastguard Worker    return categories;
197*61c4878aSAndroid Build Coastguard Worker  }
198*61c4878aSAndroid Build Coastguard Worker
199*61c4878aSAndroid Build Coastguard Worker  async function createRestSection(commits) {
200*61c4878aSAndroid Build Coastguard Worker    const locale = 'en-US';
201*61c4878aSAndroid Build Coastguard Worker    const format = { day: '2-digit', month: 'short', year: 'numeric' };
202*61c4878aSAndroid Build Coastguard Worker    const start = new Date(
203*61c4878aSAndroid Build Coastguard Worker      document.querySelector('#start').value,
204*61c4878aSAndroid Build Coastguard Worker    ).toLocaleDateString(locale, format);
205*61c4878aSAndroid Build Coastguard Worker    const end = new Date(
206*61c4878aSAndroid Build Coastguard Worker      document.querySelector('#end').value,
207*61c4878aSAndroid Build Coastguard Worker    ).toLocaleDateString(locale, format);
208*61c4878aSAndroid Build Coastguard Worker    let rest = '';
209*61c4878aSAndroid Build Coastguard Worker    rest += '.. _docs-changelog-latest:\n\n';
210*61c4878aSAndroid Build Coastguard Worker    const title = `${end}`;
211*61c4878aSAndroid Build Coastguard Worker    rest += `${'-'.repeat(title.length)}\n`;
212*61c4878aSAndroid Build Coastguard Worker    rest += `${title}\n`;
213*61c4878aSAndroid Build Coastguard Worker    rest += `${'-'.repeat(title.length)}\n\n`;
214*61c4878aSAndroid Build Coastguard Worker    rest += '.. changelog_highlights_start\n\n';
215*61c4878aSAndroid Build Coastguard Worker    rest += `Highlights (${start} to ${end}):\n\n`;
216*61c4878aSAndroid Build Coastguard Worker    rest += '* **<Highlight 1>**: Description\n';
217*61c4878aSAndroid Build Coastguard Worker    rest += '* **<Highlight 2>**: Description\n';
218*61c4878aSAndroid Build Coastguard Worker    rest += '* **<Highlight 3>**: Description\n\n';
219*61c4878aSAndroid Build Coastguard Worker    rest += '.. changelog_highlights_end\n\n';
220*61c4878aSAndroid Build Coastguard Worker    rest += 'Active SEEDs\n';
221*61c4878aSAndroid Build Coastguard Worker    rest += '============\n';
222*61c4878aSAndroid Build Coastguard Worker    rest += 'Help shape the future of Pigweed! Please visit :ref:`seed-0000`\n';
223*61c4878aSAndroid Build Coastguard Worker    rest += 'and leave feedback on the RFCs (i.e. SEEDs) marked\n';
224*61c4878aSAndroid Build Coastguard Worker    rest += '``Open for Comments``.\n\n';
225*61c4878aSAndroid Build Coastguard Worker    rest += '.. Note: There is space between the following section headings\n';
226*61c4878aSAndroid Build Coastguard Worker    rest += '.. and commit lists to remind you to write a summary for each\n';
227*61c4878aSAndroid Build Coastguard Worker    rest += '.. section. If a summary is not needed, delete the extra\n';
228*61c4878aSAndroid Build Coastguard Worker    rest += '.. space.\n\n';
229*61c4878aSAndroid Build Coastguard Worker    const categories = [
230*61c4878aSAndroid Build Coastguard Worker      'Modules',
231*61c4878aSAndroid Build Coastguard Worker      'Build systems',
232*61c4878aSAndroid Build Coastguard Worker      'Hardware targets',
233*61c4878aSAndroid Build Coastguard Worker      'Language support',
234*61c4878aSAndroid Build Coastguard Worker      'OS support',
235*61c4878aSAndroid Build Coastguard Worker      'Docs',
236*61c4878aSAndroid Build Coastguard Worker      'SEEDs',
237*61c4878aSAndroid Build Coastguard Worker      'Third-party software',
238*61c4878aSAndroid Build Coastguard Worker      'Miscellaneous',
239*61c4878aSAndroid Build Coastguard Worker    ];
240*61c4878aSAndroid Build Coastguard Worker    for (let i = 0; i < categories.length; i++) {
241*61c4878aSAndroid Build Coastguard Worker      const category = categories[i];
242*61c4878aSAndroid Build Coastguard Worker      if (!(category in commits)) continue;
243*61c4878aSAndroid Build Coastguard Worker      rest += `${category}\n`;
244*61c4878aSAndroid Build Coastguard Worker      rest += `${'='.repeat(category.length)}\n\n`;
245*61c4878aSAndroid Build Coastguard Worker      let topics = Object.keys(commits[category]);
246*61c4878aSAndroid Build Coastguard Worker      topics.sort();
247*61c4878aSAndroid Build Coastguard Worker      topics.forEach((topic) => {
248*61c4878aSAndroid Build Coastguard Worker        // Some topics should not be rendered because they're redundant.
249*61c4878aSAndroid Build Coastguard Worker        // E.g. we already have a "Docs" H3 heading so we don't need another
250*61c4878aSAndroid Build Coastguard Worker        // "docs" H4 heading right after it.
251*61c4878aSAndroid Build Coastguard Worker        const topicsToSkip = ['docs'];
252*61c4878aSAndroid Build Coastguard Worker        if (!topicsToSkip.includes(topic)) {
253*61c4878aSAndroid Build Coastguard Worker          rest += `${topic}\n`;
254*61c4878aSAndroid Build Coastguard Worker          rest += `${'-'.repeat(topic.length)}\n\n\n`;
255*61c4878aSAndroid Build Coastguard Worker        }
256*61c4878aSAndroid Build Coastguard Worker        commits[category][topic].forEach((commit) => {
257*61c4878aSAndroid Build Coastguard Worker          // Escape any backticks that are used in the commit message.
258*61c4878aSAndroid Build Coastguard Worker          const change = commit.change.replaceAll('`', '\\`');
259*61c4878aSAndroid Build Coastguard Worker          // Use double underscores to make the links anonymous so that Sphinx
260*61c4878aSAndroid Build Coastguard Worker          // doesn't error when the same link is used multiple times.
261*61c4878aSAndroid Build Coastguard Worker          // https://github.com/sphinx-doc/sphinx/issues/3921
262*61c4878aSAndroid Build Coastguard Worker          rest += `* \`${commit.title}\n  <${change}>\`__\n`;
263*61c4878aSAndroid Build Coastguard Worker          if (commit.issue)
264*61c4878aSAndroid Build Coastguard Worker            rest += `  (issue \`#${commit.issue.id} <${commit.issue.url}>\`__)\n`;
265*61c4878aSAndroid Build Coastguard Worker        });
266*61c4878aSAndroid Build Coastguard Worker        rest += '\n';
267*61c4878aSAndroid Build Coastguard Worker      });
268*61c4878aSAndroid Build Coastguard Worker    }
269*61c4878aSAndroid Build Coastguard Worker    const section = document.createElement('section');
270*61c4878aSAndroid Build Coastguard Worker    const heading = document.createElement('h2');
271*61c4878aSAndroid Build Coastguard Worker    section.appendChild(heading);
272*61c4878aSAndroid Build Coastguard Worker    const pre = document.createElement('pre');
273*61c4878aSAndroid Build Coastguard Worker    section.appendChild(pre);
274*61c4878aSAndroid Build Coastguard Worker    const code = document.createElement('code');
275*61c4878aSAndroid Build Coastguard Worker    pre.appendChild(code);
276*61c4878aSAndroid Build Coastguard Worker    code.textContent = rest;
277*61c4878aSAndroid Build Coastguard Worker    try {
278*61c4878aSAndroid Build Coastguard Worker      await navigator.clipboard.writeText(rest);
279*61c4878aSAndroid Build Coastguard Worker      document.querySelector('#status').textContent =
280*61c4878aSAndroid Build Coastguard Worker        'Done! The output was copied to your clipboard.';
281*61c4878aSAndroid Build Coastguard Worker    } catch (error) {
282*61c4878aSAndroid Build Coastguard Worker      document.querySelector('#status').textContent = 'Done!';
283*61c4878aSAndroid Build Coastguard Worker    }
284*61c4878aSAndroid Build Coastguard Worker    return section;
285*61c4878aSAndroid Build Coastguard Worker  }
286*61c4878aSAndroid Build Coastguard Worker
287*61c4878aSAndroid Build Coastguard Worker  const organizedCommits = organizeByCategoryAndTopic(commits);
288*61c4878aSAndroid Build Coastguard Worker  document.querySelector('#status').textContent = 'Rendering data...';
289*61c4878aSAndroid Build Coastguard Worker  const container = document.createElement('div');
290*61c4878aSAndroid Build Coastguard Worker  const restSection = await createRestSection(organizedCommits);
291*61c4878aSAndroid Build Coastguard Worker  container.appendChild(restSection);
292*61c4878aSAndroid Build Coastguard Worker  return container;
293*61c4878aSAndroid Build Coastguard Worker}
294*61c4878aSAndroid Build Coastguard Worker
295*61c4878aSAndroid Build Coastguard Worker// Use the placeholder in the start and end date text inputs to guide users
296*61c4878aSAndroid Build Coastguard Worker// towards the correct date format.
297*61c4878aSAndroid Build Coastguard Workerfunction populateDates() {
298*61c4878aSAndroid Build Coastguard Worker  // Suggest the start date.
299*61c4878aSAndroid Build Coastguard Worker  let twoWeeksAgo = new Date();
300*61c4878aSAndroid Build Coastguard Worker  twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14);
301*61c4878aSAndroid Build Coastguard Worker  const twoWeeksAgoFormatted = twoWeeksAgo.toISOString().slice(0, 10);
302*61c4878aSAndroid Build Coastguard Worker  document.querySelector('#start').placeholder = twoWeeksAgoFormatted;
303*61c4878aSAndroid Build Coastguard Worker  // Suggest the end date.
304*61c4878aSAndroid Build Coastguard Worker  const today = new Date();
305*61c4878aSAndroid Build Coastguard Worker  const todayFormatted = today.toISOString().slice(0, 10);
306*61c4878aSAndroid Build Coastguard Worker  document.querySelector('#end').placeholder = todayFormatted;
307*61c4878aSAndroid Build Coastguard Worker}
308*61c4878aSAndroid Build Coastguard Worker
309*61c4878aSAndroid Build Coastguard Worker// Enable the "generate" button only when the start and end dates are valid.
310*61c4878aSAndroid Build Coastguard Workerfunction validateDates() {
311*61c4878aSAndroid Build Coastguard Worker  const dateFormat = /^\d{4}-\d{2}-\d{2}$/;
312*61c4878aSAndroid Build Coastguard Worker  const start = document.querySelector('#start').value;
313*61c4878aSAndroid Build Coastguard Worker  const end = document.querySelector('#end').value;
314*61c4878aSAndroid Build Coastguard Worker  const status = document.querySelector('#status');
315*61c4878aSAndroid Build Coastguard Worker  let generate = document.querySelector('#generate');
316*61c4878aSAndroid Build Coastguard Worker  if (!start.match(dateFormat) || !end.match(dateFormat)) {
317*61c4878aSAndroid Build Coastguard Worker    generate.disabled = true;
318*61c4878aSAndroid Build Coastguard Worker    status.textContent = 'Invalid start or end date (should be YYYY-MM-DD)';
319*61c4878aSAndroid Build Coastguard Worker  } else {
320*61c4878aSAndroid Build Coastguard Worker    generate.disabled = false;
321*61c4878aSAndroid Build Coastguard Worker    status.textContent = 'Ready to generate!';
322*61c4878aSAndroid Build Coastguard Worker  }
323*61c4878aSAndroid Build Coastguard Worker}
324*61c4878aSAndroid Build Coastguard Worker
325*61c4878aSAndroid Build Coastguard Worker// Set up the date placeholder and validation stuff when the page loads.
326*61c4878aSAndroid Build Coastguard Workerwindow.addEventListener('load', () => {
327*61c4878aSAndroid Build Coastguard Worker  populateDates();
328*61c4878aSAndroid Build Coastguard Worker  document.querySelector('#start').addEventListener('keyup', validateDates);
329*61c4878aSAndroid Build Coastguard Worker  document.querySelector('#end').addEventListener('keyup', validateDates);
330*61c4878aSAndroid Build Coastguard Worker});
331*61c4878aSAndroid Build Coastguard Worker
332*61c4878aSAndroid Build Coastguard Worker// Run through the whole get/normalize/annotate/filter/render pipeline when
333*61c4878aSAndroid Build Coastguard Worker// the user clicks the "generate" button.
334*61c4878aSAndroid Build Coastguard Workerdocument.querySelector('#generate').addEventListener('click', async (e) => {
335*61c4878aSAndroid Build Coastguard Worker  e.target.disabled = true;
336*61c4878aSAndroid Build Coastguard Worker  const rawCommits = await get();
337*61c4878aSAndroid Build Coastguard Worker  const normalizedCommits = await normalize(rawCommits);
338*61c4878aSAndroid Build Coastguard Worker  const annotatedCommits = await annotate(normalizedCommits);
339*61c4878aSAndroid Build Coastguard Worker  const filteredCommits = await filter(annotatedCommits);
340*61c4878aSAndroid Build Coastguard Worker  const output = await render(filteredCommits);
341*61c4878aSAndroid Build Coastguard Worker  document.querySelector('#output').innerHTML = '';
342*61c4878aSAndroid Build Coastguard Worker  document.querySelector('#output').appendChild(output);
343*61c4878aSAndroid Build Coastguard Worker  e.target.disabled = false;
344*61c4878aSAndroid Build Coastguard Worker});
345