Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions changelog.d/1035.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
More flexible push event notifications
- Choose how commits appear: plain bullet list or collapsible dropdown
- Set a limit on how many commits to show per push
- Opt in to see full commit messages
Unified, clean formatting in both GitHub and GitLab integrations
Improved readability in Matrix rooms with collapsible commit lists

`html_dropdown`:
![html_demo](https://github.com/user-attachments/assets/e87916ec-5cd7-4853-83a8-ec1b2efad23b)

`md_bullets`:
![md_demo](https://github.com/user-attachments/assets/52ae3937-548b-47ab-8168-ffd9729cde35)
5 changes: 5 additions & 0 deletions docs/usage/room_configuration/github_repo.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ This connection supports a few options which can be defined in the room state:
| workflowRun.matchingBranch | Only report workflow runs if it matches this regex. | Regex string | _empty_ |
| workflowRun.includingWorkflows | Only report workflow runs with a matching workflow name. | Array of: String matching a workflow name | _empty_ |
| workflowRun.excludingWorkflows | Never report workflow runs with a matching workflow name. | Array of: String matching a workflow name | _empty_ |
| push | Configuration options for push events | `{ template: "md_bullets" \| "html_dropdown", maxCommits: number, showCommitBody: boolean }` |_empty_ |
| push.template | Defines the format of the message sent to the room for push events. | `"md_bullets"` \| `"html_dropdown"`|`"html_dropdown"` |
| push.maxCommits | Specifies the maximum number of commits to display in the message. | Positive integer|`5`|
| push.showCommitBody| Determines whether the commit message body (in addition to the commit title) should be included in the notification. | `true` \| `false`|`false` |


[^1]: `ignoreHooks` is no longer accepted for new state events. Use `enableHooks` to explicitly state all events you want to see.

Expand Down
4 changes: 4 additions & 0 deletions docs/usage/room_configuration/gitlab_project.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ This connection supports a few options which can be defined in the room state[^2
| includeCommentBody | Include the body of a comment when notifying on merge requests | Boolean | false |
| includingLabels | Only notify on issues matching these label names | Array of: String matching a label name | _empty_ |
| pushTagsRegex | Only mention pushed tags which match this regex | Regex string | _empty_ |
|push|Configuration options for push events|`{ template: "md_bullets" \| "html_dropdown", maxCommits: number, showCommitBody: boolean }`|*empty*|
|push.template|Defines the format of the message sent to the room for push events.|`"md_bullets"` \| `"md_bullets"`|`"html_dropdown"`|
|push.maxCommits|Specifies the maximum number of commits to display in the message.|Positive integer|`5`|
|push.showCommitBody|Determines whether the commit message body (in addition to the commit title) should be included in the notification.|`true` \| `false`|`false`|

[^1]: `ignoreHooks` is no longer accepted for new state events. Use `enableHooks` to explicitly state all events you want to see.

Expand Down
47 changes: 43 additions & 4 deletions src/Connections/GithubRepo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ export interface GitHubRepoConnectionOptions extends IConnectionState {
includingWorkflows?: string[];
excludingWorkflows?: string[];
};
push?: {
template?: "md_bullets" | "html_dropdown";
maxCommits?: number;
showCommitBody?: boolean;
};
}

export interface GitHubRepoConnectionState extends GitHubRepoConnectionOptions {
Expand Down Expand Up @@ -314,6 +319,15 @@ const ConnectionStateSchema = {
},
},
},
push: {
type: "object",
nullable: true,
properties: {
template: { type: "string", nullable: true, enum: ["md_bullets", "html_dropdown"] },
maxCommits: { type: "number", nullable: true },
showCommitBody: { type: "boolean", nullable: true }
}
}
},
required: ["org", "repo"],
additionalProperties: true,
Expand Down Expand Up @@ -1756,19 +1770,44 @@ export class GitHubRepoConnection
return;
}

const content = `**${event.sender.login}** pushed [${event.commits.length} commit${event.commits.length === 1 ? "" : "s"}](${event.compare}) to \`${event.ref}\` for ${event.repository.full_name}`;
const PUSH_MAX_COMMITS = 5;
const branchName = event.ref.replace("refs/heads/", "");
const commitsUrl = event.compare;
const branchUrl = `${event.repository.html_url}/tree/${branchName}`;

const { body, formatted_body } = FormatUtil.formatPushEventContent({
contributors: Object.values(event.commits.reduce((acc: Record<string, string>, commit) => {
acc[commit.author.name] = commit.author.name;
return acc;
}, {} as Record<string, string>)),
commits: event.commits.map(commit => ({
id: commit.id,
url: commit.url,
message: commit.message,
author: { name: commit.author.name },
})),
branchName,
branchUrl,
commitsUrl,
repoName: event.repository.full_name,
maxCommits: this.state.push?.maxCommits ?? PUSH_MAX_COMMITS,
shouldName: true,
template: this.state.push?.template ?? "html_dropdown",
showCommitBody: this.state.push?.showCommitBody,
});

