Compare commits
9 Commits
main
...
issue-596/
Author | SHA1 | Date | |
---|---|---|---|
![]() |
543406b451 | ||
![]() |
c9c0453c5e | ||
![]() |
c750aa3ff8 | ||
![]() |
e200968421 | ||
![]() |
8d5a27d5ec | ||
![]() |
867763412b | ||
![]() |
13f3458051 | ||
![]() |
e878daaea6 | ||
![]() |
a1c9a63a3d |
@ -82,6 +82,7 @@ Every argument is optional.
|
|||||||
| [ignore-updates](#ignore-updates) | Any update (update/comment) can reset the stale idle time on the issues/PRs | `false` |
|
| [ignore-updates](#ignore-updates) | Any update (update/comment) can reset the stale idle time on the issues/PRs | `false` |
|
||||||
| [ignore-issue-updates](#ignore-issue-updates) | Override [ignore-updates](#ignore-updates) for issues only | |
|
| [ignore-issue-updates](#ignore-issue-updates) | Override [ignore-updates](#ignore-updates) for issues only | |
|
||||||
| [ignore-pr-updates](#ignore-pr-updates) | Override [ignore-updates](#ignore-updates) for PRs only | |
|
| [ignore-pr-updates](#ignore-pr-updates) | Override [ignore-updates](#ignore-updates) for PRs only | |
|
||||||
|
| [include-only-assigned](#include-only-assigned) | Process only assigned issues | `false` |
|
||||||
|
|
||||||
### List of output options
|
### List of output options
|
||||||
|
|
||||||
@ -517,6 +518,12 @@ Useful to override [ignore-updates](#ignore-updates) but only to ignore the upda
|
|||||||
|
|
||||||
Default value: unset
|
Default value: unset
|
||||||
|
|
||||||
|
#### include-only-assigned
|
||||||
|
|
||||||
|
If set to `true`, only the issues or the pull requests with an assignee will be marked as stale automatically.
|
||||||
|
|
||||||
|
Default value: `false`
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
See also [action.yml](./action.yml) for a comprehensive list of all the options.
|
See also [action.yml](./action.yml) for a comprehensive list of all the options.
|
||||||
|
@ -51,5 +51,6 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({
|
|||||||
ignoreIssueUpdates: undefined,
|
ignoreIssueUpdates: undefined,
|
||||||
ignorePrUpdates: undefined,
|
ignorePrUpdates: undefined,
|
||||||
exemptDraftPr: false,
|
exemptDraftPr: false,
|
||||||
closeIssueReason: ''
|
closeIssueReason: '',
|
||||||
|
includeOnlyAssigned: false
|
||||||
});
|
});
|
||||||
|
@ -2352,3 +2352,69 @@ test('processing a pull request to be stale with the "stalePrMessage" option set
|
|||||||
expect(processor.closedIssues).toHaveLength(0);
|
expect(processor.closedIssues).toHaveLength(0);
|
||||||
expect(processor.statistics?.addedPullRequestsCommentsCount).toStrictEqual(0);
|
expect(processor.statistics?.addedPullRequestsCommentsCount).toStrictEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('processing an issue with the "includeOnlyAssigned" option and nonempty assignee list will stale the issue', async () => {
|
||||||
|
const issueDate = new Date();
|
||||||
|
issueDate.setDate(issueDate.getDate() - 2);
|
||||||
|
|
||||||
|
const opts: IIssuesProcessorOptions = {
|
||||||
|
...DefaultProcessorOptions,
|
||||||
|
staleIssueLabel: 'This issue is stale',
|
||||||
|
includeOnlyAssigned: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const TestIssueList: Issue[] = [
|
||||||
|
generateIssue(
|
||||||
|
opts,
|
||||||
|
1,
|
||||||
|
'An issue with no label',
|
||||||
|
issueDate.toDateString(),
|
||||||
|
issueDate.toDateString(),
|
||||||
|
false,
|
||||||
|
[],
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
['assignee1']
|
||||||
|
)
|
||||||
|
];
|
||||||
|
const processor = new IssuesProcessorMock(
|
||||||
|
opts,
|
||||||
|
async p => (p === 1 ? TestIssueList : []),
|
||||||
|
async () => [],
|
||||||
|
async () => new Date().toDateString()
|
||||||
|
);
|
||||||
|
|
||||||
|
// process our fake issue list
|
||||||
|
await processor.processIssues(1);
|
||||||
|
|
||||||
|
expect(processor.staleIssues).toHaveLength(1);
|
||||||
|
expect(processor.closedIssues).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('processing an issue with the "includeOnlyAssigned" option set and no assignees will not stale the issue', async () => {
|
||||||
|
const issueDate = new Date();
|
||||||
|
issueDate.setDate(issueDate.getDate() - 2);
|
||||||
|
|
||||||
|
const opts: IIssuesProcessorOptions = {
|
||||||
|
...DefaultProcessorOptions,
|
||||||
|
staleIssueLabel: 'This issue is stale',
|
||||||
|
includeOnlyAssigned: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const TestIssueList: Issue[] = [
|
||||||
|
generateIssue(opts, 1, 'An issue with no label', issueDate.toDateString())
|
||||||
|
];
|
||||||
|
const processor = new IssuesProcessorMock(
|
||||||
|
opts,
|
||||||
|
async p => (p === 1 ? TestIssueList : []),
|
||||||
|
async () => [],
|
||||||
|
async () => new Date().toDateString()
|
||||||
|
);
|
||||||
|
|
||||||
|
// process our fake issue list
|
||||||
|
await processor.processIssues(1);
|
||||||
|
|
||||||
|
expect(processor.staleIssues).toHaveLength(0);
|
||||||
|
expect(processor.closedIssues).toHaveLength(0);
|
||||||
|
});
|
||||||
|
@ -196,6 +196,10 @@ inputs:
|
|||||||
description: 'Any update (update/comment) can reset the stale idle time on the pull requests. Override "ignore-updates" option regarding only the pull requests.'
|
description: 'Any update (update/comment) can reset the stale idle time on the pull requests. Override "ignore-updates" option regarding only the pull requests.'
|
||||||
default: ''
|
default: ''
|
||||||
required: false
|
required: false
|
||||||
|
include-only-assigned:
|
||||||
|
description: 'Only the issues or the pull requests with an assignee will be marked as stale automatically.'
|
||||||
|
default: 'false'
|
||||||
|
required: false
|
||||||
outputs:
|
outputs:
|
||||||
closed-issues-prs:
|
closed-issues-prs:
|
||||||
description: 'List of all closed issues and pull requests.'
|
description: 'List of all closed issues and pull requests.'
|
||||||
|
32
dist/index.js
vendored
32
dist/index.js
vendored
@ -476,6 +476,11 @@ class IssuesProcessor {
|
|||||||
IssuesProcessor._endIssueProcessing(issue);
|
IssuesProcessor._endIssueProcessing(issue);
|
||||||
return; // Don't process locked issues
|
return; // Don't process locked issues
|
||||||
}
|
}
|
||||||
|
if (this._isIncludeOnlyAssigned(issue)) {
|
||||||
|
issueLogger.info(`Skipping this $$type because it's assignees list is empty`);
|
||||||
|
IssuesProcessor._endIssueProcessing(issue);
|
||||||
|
return; // If the issue has an 'includeOnlyAssigned' option, process only issues with nonempty assignees list
|
||||||
|
}
|
||||||
const onlyLabels = words_to_list_1.wordsToList(this._getOnlyLabels(issue));
|
const onlyLabels = words_to_list_1.wordsToList(this._getOnlyLabels(issue));
|
||||||
if (onlyLabels.length > 0) {
|
if (onlyLabels.length > 0) {
|
||||||
issueLogger.info(`The option ${issueLogger.createOptionLink(option_1.Option.OnlyLabels)} was specified to only process issues and pull requests with all those labels (${logger_service_1.LoggerService.cyan(onlyLabels.length)})`);
|
issueLogger.info(`The option ${issueLogger.createOptionLink(option_1.Option.OnlyLabels)} was specified to only process issues and pull requests with all those labels (${logger_service_1.LoggerService.cyan(onlyLabels.length)})`);
|
||||||
@ -737,7 +742,9 @@ class IssuesProcessor {
|
|||||||
if (issue.markedStaleThisRun) {
|
if (issue.markedStaleThisRun) {
|
||||||
issueLogger.info(`marked stale this run, so don't check for updates`);
|
issueLogger.info(`marked stale this run, so don't check for updates`);
|
||||||
}
|
}
|
||||||
const issueHasUpdateSinceStale = new Date(issue.updated_at) > new Date(markedStaleOn);
|
// The issue.updated_at and markedStaleOn are not always exactly in sync (they can be off by a second or 2)
|
||||||
|
// isDateMoreRecentThan makes sure they are not the same date within a certain tolerance (15 seconds in this case)
|
||||||
|
const issueHasUpdateSinceStale = is_date_more_recent_than_1.isDateMoreRecentThan(new Date(issue.updated_at), new Date(markedStaleOn), 15);
|
||||||
issueLogger.info(`$$type has been updated since it was marked stale: ${logger_service_1.LoggerService.cyan(issueHasUpdateSinceStale)}`);
|
issueLogger.info(`$$type has been updated since it was marked stale: ${logger_service_1.LoggerService.cyan(issueHasUpdateSinceStale)}`);
|
||||||
// Should we un-stale this issue?
|
// Should we un-stale this issue?
|
||||||
if (shouldRemoveStaleWhenUpdated &&
|
if (shouldRemoveStaleWhenUpdated &&
|
||||||
@ -988,6 +995,9 @@ class IssuesProcessor {
|
|||||||
}
|
}
|
||||||
return this.options.onlyLabels;
|
return this.options.onlyLabels;
|
||||||
}
|
}
|
||||||
|
_isIncludeOnlyAssigned(issue) {
|
||||||
|
return this.options.includeOnlyAssigned && !issue.hasAssignees;
|
||||||
|
}
|
||||||
_getAnyOfLabels(issue) {
|
_getAnyOfLabels(issue) {
|
||||||
if (issue.isPullRequest) {
|
if (issue.isPullRequest) {
|
||||||
if (this.options.anyOfPrLabels !== '') {
|
if (this.options.anyOfPrLabels !== '') {
|
||||||
@ -1957,12 +1967,25 @@ exports.getHumanizedDate = getHumanizedDate;
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
/// returns false if the dates are equal within the `equalityToleranceInSeconds` number of seconds
|
||||||
|
/// otherwise returns true if `comparedDate` is after `date`
|
||||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||||
exports.isDateMoreRecentThan = void 0;
|
exports.isDateEqualTo = exports.isDateMoreRecentThan = void 0;
|
||||||
function isDateMoreRecentThan(date, comparedDate) {
|
function isDateMoreRecentThan(date, comparedDate, equalityToleranceInSeconds = 0) {
|
||||||
|
if (equalityToleranceInSeconds > 0) {
|
||||||
|
const areDatesEqual = isDateEqualTo(date, comparedDate, equalityToleranceInSeconds);
|
||||||
|
return !areDatesEqual && date > comparedDate;
|
||||||
|
}
|
||||||
return date > comparedDate;
|
return date > comparedDate;
|
||||||
}
|
}
|
||||||
exports.isDateMoreRecentThan = isDateMoreRecentThan;
|
exports.isDateMoreRecentThan = isDateMoreRecentThan;
|
||||||
|
function isDateEqualTo(date, otherDate, toleranceInSeconds) {
|
||||||
|
const timestamp = date.getTime();
|
||||||
|
const otherTimestamp = otherDate.getTime();
|
||||||
|
const deltaInSeconds = Math.abs(timestamp - otherTimestamp) / 1000;
|
||||||
|
return deltaInSeconds <= toleranceInSeconds;
|
||||||
|
}
|
||||||
|
exports.isDateEqualTo = isDateEqualTo;
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
@ -2207,7 +2230,8 @@ function _getAndValidateArgs() {
|
|||||||
ignoreIssueUpdates: _toOptionalBoolean('ignore-issue-updates'),
|
ignoreIssueUpdates: _toOptionalBoolean('ignore-issue-updates'),
|
||||||
ignorePrUpdates: _toOptionalBoolean('ignore-pr-updates'),
|
ignorePrUpdates: _toOptionalBoolean('ignore-pr-updates'),
|
||||||
exemptDraftPr: core.getInput('exempt-draft-pr') === 'true',
|
exemptDraftPr: core.getInput('exempt-draft-pr') === 'true',
|
||||||
closeIssueReason: core.getInput('close-issue-reason')
|
closeIssueReason: core.getInput('close-issue-reason'),
|
||||||
|
includeOnlyAssigned: core.getInput('include-only-assigned') === 'true'
|
||||||
};
|
};
|
||||||
for (const numberInput of [
|
for (const numberInput of [
|
||||||
'days-before-stale',
|
'days-before-stale',
|
||||||
|
@ -62,7 +62,8 @@ describe('Issue', (): void => {
|
|||||||
ignoreIssueUpdates: undefined,
|
ignoreIssueUpdates: undefined,
|
||||||
ignorePrUpdates: undefined,
|
ignorePrUpdates: undefined,
|
||||||
exemptDraftPr: false,
|
exemptDraftPr: false,
|
||||||
closeIssueReason: ''
|
closeIssueReason: '',
|
||||||
|
includeOnlyAssigned: false
|
||||||
};
|
};
|
||||||
issueInterface = {
|
issueInterface = {
|
||||||
title: 'dummy-title',
|
title: 'dummy-title',
|
||||||
|
@ -221,6 +221,14 @@ export class IssuesProcessor {
|
|||||||
return; // Don't process locked issues
|
return; // Don't process locked issues
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._isIncludeOnlyAssigned(issue)) {
|
||||||
|
issueLogger.info(
|
||||||
|
`Skipping this $$type because it's assignees list is empty`
|
||||||
|
);
|
||||||
|
IssuesProcessor._endIssueProcessing(issue);
|
||||||
|
return; // If the issue has an 'include-only-assigned' option, process only issues with nonempty assignees list
|
||||||
|
}
|
||||||
|
|
||||||
const onlyLabels: string[] = wordsToList(this._getOnlyLabels(issue));
|
const onlyLabels: string[] = wordsToList(this._getOnlyLabels(issue));
|
||||||
|
|
||||||
if (onlyLabels.length > 0) {
|
if (onlyLabels.length > 0) {
|
||||||
@ -665,8 +673,13 @@ export class IssuesProcessor {
|
|||||||
issueLogger.info(`marked stale this run, so don't check for updates`);
|
issueLogger.info(`marked stale this run, so don't check for updates`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const issueHasUpdateSinceStale =
|
// The issue.updated_at and markedStaleOn are not always exactly in sync (they can be off by a second or 2)
|
||||||
new Date(issue.updated_at) > new Date(markedStaleOn);
|
// isDateMoreRecentThan makes sure they are not the same date within a certain tolerance (15 seconds in this case)
|
||||||
|
const issueHasUpdateSinceStale = isDateMoreRecentThan(
|
||||||
|
new Date(issue.updated_at),
|
||||||
|
new Date(markedStaleOn),
|
||||||
|
15
|
||||||
|
);
|
||||||
|
|
||||||
issueLogger.info(
|
issueLogger.info(
|
||||||
`$$type has been updated since it was marked stale: ${LoggerService.cyan(
|
`$$type has been updated since it was marked stale: ${LoggerService.cyan(
|
||||||
@ -1012,6 +1025,10 @@ export class IssuesProcessor {
|
|||||||
return this.options.onlyLabels;
|
return this.options.onlyLabels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _isIncludeOnlyAssigned(issue: Issue): boolean {
|
||||||
|
return this.options.includeOnlyAssigned && !issue.hasAssignees;
|
||||||
|
}
|
||||||
|
|
||||||
private _getAnyOfLabels(issue: Issue): string {
|
private _getAnyOfLabels(issue: Issue): string {
|
||||||
if (issue.isPullRequest) {
|
if (issue.isPullRequest) {
|
||||||
if (this.options.anyOfPrLabels !== '') {
|
if (this.options.anyOfPrLabels !== '') {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {isDateMoreRecentThan} from './is-date-more-recent-than';
|
import {isDateEqualTo, isDateMoreRecentThan} from './is-date-more-recent-than';
|
||||||
|
|
||||||
describe('isDateMoreRecentThan()', (): void => {
|
describe('isDateMoreRecentThan()', (): void => {
|
||||||
let date: Date;
|
let date: Date;
|
||||||
@ -48,4 +48,68 @@ describe('isDateMoreRecentThan()', (): void => {
|
|||||||
expect(result).toStrictEqual(true);
|
expect(result).toStrictEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('date equality', (): void => {
|
||||||
|
it('should correctly compare a before date outside tolerance', (): void => {
|
||||||
|
const aDate = new Date('2022-09-09T13:00:00');
|
||||||
|
const otherDate = new Date('2022-09-09T14:00:00');
|
||||||
|
expect(isDateEqualTo(aDate, otherDate, 60)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly compare a before date inside tolerance', (): void => {
|
||||||
|
const aDate = new Date('2022-09-09T13:00:00');
|
||||||
|
const otherDate = new Date('2022-09-09T13:00:42');
|
||||||
|
expect(isDateEqualTo(aDate, otherDate, 60)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly compare an after date outside tolerance', (): void => {
|
||||||
|
const aDate = new Date('2022-09-09T13:00:00');
|
||||||
|
const otherDate = new Date('2022-09-09T12:00:00');
|
||||||
|
expect(isDateEqualTo(aDate, otherDate, 60)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly compare an after date inside tolerance', (): void => {
|
||||||
|
const aDate = new Date('2022-09-09T13:00:00');
|
||||||
|
const otherDate = new Date('2022-09-09T12:59:42');
|
||||||
|
expect(isDateEqualTo(aDate, otherDate, 60)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly compare an exactly equal date', (): void => {
|
||||||
|
const aDate = new Date('2022-09-09T13:00:00');
|
||||||
|
const otherDate = new Date('2022-09-09T13:00:00');
|
||||||
|
expect(isDateEqualTo(aDate, otherDate, 60)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('date comparison with tolerances', (): void => {
|
||||||
|
it('should correctly compare a before date outside tolerance', (): void => {
|
||||||
|
const aDate = new Date('2022-09-09T13:00:00');
|
||||||
|
const otherDate = new Date('2022-09-09T14:00:00');
|
||||||
|
expect(isDateMoreRecentThan(aDate, otherDate)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly compare a before date inside tolerance', (): void => {
|
||||||
|
const aDate = new Date('2022-09-09T13:00:00');
|
||||||
|
const otherDate = new Date('2022-09-09T13:00:42');
|
||||||
|
expect(isDateMoreRecentThan(aDate, otherDate, 60)).toBe(false); // considered equal here
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly compare an after date outside tolerance', (): void => {
|
||||||
|
const aDate = new Date('2022-09-09T13:00:00');
|
||||||
|
const otherDate = new Date('2022-09-09T12:00:00');
|
||||||
|
expect(isDateMoreRecentThan(aDate, otherDate, 60)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly compare an after date inside tolerance', (): void => {
|
||||||
|
const aDate = new Date('2022-09-09T13:00:00');
|
||||||
|
const otherDate = new Date('2022-09-09T12:59:42');
|
||||||
|
expect(isDateMoreRecentThan(aDate, otherDate, 60)).toBe(false); // considered equal here
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly compare an exactly equal date', (): void => {
|
||||||
|
const aDate = new Date('2022-09-09T13:00:00');
|
||||||
|
const otherDate = new Date('2022-09-09T13:00:00');
|
||||||
|
expect(isDateMoreRecentThan(aDate, otherDate, 60)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,31 @@
|
|||||||
|
/// returns false if the dates are equal within the `equalityToleranceInSeconds` number of seconds
|
||||||
|
/// otherwise returns true if `comparedDate` is after `date`
|
||||||
|
|
||||||
export function isDateMoreRecentThan(
|
export function isDateMoreRecentThan(
|
||||||
date: Readonly<Date>,
|
date: Readonly<Date>,
|
||||||
comparedDate: Readonly<Date>
|
comparedDate: Readonly<Date>,
|
||||||
|
equalityToleranceInSeconds = 0
|
||||||
): boolean {
|
): boolean {
|
||||||
|
if (equalityToleranceInSeconds > 0) {
|
||||||
|
const areDatesEqual = isDateEqualTo(
|
||||||
|
date,
|
||||||
|
comparedDate,
|
||||||
|
equalityToleranceInSeconds
|
||||||
|
);
|
||||||
|
|
||||||
|
return !areDatesEqual && date > comparedDate;
|
||||||
|
}
|
||||||
|
|
||||||
return date > comparedDate;
|
return date > comparedDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isDateEqualTo(
|
||||||
|
date: Date,
|
||||||
|
otherDate: Date,
|
||||||
|
toleranceInSeconds: number
|
||||||
|
): boolean {
|
||||||
|
const timestamp = date.getTime();
|
||||||
|
const otherTimestamp = otherDate.getTime();
|
||||||
|
const deltaInSeconds = Math.abs(timestamp - otherTimestamp) / 1000;
|
||||||
|
return deltaInSeconds <= toleranceInSeconds;
|
||||||
|
}
|
||||||
|
@ -52,4 +52,5 @@ export interface IIssuesProcessorOptions {
|
|||||||
ignorePrUpdates: boolean | undefined;
|
ignorePrUpdates: boolean | undefined;
|
||||||
exemptDraftPr: boolean;
|
exemptDraftPr: boolean;
|
||||||
closeIssueReason: string;
|
closeIssueReason: string;
|
||||||
|
includeOnlyAssigned: boolean;
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,8 @@ function _getAndValidateArgs(): IIssuesProcessorOptions {
|
|||||||
ignoreIssueUpdates: _toOptionalBoolean('ignore-issue-updates'),
|
ignoreIssueUpdates: _toOptionalBoolean('ignore-issue-updates'),
|
||||||
ignorePrUpdates: _toOptionalBoolean('ignore-pr-updates'),
|
ignorePrUpdates: _toOptionalBoolean('ignore-pr-updates'),
|
||||||
exemptDraftPr: core.getInput('exempt-draft-pr') === 'true',
|
exemptDraftPr: core.getInput('exempt-draft-pr') === 'true',
|
||||||
closeIssueReason: core.getInput('close-issue-reason')
|
closeIssueReason: core.getInput('close-issue-reason'),
|
||||||
|
includeOnlyAssigned: core.getInput('include-only-assigned') === 'true'
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const numberInput of [
|
for (const numberInput of [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user