Skip to content

Commit 92d987f

Browse files
committed
feat: support creation and update of closed issues and PRs #25
- Added a new feature that creates and updates closed issues and PRs . - Closed issues and PRs are no longer deleted during the cleanup process. - Only closed issues and PRs within the defined cleanup timer range are created or updated.
1 parent 74acbcc commit 92d987f

8 files changed

Lines changed: 237 additions & 53 deletions

File tree

src/issue-file-manager.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
processFilenameTemplate
99
} from "./util/templateUtils";
1010
import { getEffectiveRepoSettings } from "./util/settingsUtils";
11-
import { extractPersistBlocks, mergePersistBlocks, shouldUpdateContent } from "./util/persistUtils";
11+
import { extractPersistBlocks, mergePersistBlocks } from "./util/persistUtils";
12+
import { shouldUpdateContent, hasStatusChanged } from "./util/contentUtils";
1213
import { FileHelpers } from "./util/file-helpers";
1314
import { FolderPathManager } from "./folder-path-manager";
1415
import { CleanupManager } from "./cleanup-manager";
@@ -55,7 +56,7 @@ export class IssueFileManager {
5556
allIssuesIncludingRecentlyClosed,
5657
);
5758

58-
// Create or update issue files for open issues
59+
// Create or update issue files (openIssues contains filtered issues from main.ts)
5960
for (const issue of openIssues) {
6061
await this.createOrUpdateIssueFile(
6162
effectiveRepo,
@@ -118,12 +119,17 @@ export class IssueFileManager {
118119
// Use current repository updateMode setting (not the old value from file properties)
119120
const updateMode = repo.issueUpdateMode;
120121

121-
if (updateMode === "update") {
122-
// Read existing content first
123-
const existingContent = await this.app.vault.read(file);
122+
// Read existing content to check for changes
123+
const existingContent = await this.app.vault.read(file);
124124

125+
// Check if status has changed (e.g., open -> closed)
126+
const statusHasChanged = hasStatusChanged(existingContent, issue.state);
127+
128+
// If status changed, always update regardless of updateMode
129+
// Otherwise, respect the updateMode setting
130+
if (statusHasChanged || updateMode === "update") {
125131
// Check if content needs updating based on updated_at field
126-
if (!shouldUpdateContent(existingContent, issue.updated_at)) {
132+
if (!statusHasChanged && !shouldUpdateContent(existingContent, issue.updated_at)) {
127133
this.noticeManager.debug(
128134
`Skipped update for issue ${issue.number}: no changes detected (updated_at match)`
129135
);
@@ -150,7 +156,11 @@ export class IssueFileManager {
150156
}
151157

152158
await this.app.vault.modify(file, updatedContent);
153-
this.noticeManager.debug(`Updated issue ${issue.number}`);
159+
if (statusHasChanged) {
160+
this.noticeManager.debug(`Updated issue ${issue.number} (status changed to ${issue.state})`);
161+
} else {
162+
this.noticeManager.debug(`Updated issue ${issue.number}`);
163+
}
154164
} else if (updateMode === "append") {
155165
content = `---\n### New status: "${
156166
issue.state

src/main.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,14 @@ export default class GitHubTrackerPlugin extends Plugin {
9494
(issue: { state: string }) => issue.state === "open",
9595
);
9696

97+
// Decide which issues to filter based on settings
98+
const issuesToFilter = repo.includeClosedIssues
99+
? allIssuesIncludingRecentlyClosed
100+
: openIssues;
101+
97102
const filteredIssues = this.fileManager.filterIssues(
98103
repo,
99-
openIssues,
104+
issuesToFilter,
100105
);
101106

102107
this.noticeManager.debug(
@@ -134,9 +139,14 @@ export default class GitHubTrackerPlugin extends Plugin {
134139
(pr: { state: string }) => pr.state === "open",
135140
);
136141

142+
// Decide which pull requests to filter based on settings
143+
const pullRequestsToFilter = repo.includeClosedPullRequests
144+
? allPullRequestsIncludingRecentlyClosed
145+
: openPullRequests;
146+
137147
const filteredPRs = this.fileManager.filterPullRequests(
138148
repo,
139-
openPullRequests,
149+
pullRequestsToFilter,
140150
);
141151

142152
this.noticeManager.debug(
@@ -352,9 +362,14 @@ export default class GitHubTrackerPlugin extends Plugin {
352362
(issue: { state: string }) => issue.state === "open",
353363
);
354364

365+
// Decide which issues to filter based on settings
366+
const issuesToFilter = repo.includeClosedIssues
367+
? allIssuesIncludingRecentlyClosed
368+
: openIssues;
369+
355370
const filteredIssues = this.fileManager.filterIssues(
356371
repo,
357-
openIssues,
372+
issuesToFilter,
358373
);
359374

360375
this.noticeManager.debug(
@@ -425,9 +440,14 @@ export default class GitHubTrackerPlugin extends Plugin {
425440
(pr: { state: string }) => pr.state === "open",
426441
);
427442

443+
// Decide which pull requests to filter based on settings
444+
const pullRequestsToFilter = repo.includeClosedPullRequests
445+
? allPullRequestsIncludingRecentlyClosed
446+
: openPullRequests;
447+
428448
const filteredPRs = this.fileManager.filterPullRequests(
429449
repo,
430-
openPullRequests,
450+
pullRequestsToFilter,
431451
);
432452

433453
this.noticeManager.debug(

src/pr-file-manager.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
processFilenameTemplate
99
} from "./util/templateUtils";
1010
import { getEffectiveRepoSettings } from "./util/settingsUtils";
11-
import { extractPersistBlocks, mergePersistBlocks, shouldUpdateContent } from "./util/persistUtils";
11+
import { extractPersistBlocks, mergePersistBlocks } from "./util/persistUtils";
12+
import { shouldUpdateContent, hasStatusChanged } from "./util/contentUtils";
1213
import { FileHelpers } from "./util/file-helpers";
1314
import { FolderPathManager } from "./folder-path-manager";
1415
import { CleanupManager } from "./cleanup-manager";
@@ -57,6 +58,7 @@ export class PullRequestFileManager {
5758
allPullRequestsIncludingRecentlyClosed,
5859
);
5960

61+
// Create or update pull request files (openPullRequests contains filtered PRs from main.ts)
6062
for (const pr of openPullRequests) {
6163
await this.createOrUpdatePullRequestFile(
6264
effectiveRepo,
@@ -119,12 +121,17 @@ export class PullRequestFileManager {
119121
// Use current repository updateMode setting (not the old value from file properties)
120122
const updateMode = repo.pullRequestUpdateMode;
121123

122-
if (updateMode === "update") {
123-
// Read existing content first
124-
const existingContent = await this.app.vault.read(file);
124+
// Read existing content to check for changes
125+
const existingContent = await this.app.vault.read(file);
125126

127+
// Check if status has changed (e.g., open -> closed)
128+
const statusHasChanged = hasStatusChanged(existingContent, pr.state);
129+
130+
// If status changed, always update regardless of updateMode
131+
// Otherwise, respect the updateMode setting
132+
if (statusHasChanged || updateMode === "update") {
126133
// Check if content needs updating based on updated_at field
127-
if (!shouldUpdateContent(existingContent, pr.updated_at)) {
134+
if (!statusHasChanged && !shouldUpdateContent(existingContent, pr.updated_at)) {
128135
this.noticeManager.debug(
129136
`Skipped update for PR ${pr.number}: no changes detected (updated_at match)`
130137
);
@@ -151,7 +158,11 @@ export class PullRequestFileManager {
151158
}
152159

153160
await this.app.vault.modify(file, updatedContent);
154-
this.noticeManager.debug(`Updated PR ${pr.number}`);
161+
if (statusHasChanged) {
162+
this.noticeManager.debug(`Updated PR ${pr.number} (status changed to ${pr.state})`);
163+
} else {
164+
this.noticeManager.debug(`Updated PR ${pr.number}`);
165+
}
155166
} else if (updateMode === "append") {
156167
content = `---\n### New status: "${
157168
pr.state

src/settings/repository-renderer.ts

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -169,19 +169,23 @@ export class RepositoryRenderer {
169169
// Assignee filtering settings
170170
this.renderAssigneeFilter(issuesSettingsContainer, repo, 'issue');
171171

172+
// Store reference to allow issue deletion toggle for updating
173+
let allowDeleteIssueToggle: any;
172174
new Setting(issuesSettingsContainer)
173175
.setName("Default: Allow issue deletion")
174176
.setDesc(
175177
"If enabled, issue files will be set to be deleted from your vault when the issue is closed or no longer matches your filter criteria",
176178
)
177-
.addToggle((toggle) =>
178-
toggle
179+
.addToggle((toggle) => {
180+
allowDeleteIssueToggle = toggle;
181+
return toggle
179182
.setValue(repo.allowDeleteIssue)
183+
.setDisabled(repo.includeClosedIssues)
180184
.onChange(async (value) => {
181-
repo.allowDeleteIssue = value;
182-
await this.plugin.saveSettings();
183-
}),
184-
);
185+
repo.allowDeleteIssue = value;
186+
await this.plugin.saveSettings();
187+
});
188+
});
185189

186190
new Setting(issuesSettingsContainer)
187191
.setName("Issue note template")
@@ -264,6 +268,32 @@ export class RepositoryRenderer {
264268
await this.plugin.saveSettings();
265269
}),
266270
);
271+
272+
new Setting(issuesSettingsContainer)
273+
.setName("Include closed issues")
274+
.setDesc(
275+
"If enabled, closed issues will also be created and updated (not just deleted after the cleanup period).",
276+
)
277+
.addToggle((toggle) =>
278+
toggle
279+
.setValue(repo.includeClosedIssues)
280+
.onChange(async (value) => {
281+
repo.includeClosedIssues = value;
282+
// If including closed issues, disable deletion to prevent conflicts
283+
if (value) {
284+
repo.allowDeleteIssue = false;
285+
// Update the allow deletion toggle directly
286+
if (allowDeleteIssueToggle) {
287+
allowDeleteIssueToggle.setValue(false);
288+
}
289+
}
290+
// Update the disabled state of the allow deletion toggle
291+
if (allowDeleteIssueToggle) {
292+
allowDeleteIssueToggle.setDisabled(value);
293+
}
294+
await this.plugin.saveSettings();
295+
}),
296+
);
267297
}
268298

269299
renderPullRequestSettings(
@@ -486,16 +516,60 @@ export class RepositoryRenderer {
486516
});
487517
});
488518

519+
// Store reference to allow PR deletion toggle for updating
520+
let allowDeletePRToggle: any;
489521
new Setting(pullRequestsSettingsContainer)
490522
.setName("Default: Allow pull request deletion")
491523
.setDesc(
492-
"If enabled, pull request files will be set to be deleted from your vault when the pull request is closed or no longer matches your filter criteria",
524+
"If enabled, pull request files will be set to be deleted from your vault when the pull request is closed or no longer matches your filter criteria. Automatically disabled when 'Include closed pull requests' is enabled.",
525+
)
526+
.addToggle((toggle) => {
527+
allowDeletePRToggle = toggle;
528+
return toggle
529+
.setValue(repo.allowDeletePullRequest)
530+
.setDisabled(repo.includeClosedPullRequests)
531+
.onChange(async (value) => {
532+
repo.allowDeletePullRequest = value;
533+
await this.plugin.saveSettings();
534+
});
535+
});
536+
537+
new Setting(pullRequestsSettingsContainer)
538+
.setName("Include pull request comments")
539+
.setDesc(
540+
"If enabled, comments from pull requests will be included in the generated files",
493541
)
494542
.addToggle((toggle) =>
495543
toggle
496-
.setValue(repo.allowDeletePullRequest)
544+
.setValue(repo.includePullRequestComments)
545+
.onChange(async (value) => {
546+
repo.includePullRequestComments = value;
547+
await this.plugin.saveSettings();
548+
}),
549+
);
550+
551+
new Setting(pullRequestsSettingsContainer)
552+
.setName("Include closed pull requests")
553+
.setDesc(
554+
"If enabled, closed pull requests will also be created and updated (not just deleted after the cleanup period). This is useful for building a knowledge base.",
555+
)
556+
.addToggle((toggle) =>
557+
toggle
558+
.setValue(repo.includeClosedPullRequests)
497559
.onChange(async (value) => {
498-
repo.allowDeletePullRequest = value;
560+
repo.includeClosedPullRequests = value;
561+
// If including closed PRs, disable deletion to prevent conflicts
562+
if (value) {
563+
repo.allowDeletePullRequest = false;
564+
// Update the allow deletion toggle directly
565+
if (allowDeletePRToggle) {
566+
allowDeletePRToggle.setValue(false);
567+
}
568+
}
569+
// Update the disabled state of the allow deletion toggle
570+
if (allowDeletePRToggle) {
571+
allowDeletePRToggle.setDisabled(value);
572+
}
499573
await this.plugin.saveSettings();
500574
}),
501575
);

src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export interface RepositoryTracking {
3333
prAssigneeFilters: string[];
3434
includeIssueComments: boolean;
3535
includePullRequestComments: boolean;
36+
includeClosedIssues: boolean;
37+
includeClosedPullRequests: boolean;
3638
}
3739

3840
export interface GlobalDefaults {
@@ -48,6 +50,8 @@ export interface GlobalDefaults {
4850
pullRequestNoteTemplate: string;
4951
pullRequestContentTemplate: string;
5052
includePullRequestComments: boolean;
53+
includeClosedIssues: boolean;
54+
includeClosedPullRequests: boolean;
5155
}
5256

5357
export interface GitHubTrackerSettings {
@@ -77,6 +81,8 @@ export const DEFAULT_GLOBAL_DEFAULTS: GlobalDefaults = {
7781
pullRequestNoteTemplate: "PR - {number}",
7882
pullRequestContentTemplate: "",
7983
includePullRequestComments: true,
84+
includeClosedIssues: false,
85+
includeClosedPullRequests: false,
8086
};
8187

8288
export const DEFAULT_SETTINGS: GitHubTrackerSettings = {
@@ -129,4 +135,6 @@ export const DEFAULT_REPOSITORY_TRACKING: RepositoryTracking = {
129135
prAssigneeFilters: [],
130136
includeIssueComments: true,
131137
includePullRequestComments: true,
138+
includeClosedIssues: false,
139+
includeClosedPullRequests: false,
132140
};

src/util/contentUtils.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Utility functions for comparing and validating content
3+
*/
4+
5+
/**
6+
* Check if content has changed based on updated_at field
7+
* @param existingContent The existing file content
8+
* @param githubUpdatedAt The updated_at timestamp from GitHub API
9+
* @returns true if content should be updated
10+
*/
11+
export function shouldUpdateContent(
12+
existingContent: string,
13+
githubUpdatedAt: string
14+
): boolean {
15+
// Extract updated field from frontmatter
16+
const updatedMatch = existingContent.match(/^updated:\s*["']?([^"'\n]+)["']?$/m);
17+
18+
if (!updatedMatch) {
19+
// No updated field found, should update
20+
return true;
21+
}
22+
23+
const existingUpdated = updatedMatch[1];
24+
const githubUpdated = new Date(githubUpdatedAt).toISOString();
25+
const existingUpdatedDate = new Date(existingUpdated);
26+
const githubUpdatedDate = new Date(githubUpdated);
27+
28+
// Compare dates - update if GitHub version is newer
29+
return githubUpdatedDate > existingUpdatedDate;
30+
}
31+
32+
/**
33+
* Check if status has changed (e.g., open -> closed or closed -> open)
34+
* @param existingContent The existing file content
35+
* @param githubStatus The current status from GitHub API
36+
* @returns true if status has changed
37+
*/
38+
export function hasStatusChanged(
39+
existingContent: string,
40+
githubStatus: string
41+
): boolean {
42+
// Extract status field from frontmatter
43+
const statusMatch = existingContent.match(/^status:\s*["']?([^"'\n]+)["']?$/m);
44+
45+
if (!statusMatch) {
46+
// No status field found, should update
47+
return true;
48+
}
49+
50+
const existingStatus = statusMatch[1];
51+
52+
// Status has changed if they don't match
53+
return existingStatus !== githubStatus;
54+
}

0 commit comments

Comments
 (0)