forked from espressif/esp-idf
Merge branch 'update/conventional_commit_rules' into 'master'
ci(danger): Change commit message default rules Closes IDF-7656 See merge request espressif/esp-idf!24428
This commit is contained in:
@@ -1,120 +1,172 @@
|
|||||||
const { OpenAI } = require("langchain/llms/openai");
|
|
||||||
const {
|
const {
|
||||||
ChatPromptTemplate,
|
minimumSummaryChars,
|
||||||
SystemMessagePromptTemplate,
|
maximumSummaryChars,
|
||||||
} = require("langchain/prompts");
|
maximumBodyLineChars,
|
||||||
|
allowedTypes,
|
||||||
|
} = require("./mrCommitsConstants.js");
|
||||||
|
const { gptStandardModelTokens } = require("./mrCommitsConstants.js");
|
||||||
|
|
||||||
|
const { ChatPromptTemplate } = require("langchain/prompts");
|
||||||
|
const { SystemMessagePromptTemplate } = require("langchain/prompts");
|
||||||
const { LLMChain } = require("langchain/chains");
|
const { LLMChain } = require("langchain/chains");
|
||||||
const { ChatOpenAI } = require("langchain/chat_models/openai");
|
const { ChatOpenAI } = require("langchain/chat_models/openai");
|
||||||
const openAiTokenCount = require("openai-gpt-token-counter");
|
const openAiTokenCount = require("openai-gpt-token-counter");
|
||||||
|
|
||||||
const mrModifiedFiles = danger.git.modified_files;
|
|
||||||
const mrCommits = danger.gitlab.commits;
|
|
||||||
|
|
||||||
module.exports = async function () {
|
module.exports = async function () {
|
||||||
let mrDiff = await getMrGitDiff(mrModifiedFiles);
|
|
||||||
const mrCommitMessages = getCommitMessages(mrCommits);
|
|
||||||
|
|
||||||
// Init output message
|
|
||||||
let outputDangerMessage = `\n\nPerhaps you could use an AI-generated suggestion for your commit message. Here is one `;
|
let outputDangerMessage = `\n\nPerhaps you could use an AI-generated suggestion for your commit message. Here is one `;
|
||||||
|
|
||||||
// Setup LLM prompt
|
let mrDiff = await getMrGitDiff(danger.git.modified_files);
|
||||||
const inputPrompt = `You are a helpful assistant that creates suggestions for single git commit message, that user can use to describe all the changes in their merge request.
|
const mrCommitMessages = getCommitMessages(danger.gitlab.commits);
|
||||||
Use git diff: {mrDiff} and users current commit messages: {mrCommitMessages} to get the changes made in the commit.
|
const inputPrompt = getInputPrompt();
|
||||||
|
const inputLlmTokens = getInputLlmTokens(
|
||||||
Output should be git commit message following the conventional commit format.
|
|
||||||
|
|
||||||
Output only git commit message in desired format, without comments and other text.
|
|
||||||
|
|
||||||
Do not include lines with JIRA tickets mentions (e.g. "Closes JIRA-123") to the output.
|
|
||||||
|
|
||||||
Avoid including temporary commit messages (e.g. "Cleanup", "Merged" or "wip: Test") to the output.
|
|
||||||
|
|
||||||
Avoid using vague terms (e.g. "some checks", "add new ones", "few changes" ) in the output.
|
|
||||||
|
|
||||||
|
|
||||||
[EXAMPLE OUTPUT]
|
|
||||||
feat(scope): add support for component XXX
|
|
||||||
|
|
||||||
- adds support for wifi6
|
|
||||||
- adds validations for logging script
|
|
||||||
|
|
||||||
|
|
||||||
[EXAMPLE OUTPUT]
|
|
||||||
feat(scope): add support for component XXX
|
|
||||||
|
|
||||||
- adds support for wifi6
|
|
||||||
- adds validations for logging script
|
|
||||||
- Closes https://github.com/espressif/esp-idf/issues/1234
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Count input tokens for LLM
|
|
||||||
const mrCommitMessagesTokens = openAiTokenCount(mrCommitMessages.join(" "));
|
|
||||||
const gitDiffTokens = openAiTokenCount(mrDiff);
|
|
||||||
const promptTokens = openAiTokenCount(inputPrompt);
|
|
||||||
|
|
||||||
const inputLlmTokens =
|
|
||||||
mrCommitMessagesTokens + gitDiffTokens + promptTokens;
|
|
||||||
|
|
||||||
console.log(`Input tokens for LLM: ${inputLlmTokens}`);
|
|
||||||
|
|
||||||
if (inputLlmTokens < 4096) {
|
|
||||||
outputDangerMessage += `(based on your MR git-diff and your current commit messages):\n\n`;
|
|
||||||
} else {
|
|
||||||
outputDangerMessage += `(based only on your current commit messages, git-diff of this MR is too big (${inputLlmTokens} tokens) for the AI model):\n\n`;
|
|
||||||
mrDiff = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call LLM
|
|
||||||
const generatedCommitMessage = await createAiGitMessage(
|
|
||||||
inputPrompt,
|
inputPrompt,
|
||||||
mrDiff,
|
mrDiff,
|
||||||
mrCommitMessages
|
mrCommitMessages
|
||||||
);
|
);
|
||||||
|
console.log(`Input tokens for LLM: ${inputLlmTokens}`);
|
||||||
|
|
||||||
outputDangerMessage += "```\n" + generatedCommitMessage + "\n```\n"; // Add the generated git message, format to the markdown code block
|
if (inputLlmTokens >= gptStandardModelTokens) {
|
||||||
outputDangerMessage += "\n**NOTE: AI-generated suggestions may not always be correct, please review the suggestion before using it.**" // Add disclaimer
|
mrDiff = ""; // If the input mrDiff is larger than 16k model, don't use mrDiff, use only current commit messages
|
||||||
|
outputDangerMessage += `(based only on your current commit messages, git-diff of this MR is too big (${inputLlmTokens} tokens) for the AI models):\n\n`;
|
||||||
|
} else {
|
||||||
|
outputDangerMessage += `(based on your MR git-diff and your current commit messages):\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate AI commit message
|
||||||
|
let generatedCommitMessage = "";
|
||||||
|
try {
|
||||||
|
const rawCommitMessage = await createAiGitMessage(
|
||||||
|
inputPrompt,
|
||||||
|
mrDiff,
|
||||||
|
mrCommitMessages
|
||||||
|
);
|
||||||
|
generatedCommitMessage = postProcessCommitMessage(rawCommitMessage);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in generating AI commit message: ", error);
|
||||||
|
outputDangerMessage +=
|
||||||
|
"\nCould not generate commit message due to an error.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append closing statements ("Closes https://github.com/espressif/esp-idf/issues/XXX") to the generated commit message
|
||||||
|
let closingStatements = extractClosingStatements(mrCommitMessages);
|
||||||
|
if (closingStatements.length > 0) {
|
||||||
|
generatedCommitMessage += "\n\n" + closingStatements;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the generated git message, format to the markdown code block
|
||||||
|
outputDangerMessage += `\n\`\`\`\n${generatedCommitMessage}\n\`\`\`\n`;
|
||||||
|
outputDangerMessage +=
|
||||||
|
"\n**NOTE: AI-generated suggestions may not always be correct, please review the suggestion before using it.**"; // Add disclaimer
|
||||||
return outputDangerMessage;
|
return outputDangerMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getMrGitDiff(mrModifiedFiles) {
|
async function getMrGitDiff(mrModifiedFiles) {
|
||||||
let mrDiff = "";
|
const fileDiffs = await Promise.all(
|
||||||
for (const file of mrModifiedFiles) {
|
mrModifiedFiles.map((file) => danger.git.diffForFile(file))
|
||||||
const fileDiff = await danger.git.diffForFile(file);
|
);
|
||||||
mrDiff += fileDiff.diff.trim();
|
return fileDiffs.map((fileDiff) => fileDiff.diff.trim()).join(" ");
|
||||||
}
|
|
||||||
|
|
||||||
return mrDiff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCommitMessages(mrCommits) {
|
function getCommitMessages(mrCommits) {
|
||||||
let mrCommitMessages = [];
|
return mrCommits.map((commit) => commit.message);
|
||||||
for (const commit of mrCommits) {
|
}
|
||||||
mrCommitMessages.push(commit.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return mrCommitMessages;
|
function getInputPrompt() {
|
||||||
|
return `You are a helpful assistant that creates suggestions for single git commit message, that user can use to describe all the changes in their merge request.
|
||||||
|
Use git diff: {mrDiff} and users current commit messages: {mrCommitMessages} to get the changes made in the commit.
|
||||||
|
|
||||||
|
Output should be git commit message following the conventional commit format.
|
||||||
|
|
||||||
|
Output only git commit message in desired format, without comments and other text.
|
||||||
|
|
||||||
|
Do not include the closing statements ("Closes https://....") in the output.
|
||||||
|
|
||||||
|
Here are the strict rules you must follow:
|
||||||
|
|
||||||
|
- Avoid mentioning any JIRA tickets (e.g., "Closes JIRA-123").
|
||||||
|
- Be specific. Don't use vague terms (e.g., "some checks", "add new ones", "few changes").
|
||||||
|
- The commit message structure should be: <type><(scope/component)>: <summary>
|
||||||
|
- Types allowed: ${allowedTypes.join(", ")}
|
||||||
|
- If 'scope/component' is used, it must start with a lowercase letter.
|
||||||
|
- The 'summary' must NOT end with a period.
|
||||||
|
- The 'summary' must be between ${minimumSummaryChars} and ${maximumSummaryChars} characters long.
|
||||||
|
|
||||||
|
If a 'body' of commit message is used:
|
||||||
|
|
||||||
|
- Each line must be no longer than ${maximumBodyLineChars} characters.
|
||||||
|
- It must be separated from the 'summary' by a blank line.
|
||||||
|
|
||||||
|
Examples of correct commit messages:
|
||||||
|
|
||||||
|
- With scope and body:
|
||||||
|
fix(freertos): Fix startup timeout issue
|
||||||
|
|
||||||
|
This is a text of commit message body...
|
||||||
|
- adds support for wifi6
|
||||||
|
- adds validations for logging script
|
||||||
|
|
||||||
|
- Without scope and body:
|
||||||
|
ci: added target test job for ESP32-Wifi6`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInputLlmTokens(inputPrompt, mrDiff, mrCommitMessages) {
|
||||||
|
const mrCommitMessagesTokens = openAiTokenCount(mrCommitMessages.join(" "));
|
||||||
|
const gitDiffTokens = openAiTokenCount(mrDiff);
|
||||||
|
const promptTokens = openAiTokenCount(inputPrompt);
|
||||||
|
return mrCommitMessagesTokens + gitDiffTokens + promptTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createAiGitMessage(inputPrompt, mrDiff, mrCommitMessages) {
|
async function createAiGitMessage(inputPrompt, mrDiff, mrCommitMessages) {
|
||||||
const chat = new ChatOpenAI({
|
const chat = new ChatOpenAI({ engine: "gpt-3.5-turbo", temperature: 0 });
|
||||||
engine: "gpt-3.5-turbo",
|
|
||||||
temperature: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const chatPrompt = ChatPromptTemplate.fromPromptMessages([
|
const chatPrompt = ChatPromptTemplate.fromPromptMessages([
|
||||||
SystemMessagePromptTemplate.fromTemplate(inputPrompt),
|
SystemMessagePromptTemplate.fromTemplate(inputPrompt),
|
||||||
]);
|
]);
|
||||||
|
const chain = new LLMChain({ prompt: chatPrompt, llm: chat });
|
||||||
const chain = new LLMChain({
|
|
||||||
prompt: chatPrompt,
|
|
||||||
llm: chat,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await chain.call({
|
const response = await chain.call({
|
||||||
mrDiff: mrDiff,
|
mrDiff: mrDiff,
|
||||||
mrCommitMessages: mrCommitMessages,
|
mrCommitMessages: mrCommitMessages,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.text;
|
return response.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function postProcessCommitMessage(rawCommitMessage) {
|
||||||
|
// Split the result into lines
|
||||||
|
let lines = rawCommitMessage.split("\n");
|
||||||
|
|
||||||
|
// Format each line
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
let line = lines[i].trim();
|
||||||
|
|
||||||
|
// If the line is longer than maximumBodyLineChars, split it into multiple lines
|
||||||
|
if (line.length > maximumBodyLineChars) {
|
||||||
|
let newLines = [];
|
||||||
|
while (line.length > maximumBodyLineChars) {
|
||||||
|
let lastSpaceIndex = line.lastIndexOf(
|
||||||
|
" ",
|
||||||
|
maximumBodyLineChars
|
||||||
|
);
|
||||||
|
newLines.push(line.substring(0, lastSpaceIndex));
|
||||||
|
line = line.substring(lastSpaceIndex + 1);
|
||||||
|
}
|
||||||
|
newLines.push(line);
|
||||||
|
lines[i] = newLines.join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join the lines back into a single string with a newline between each one
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractClosingStatements(mrCommitMessages) {
|
||||||
|
let closingStatements = [];
|
||||||
|
mrCommitMessages.forEach((message) => {
|
||||||
|
const lines = message.split("\n");
|
||||||
|
lines.forEach((line) => {
|
||||||
|
if (line.startsWith("Closes")) {
|
||||||
|
closingStatements.push(line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return closingStatements.join("\n");
|
||||||
|
}
|
||||||
|
@@ -1,33 +1,27 @@
|
|||||||
|
const {
|
||||||
|
minimumSummaryChars,
|
||||||
|
maximumSummaryChars,
|
||||||
|
maximumBodyLineChars,
|
||||||
|
allowedTypes,
|
||||||
|
} = require("./mrCommitsConstants.js");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check that commit messages are based on the Espressif ESP-IDF project's internal rules for git commit messages.
|
* Check that commit messages are based on the Espressif ESP-IDF project's rules for git commit messages.
|
||||||
*
|
*
|
||||||
* @dangerjs WARN
|
* @dangerjs WARN
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = async function () {
|
module.exports = async function () {
|
||||||
const mrCommits = danger.gitlab.commits;
|
const mrCommits = danger.gitlab.commits;
|
||||||
const lint = require("@commitlint/lint").default;
|
const lint = require("@commitlint/lint").default;
|
||||||
|
|
||||||
const allowedTypes = [
|
|
||||||
"change",
|
|
||||||
"ci",
|
|
||||||
"docs",
|
|
||||||
"feat",
|
|
||||||
"fix",
|
|
||||||
"refactor",
|
|
||||||
"remove",
|
|
||||||
"revert",
|
|
||||||
];
|
|
||||||
const lintingRules = {
|
const lintingRules = {
|
||||||
// rule definition: [(0-1 = off/on), (always/never = must be/mustn't be), (value)]
|
// rule definition: [(0-1 = off/on), (always/never = must be/mustn't be), (value)]
|
||||||
"body-max-line-length": [1, "always", 100], // Max length of the body line
|
"body-max-line-length": [1, "always", maximumBodyLineChars], // Max length of the body line
|
||||||
"footer-leading-blank": [1, "always"], // Always have a blank line before the footer section
|
"footer-leading-blank": [1, "always"], // Always have a blank line before the footer section
|
||||||
"footer-max-line-length": [1, "always", 100], // Max length of the footer line
|
"footer-max-line-length": [1, "always", maximumBodyLineChars], // Max length of the footer line
|
||||||
"subject-max-length": [1, "always", 50], // Max length of the "Summary"
|
"subject-max-length": [1, "always", maximumSummaryChars], // Max length of the "Summary"
|
||||||
"subject-min-length": [1, "always", 20], // Min length of the "Summary"
|
"subject-min-length": [1, "always", minimumSummaryChars], // Min length of the "Summary"
|
||||||
"scope-case": [1, "always", "lower-case"], // "scope/component" must start with lower-case
|
"scope-case": [1, "always", "lower-case"], // "scope/component" must start with lower-case
|
||||||
// "scope-empty": [1, "never"], // "scope/component" is mandatory
|
|
||||||
"subject-case": [1, "always", ["sentence-case"]], // "Summary" must start with upper-case
|
|
||||||
"subject-full-stop": [1, "never", "."], // "Summary" must not end with a full stop (period)
|
"subject-full-stop": [1, "never", "."], // "Summary" must not end with a full stop (period)
|
||||||
"subject-empty": [1, "never"], // "Summary" is mandatory
|
"subject-empty": [1, "never"], // "Summary" is mandatory
|
||||||
"type-case": [1, "always", "lower-case"], // "type/action" must start with lower-case
|
"type-case": [1, "always", "lower-case"], // "type/action" must start with lower-case
|
||||||
@@ -36,6 +30,9 @@ module.exports = async function () {
|
|||||||
"body-leading-blank": [1, "always"], // Always have a blank line before the body section
|
"body-leading-blank": [1, "always"], // Always have a blank line before the body section
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Switcher for AI suggestions (for poor messages)
|
||||||
|
let generateAISuggestion = false;
|
||||||
|
|
||||||
// Search for the messages in each commit
|
// Search for the messages in each commit
|
||||||
let issuesAllCommitMessages = [];
|
let issuesAllCommitMessages = [];
|
||||||
|
|
||||||
@@ -90,11 +87,13 @@ module.exports = async function () {
|
|||||||
break;
|
break;
|
||||||
case "subject-empty":
|
case "subject-empty":
|
||||||
issuesSingleCommitMessage.push(`- *summary* looks empty`);
|
issuesSingleCommitMessage.push(`- *summary* looks empty`);
|
||||||
|
generateAISuggestion = true;
|
||||||
break;
|
break;
|
||||||
case "subject-min-length":
|
case "subject-min-length":
|
||||||
issuesSingleCommitMessage.push(
|
issuesSingleCommitMessage.push(
|
||||||
`- *summary* looks too short`
|
`- *summary* looks too short`
|
||||||
);
|
);
|
||||||
|
generateAISuggestion = true;
|
||||||
break;
|
break;
|
||||||
case "subject-case":
|
case "subject-case":
|
||||||
issuesSingleCommitMessage.push(
|
issuesSingleCommitMessage.push(
|
||||||
@@ -131,8 +130,9 @@ module.exports = async function () {
|
|||||||
if (issuesAllCommitMessages.length) {
|
if (issuesAllCommitMessages.length) {
|
||||||
issuesAllCommitMessages.sort();
|
issuesAllCommitMessages.sort();
|
||||||
const basicTips = [
|
const basicTips = [
|
||||||
`- correct format of commit message should be: \`<type/action>(<scope/component>): <Summary>\`, for example \`fix(esp32): Fixed startup timeout issue\``,
|
`- correct format of commit message should be: \`<type/action>(<scope/component>): <summary>\`, for example \`fix(esp32): Fixed startup timeout issue\``,
|
||||||
`- sufficiently descriptive message summary should be between 20 to 50 characters and start with upper case letter`,
|
`- allowed types are: \`${allowedTypes}\``,
|
||||||
|
`- sufficiently descriptive message summary should be between ${minimumSummaryChars} to ${maximumSummaryChars} characters and start with upper case letter`,
|
||||||
`- avoid Jira references in commit messages (unavailable/irrelevant for our customers)`,
|
`- avoid Jira references in commit messages (unavailable/irrelevant for our customers)`,
|
||||||
`- follow this [commit messages guide](${process.env.DANGER_GITLAB_HOST}/espressif/esp-idf/-/wikis/dev-proc/Commit-messages)`,
|
`- follow this [commit messages guide](${process.env.DANGER_GITLAB_HOST}/espressif/esp-idf/-/wikis/dev-proc/Commit-messages)`,
|
||||||
];
|
];
|
||||||
@@ -143,13 +143,16 @@ module.exports = async function () {
|
|||||||
\n**Please consider updating these commit messages** - here are some basic tips:\n${basicTips.join(
|
\n**Please consider updating these commit messages** - here are some basic tips:\n${basicTips.join(
|
||||||
"\n"
|
"\n"
|
||||||
)}
|
)}
|
||||||
|
\n \`TIP:\` You can install commit-msg pre-commit hook (\`pre-commit install -t pre-commit -t commit-msg\`) to run this check when committing.
|
||||||
\n***
|
\n***
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Create AI generated suggestion for git commit message based of gitDiff and current commit messages
|
if (generateAISuggestion) {
|
||||||
const AImessageSuggestion =
|
// Create AI generated suggestion for git commit message based of gitDiff and current commit messages
|
||||||
await require("./aiGenerateGitMessage.js")();
|
const AImessageSuggestion =
|
||||||
dangerMessage += AImessageSuggestion;
|
await require("./aiGenerateGitMessage.js")();
|
||||||
|
dangerMessage += AImessageSuggestion;
|
||||||
|
}
|
||||||
|
|
||||||
warn(dangerMessage);
|
warn(dangerMessage);
|
||||||
}
|
}
|
||||||
|
16
.gitlab/dangerjs/mrCommitsConstants.js
Normal file
16
.gitlab/dangerjs/mrCommitsConstants.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
gptStandardModelTokens: 4096,
|
||||||
|
minimumSummaryChars: 20,
|
||||||
|
maximumSummaryChars: 72,
|
||||||
|
maximumBodyLineChars: 100,
|
||||||
|
allowedTypes: [
|
||||||
|
"change",
|
||||||
|
"ci",
|
||||||
|
"docs",
|
||||||
|
"feat",
|
||||||
|
"fix",
|
||||||
|
"refactor",
|
||||||
|
"remove",
|
||||||
|
"revert",
|
||||||
|
],
|
||||||
|
};
|
@@ -169,7 +169,7 @@ repos:
|
|||||||
- id: check-copyright
|
- id: check-copyright
|
||||||
args: ['--ignore', 'tools/ci/check_copyright_ignore.txt', '--config', 'tools/ci/check_copyright_config.yaml']
|
args: ['--ignore', 'tools/ci/check_copyright_ignore.txt', '--config', 'tools/ci/check_copyright_config.yaml']
|
||||||
- repo: https://github.com/espressif/conventional-precommit-linter
|
- repo: https://github.com/espressif/conventional-precommit-linter
|
||||||
rev: v1.0.0
|
rev: v1.2.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: conventional-precommit-linter
|
- id: conventional-precommit-linter
|
||||||
stages: [commit-msg]
|
stages: [commit-msg]
|
||||||
|
@@ -18,7 +18,7 @@ Install pre-commit hook
|
|||||||
|
|
||||||
1. Go to the IDF Project Directory
|
1. Go to the IDF Project Directory
|
||||||
|
|
||||||
2. Run ``pre-commit install --allow-missing-config``. Install hook by this approach will let you commit successfully even in branches without the ``.pre-commit-config.yaml``
|
2. Run ``pre-commit install --allow-missing-config -t pre-commit -t commit-msg``. Install hook by this approach will let you commit successfully even in branches without the ``.pre-commit-config.yaml``
|
||||||
|
|
||||||
3. pre-commit hook will run automatically when you're running ``git commit`` command
|
3. pre-commit hook will run automatically when you're running ``git commit`` command
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user