2
0

list-branches.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. /*
  2. * USAGE:
  3. * node list-branches [OPTION]
  4. *
  5. * OPTIONS:
  6. * --inactive : Return inactive branches (default)
  7. * --illegal : Return illegal named branches
  8. */
  9. const { execSync } = require('node:child_process');
  10. const url = require('node:url');
  11. const EXCLUDE_TERM_DAYS = 14;
  12. const EXCLUDE_PATTERNS = [
  13. /^support\/apply-tsed$/,
  14. // https://regex101.com/r/Lnx7Pz/3
  15. /^dev\/[\d.x]*$/,
  16. /^release\/.+$/,
  17. /^dependabot\/.+$/,
  18. ];
  19. const LEGAL_PATTERNS = [
  20. /^master$/,
  21. // https://regex101.com/r/p9xswM/4
  22. /^(dev|feat|imprv|support|fix|rc|release|tmp)\/.+$/,
  23. ];
  24. const GITHUB_REPOS_URI = 'https://github.com/growilabs/growi/';
  25. class BranchSummary {
  26. constructor(line) {
  27. const splitted = line.split('\t'); // split with '%09'
  28. this.authorDate = new Date(splitted[0].trim());
  29. this.authorName = splitted[1].trim();
  30. this.branchName = splitted[2].trim().replace(/^origin\//, '');
  31. this.subject = splitted[3].trim();
  32. }
  33. }
  34. function getExcludeTermDate() {
  35. const date = new Date();
  36. date.setDate(date.getDate() - EXCLUDE_TERM_DAYS);
  37. return date;
  38. }
  39. function getBranchSummaries() {
  40. // exec git for-each-ref
  41. const out = execSync(`\
  42. git for-each-ref refs/remotes \
  43. --sort=-committerdate \
  44. --format='%(authordate:iso) %09 %(authorname) %09 %(refname:short) %09 %(subject)'
  45. `).toString();
  46. // parse
  47. const summaries = out
  48. .split('\n')
  49. .filter((v) => v !== '') // trim empty string
  50. .map((line) => new BranchSummary(line))
  51. .filter((summary) => {
  52. // exclude branches that matches to patterns
  53. return !EXCLUDE_PATTERNS.some((pattern) =>
  54. pattern.test(summary.branchName),
  55. );
  56. });
  57. return summaries;
  58. }
  59. function getGitHubCommitsUrl(branchName) {
  60. return url.resolve(GITHUB_REPOS_URI, `commits/${branchName}`);
  61. }
  62. function getGitHubComparingLink(branchName) {
  63. const label = `master <- ${branchName}`;
  64. const link = url.resolve(GITHUB_REPOS_URI, `compare/${branchName}`);
  65. return `<${link}|${label}>`;
  66. }
  67. /**
  68. * @see https://api.slack.com/messaging/composing/layouts#building-attachments
  69. * @see https://github.com/marketplace/actions/slack-incoming-webhook
  70. *
  71. * @param {string} mode
  72. * @param {BranchSummary} summaries
  73. */
  74. function printSlackAttachments(mode, summaries) {
  75. const color = mode === 'illegal' ? 'warning' : '#999999';
  76. const attachments = summaries.map((summary) => {
  77. const { authorName, authorDate, branchName, subject } = summary;
  78. return {
  79. color,
  80. title: branchName,
  81. title_link: getGitHubCommitsUrl(branchName),
  82. fields: [
  83. {
  84. title: 'Author Date',
  85. value: authorDate,
  86. short: true,
  87. },
  88. {
  89. title: 'Author',
  90. value: authorName,
  91. short: true,
  92. },
  93. {
  94. title: 'Last Commit Subject',
  95. value: subject,
  96. },
  97. {
  98. title: 'Comparing Link',
  99. value: getGitHubComparingLink(branchName),
  100. },
  101. ],
  102. };
  103. });
  104. console.log(JSON.stringify(attachments));
  105. }
  106. function main(mode) {
  107. const summaries = getBranchSummaries();
  108. let filteredSummaries;
  109. switch (mode) {
  110. case 'illegal':
  111. filteredSummaries = summaries.filter((summary) => {
  112. // exclude branches that matches to patterns
  113. return !LEGAL_PATTERNS.some((pattern) =>
  114. pattern.test(summary.branchName),
  115. );
  116. });
  117. break;
  118. default: {
  119. const excludeTermDate = getExcludeTermDate();
  120. filteredSummaries = summaries.filter((summary) => {
  121. return summary.authorDate < excludeTermDate;
  122. });
  123. break;
  124. }
  125. }
  126. printSlackAttachments(mode, filteredSummaries);
  127. }
  128. const args = process.argv.slice(2);
  129. let mode = 'inactive';
  130. if (args.length > 0 && args[0] === '--illegal') {
  131. mode = 'illegal';
  132. }
  133. main(mode);