const eventContent: IPushEventContent = {
...FormatUtil.getPartialBodyForGithubRepo(event.repository),
external_url: event.compare,
"uk.half-shot.matrix-hookshot.github.push": {
commits: event.commits.map((c) => c.id),
commits: event.commits.map(c => c.id),
pusher: `${event.pusher.name} <${event.pusher.email}>`,
ref: event.ref,
base_ref: event.base_ref,
},
msgtype: "m.notice",
body: content,
formatted_body: md.render(content),
body,
formatted_body,
format: "org.matrix.custom.html",
};
await this.intent.sendEvent(this.roomId, eventContent);
Expand Down
71 changes: 40 additions & 31 deletions src/Connections/GitlabRepo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { GitLabClient } from "../gitlab/Client";
import { IBridgeStorageProvider } from "../stores/StorageProvider";
import axios from "axios";
import { GitLabGrantChecker } from "../gitlab/GrantChecker";
import { FormatUtil } from "../FormatUtil";

export interface GitLabRepoConnectionState extends IConnectionState {
instance: string;
Expand All @@ -52,6 +53,11 @@ export interface GitLabRepoConnectionState extends IConnectionState {
pushTagsRegex?: string;
includingLabels?: string[];
excludingLabels?: string[];
push?: {
template?: "md_bullets" | "html_dropdown";
maxCommits?: number;
showCommitBody?: boolean;
};
}

interface ConnectionStateValidated extends GitLabRepoConnectionState {
Expand Down Expand Up @@ -169,6 +175,15 @@ const ConnectionStateSchema = {
type: "boolean",
nullable: true,
},
push: {
type: "object",
nullable: true,
properties: {
template: { type: "string", nullable: true, enum: ["md_bullets", "html_dropdown"] },
maxCommits: { type: "number", nullable: true },
showCommitBody: { type: "boolean", nullable: true }
}
}
},
required: ["instance", "path"],
additionalProperties: true,
Expand Down Expand Up @@ -885,44 +900,38 @@ export class GitLabRepoConnection
log.info(
`onGitLabPush ${this.roomId} ${this.instance.url}/${this.path} ${event.after}`,
);
const branchname = event.ref.replace("refs/heads/", "");
const commitsurl = `${event.project.homepage}/-/commits/${branchname}`;
const branchurl = `${event.project.homepage}/-/tree/${branchname}`;
const branchName = event.ref.replace("refs/heads/", "");
const commitsUrl = `${event.project.homepage}/-/commits/${branchName}`;
const branchUrl = `${event.project.homepage}/-/tree/${branchName}`;
const shouldName = !event.commits.every(
(c) => c.author.email === event.user_email,
);

const tooManyCommits = event.total_commits_count > PUSH_MAX_COMMITS;
const displayedCommits = tooManyCommits
? 1
: Math.min(event.total_commits_count, PUSH_MAX_COMMITS);

// Take the top 5 commits. The array is ordered in reverse.
const commits = event.commits
.reverse()
.slice(0, displayedCommits)
.map((commit) => {
return `[\`${commit.id.slice(0, 8)}\`](${event.project.homepage}/-/commit/${commit.id}) ${commit.title}${shouldName ? ` by ${commit.author.name}` : ""}`;
})
.join("\n - ");

let content =
`**${event.user_name}** pushed [${event.total_commits_count} commit${event.total_commits_count > 1 ? "s" : ""}](${commitsurl})` +
` to [\`${branchname}\`](${branchurl}) for ${event.project.path_with_namespace}`;

if (displayedCommits >= 2) {
content += `\n - ${commits}\n`;
} else if (displayedCommits === 1) {
content += `: ${commits}`;
if (tooManyCommits) {
content += `, and [${event.total_commits_count - 1} more](${commitsurl}) commits`;
}
}
const { body, formatted_body } = FormatUtil.formatPushEventContent({
contributors: Object.values(event.commits.reduce((acc: Record<string, string>, commit) => {
acc[commit.author.name] = commit.author.name;
return acc;
}, {} as Record<string, string>)),
commits: event.commits.map(commit => ({
id: commit.id,
url: commit.url,
message: commit.message,
author: { name: commit.author.name },
})),
branchName,
branchUrl,
commitsUrl,
repoName: event.repository.name,
maxCommits: this.state.push?.maxCommits ?? PUSH_MAX_COMMITS,
shouldName,
template: this.state.push?.template ?? "html_dropdown",
showCommitBody: this.state.push?.showCommitBody,
});

await this.intent.sendEvent(this.roomId, {
msgtype: "m.notice",
body: content,
formatted_body: md.render(content),
body,
formatted_body,
format: "org.matrix.custom.html",
});
}
Expand Down
142 changes: 142 additions & 0 deletions src/FormatUtil.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ProjectsListResponseData } from "./github/Types";
import { emojify } from "node-emoji";
import markdown from "markdown-it";
import { JiraIssue } from "./jira/Types";
import {
formatLabels,
Expand All @@ -10,6 +11,8 @@ import {
MinimalGitHubIssue,
} from "./libRs";

const md = new markdown();

interface IMinimalPR {
html_url: string;
id: number;
Expand Down Expand Up @@ -57,6 +60,145 @@ export class FormatUtil {
return `${repo.html_url}`;
}

public static formatPushEventContent({
contributors,
commits,
branchName,
branchUrl,
commitsUrl,
repoName,
maxCommits = 5,
shouldName = false,
template = "md_bullets",
showCommitBody = false,
}: {
contributors: string[];
commits: {
id: string;
url: string;
message: string;
author: { name: string };
}[];
branchName: string;
branchUrl: string;
commitsUrl: string;
repoName: string;
maxCommits?: number;
shouldName?: boolean;
template?: "md_bullets" | "html_dropdown";
showCommitBody?: boolean;
}) {
const tooManyCommits = commits.length > maxCommits;
const displayedCommits = Math.min(commits.length, maxCommits);
const separator = template === "md_bullets" ? "\n" : "<br>";
const multipleContributors = contributors.length > 1;

const formatCommitMessage = ({
commit,
showAuthor,
}: {
commit: typeof commits[0];
showAuthor: boolean;
}) => {
const { id, url, message, author } = commit;
const [title, ...body] = message.split("\n");
const authorInfo =
shouldName && showAuthor ? ` by \`${author.name}\`` : "";
const formattedBody =
showCommitBody && body.length
? `${separator}${body.join(separator)}`
: "";
const commitId = id.slice(0, 8);

return template === "md_bullets"
? `[\`${commitId}\`](${url}) ${title}${authorInfo}${formattedBody}`
: `<a href="${url}"><code>${commitId}</code></a> ${title}${authorInfo}${formattedBody}`;
};

if (template === "html_dropdown") {
if (commits.length === 1) {
const singleCommitMessage = formatCommitMessage({
commit: commits[0],
showAuthor: false,
});

return {
body: [
`**${contributors.join(", ")}** pushed [1 commit](${commitsUrl}) to [\`${branchName}\`](${branchUrl}) for ${repoName}: `,
"\n\n",
singleCommitMessage,
].join(""),
formatted_body: `<b>${contributors.join(
", "
)}</b> pushed <a href="${commitsUrl}">1 commit</a> to <a href="${branchUrl}"><code>${branchName}</code></a>
for ${repoName}: <br><br> ${singleCommitMessage}`,
format: "org.matrix.custom.html",
};
}

const commitList = commits
.slice(0, displayedCommits)
.map((commit) =>
formatCommitMessage({
commit,
showAuthor: multipleContributors,
})
)
.join("<hr>");

const extraCommits = tooManyCommits
? `<br><br><a href="${commitsUrl}">and ${commits.length - displayedCommits} more commits</a>`
: "";

return {
body: `**${contributors.join(", ")}** pushed [${commits.length} commit${
commits.length === 1 ? "" : "s"
}](${commitsUrl}) to [\`${branchName}\`](${branchUrl}) for ${repoName}`,
formatted_body: `
<details>
<summary><b>${contributors.join(", ")}</b> pushed
<a href="${commitsUrl}">${commits.length} commit${
commits.length === 1 ? "" : "s"
}</a> to <a href="${branchUrl}"><code>${branchName}</code></a> for ${repoName}
</summary>
<br>${commitList}${extraCommits}
</details>
`,
format: "org.matrix.custom.html",
};
}

const commitMessages = commits
.slice(0, displayedCommits)
.map((commit) =>
formatCommitMessage({ commit, showAuthor: multipleContributors })
)
.join("\n - ");

let content = `**${contributors.join(", ")}** pushed [${commits.length} commit${
commits.length === 1 ? "" : "s"
}](${commitsUrl}) to [\`${branchName}\`](${branchUrl}) for ${repoName}`;

if (displayedCommits === 1) {
const onlyTitle = commits[0].message.split("\n").length === 1;
content += `: \n\n ${formatCommitMessage({
commit: commits[0],
showAuthor: false,
})}`;
} else if (displayedCommits > 1) {
content += `\n - ${commitMessages}\n`;
}

if (tooManyCommits) {
content += `\nand [${commits.length - displayedCommits} more](${commitsUrl}) commits`;
}

return {
body: content,
formatted_body: md.render(content),
};
}

public static getPartialBodyForGithubRepo(repo: LooseMinimalGitHubRepo) {
if (!repo.id || !repo.html_url || !repo.full_name) {
throw Error("Missing keys in repo object");
Expand Down