mirror of
https://github.com/flyingnobita/obsidian-github-stars.git
synced 2026-07-22 05:42:52 +00:00
feat: release 1.5.0 vault-wide star maintenance
Release 1.5.0 with vault-wide GitHub star maintenance commands, CI enforcement, dependency fixes, and updated release metadata.
This commit is contained in:
parent
4bbe427722
commit
b0d434cc5c
11 changed files with 2762 additions and 14 deletions
37
.github/workflows/ci.yml
vendored
Normal file
37
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 'codex/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm test
|
||||
|
||||
- name: Build plugin
|
||||
run: pnpm build
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -1,5 +1,18 @@
|
|||
# Changelog
|
||||
|
||||
- Mar-27, 2026 - 12:53 AM +08 - [Released 1.5.0 with vault-wide star maintenance commands and CI enforcement]
|
||||
|
||||
## [1.5.0] - 2026-03-27
|
||||
|
||||
### Added
|
||||
- Commands to refresh, embed, and remove GitHub star counts across all markdown notes in the vault
|
||||
- GitHub Actions CI that installs dependencies, runs `pnpm test`, and runs `pnpm build` on pushes and pull requests
|
||||
- Unit tests for vault-wide repo deduplication and embedded-star removal helpers
|
||||
|
||||
### Changed
|
||||
- Refactored vault-wide refresh and removal flows to use shared helper logic covered by unit tests
|
||||
- Added declared CodeMirror dependencies required for clean CI and reproducible installs
|
||||
|
||||
- Mar-19, 2026 - 10:59 PM +08 - [Refined refresh behavior, added tests, and updated docs]
|
||||
|
||||
## [1.4.0] - 2026-03-19
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# GitHub Stars Plugin
|
||||
|
||||
[](LICENSE)
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](https://obsidian.md/plugins?id=github-stars)
|
||||
|
||||
An Obsidian plugin that automatically displays GitHub star counts next to repository links in your notes — in both Reading View and Live Preview. Star counts can also be embedded directly into your markdown, making them visible outside Obsidian.
|
||||
|
|
@ -57,9 +57,12 @@ The plugin can be configured in the Settings tab:
|
|||
The plugin adds the following commands:
|
||||
|
||||
- **Refresh for current note**: Fetches fresh star counts from GitHub for all repositories in the current note and rerenders the note. If `Update embedded stars on refresh` is enabled, it also updates already-embedded star text.
|
||||
- **Refresh for all notes**: Fetches fresh star counts from GitHub for repositories found across your entire vault. Duplicate repository links are deduplicated so each repository is fetched once per run. If `Update embedded stars on refresh` is enabled, embedded star text is updated across notes.
|
||||
- **Clear cache**: Clears the cached star counts
|
||||
- **Embed star counts in current note**: Writes star counts (e.g. `⭐ 1.2k`) directly into the markdown file after each GitHub link. Re-running updates existing counts.
|
||||
- **Embed star counts in all notes**: Writes or updates embedded star counts in every markdown note in the vault.
|
||||
- **Remove embedded star counts from current note**: Strips all embedded star counts from the file
|
||||
- **Remove embedded star counts from all notes**: Removes embedded star counts from every markdown note in the vault.
|
||||
|
||||
## 🧪 Development
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
extractReposFromContent,
|
||||
extractUniqueReposFromContents,
|
||||
findEmbeddedGitHubLinkMatches,
|
||||
findGitHubLinkMatches,
|
||||
removeEmbeddedGitHubStars,
|
||||
rewriteGitHubLinksWithStars,
|
||||
shouldUseCachedEntry,
|
||||
} from './githubStarsCore';
|
||||
|
|
@ -39,6 +41,27 @@ https://github.com/baz/qux/tree/main
|
|||
});
|
||||
});
|
||||
|
||||
describe('extractUniqueReposFromContents', () => {
|
||||
it('deduplicates repositories across multiple note contents', () => {
|
||||
const contents = [
|
||||
`
|
||||
[Repo](https://github.com/foo/bar)
|
||||
https://github.com/baz/qux
|
||||
`,
|
||||
`
|
||||
https://github.com/foo/bar.git
|
||||
[Another](https://github.com/quux/corge/tree/main)
|
||||
`,
|
||||
];
|
||||
|
||||
expect(extractUniqueReposFromContents(contents)).toEqual([
|
||||
{ owner: 'foo', repo: 'bar' },
|
||||
{ owner: 'baz', repo: 'qux' },
|
||||
{ owner: 'quux', repo: 'corge' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refresh-related embedded star behavior', () => {
|
||||
it('finds only links that already have embedded stars', () => {
|
||||
const content = `
|
||||
|
|
@ -117,3 +140,33 @@ describe('explicit embed command behavior', () => {
|
|||
expect(result.content).toContain('[Plain](https://github.com/foo/plain) ⭐ 10');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeEmbeddedGitHubStars', () => {
|
||||
it('removes all embedded star counts and reports how many were removed', () => {
|
||||
const content = `
|
||||
[Embedded](https://github.com/foo/embedded) ⭐ 1.2k
|
||||
[Plain](https://github.com/foo/plain)
|
||||
[Also Embedded](https://github.com/bar/baz) ⭐ 42
|
||||
`;
|
||||
|
||||
expect(removeEmbeddedGitHubStars(content)).toEqual({
|
||||
content: `
|
||||
[Embedded](https://github.com/foo/embedded)
|
||||
[Plain](https://github.com/foo/plain)
|
||||
[Also Embedded](https://github.com/bar/baz)
|
||||
`,
|
||||
removedCount: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('leaves content unchanged when no embedded star counts exist', () => {
|
||||
const content = `
|
||||
[Plain](https://github.com/foo/plain)
|
||||
`;
|
||||
|
||||
expect(removeEmbeddedGitHubStars(content)).toEqual({
|
||||
content,
|
||||
removedCount: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ export interface GitHubLinkMatch extends RepoRef {
|
|||
linkText: string;
|
||||
}
|
||||
|
||||
export interface RemovalResult {
|
||||
content: string;
|
||||
removedCount: number;
|
||||
}
|
||||
|
||||
export function shouldUseCachedEntry(
|
||||
entry: CacheEntry | undefined,
|
||||
cacheExpiryMinutes: number,
|
||||
|
|
@ -47,6 +52,18 @@ export function extractReposFromContent(content: string): RepoRef[] {
|
|||
return Array.from(repos.values());
|
||||
}
|
||||
|
||||
export function extractUniqueReposFromContents(contents: string[]): RepoRef[] {
|
||||
const repos = new Map<string, RepoRef>();
|
||||
|
||||
for (const content of contents) {
|
||||
for (const repo of extractReposFromContent(content)) {
|
||||
repos.set(`${repo.owner}/${repo.repo}`, repo);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(repos.values());
|
||||
}
|
||||
|
||||
export function findGitHubLinkMatches(content: string): GitHubLinkMatch[] {
|
||||
const linkRegex = /(\[[^\]]*\]\(https?:\/\/(?:www\.)?github\.com\/([^/\s)]+)\/([^/\s)#?]+)[^)]*\))( ⭐ [\d,.]+[kMB]?)?/g;
|
||||
return collectLinkMatches(content, linkRegex);
|
||||
|
|
@ -84,6 +101,16 @@ export function rewriteGitHubLinksWithStars(
|
|||
return { content: updatedContent, updatedCount };
|
||||
}
|
||||
|
||||
export function removeEmbeddedGitHubStars(content: string): RemovalResult {
|
||||
const starRegex = /(\[[^\]]*\]\(https?:\/\/(?:www\.)?github\.com\/[^)]+\)) ⭐ [\d,.]+[kMB]?/g;
|
||||
const matches = content.match(starRegex);
|
||||
|
||||
return {
|
||||
content: content.replace(starRegex, '$1'),
|
||||
removedCount: matches?.length ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
function collectLinkMatches(content: string, regex: RegExp): GitHubLinkMatch[] {
|
||||
const matches: GitHubLinkMatch[] = [];
|
||||
let match;
|
||||
|
|
|
|||
163
main.ts
163
main.ts
|
|
@ -1,7 +1,7 @@
|
|||
import { App, MarkdownView, Notice, Plugin, PluginSettingTab, Setting, MarkdownPostProcessorContext, requestUrl } from 'obsidian';
|
||||
import { EditorView, ViewPlugin, ViewUpdate, Decoration, DecorationSet, WidgetType } from '@codemirror/view';
|
||||
import { RangeSetBuilder } from '@codemirror/state';
|
||||
import { extractReposFromContent, findEmbeddedGitHubLinkMatches, findGitHubLinkMatches, GitHubLinkMatch, RepoRef, rewriteGitHubLinksWithStars, shouldUseCachedEntry } from './githubStarsCore';
|
||||
import { extractReposFromContent, extractUniqueReposFromContents, findEmbeddedGitHubLinkMatches, findGitHubLinkMatches, GitHubLinkMatch, removeEmbeddedGitHubStars, RepoRef, rewriteGitHubLinksWithStars, shouldUseCachedEntry } from './githubStarsCore';
|
||||
|
||||
|
||||
interface GitHubStarsSettings {
|
||||
|
|
@ -212,6 +212,15 @@ export default class GitHubStarsPlugin extends Plugin {
|
|||
}
|
||||
});
|
||||
|
||||
// Add a command to refresh star counts for every markdown note in the vault
|
||||
this.addCommand({
|
||||
id: 'refresh-github-stars-all-notes',
|
||||
name: 'Refresh for all notes',
|
||||
callback: () => {
|
||||
void this.refreshAllNotes();
|
||||
}
|
||||
});
|
||||
|
||||
// Add a command to embed star counts directly into the markdown file
|
||||
this.addCommand({
|
||||
id: 'embed-github-stars',
|
||||
|
|
@ -228,6 +237,15 @@ export default class GitHubStarsPlugin extends Plugin {
|
|||
}
|
||||
});
|
||||
|
||||
// Add a command to embed star counts in every markdown note in the vault
|
||||
this.addCommand({
|
||||
id: 'embed-github-stars-all-notes',
|
||||
name: 'Embed star counts in all notes',
|
||||
callback: () => {
|
||||
void this.embedStarCountsForAllNotes();
|
||||
}
|
||||
});
|
||||
|
||||
// Add a command to remove embedded star counts from the file
|
||||
this.addCommand({
|
||||
id: 'remove-embedded-github-stars',
|
||||
|
|
@ -243,6 +261,15 @@ export default class GitHubStarsPlugin extends Plugin {
|
|||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Add a command to remove embedded star counts from every markdown note in the vault
|
||||
this.addCommand({
|
||||
id: 'remove-embedded-github-stars-all-notes',
|
||||
name: 'Remove embedded star counts from all notes',
|
||||
callback: () => {
|
||||
void this.removeEmbeddedStarCountsFromAllNotes();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onunload() {
|
||||
|
|
@ -305,6 +332,15 @@ export default class GitHubStarsPlugin extends Plugin {
|
|||
view.previewMode.rerender(true);
|
||||
}
|
||||
|
||||
private rerenderAllOpenMarkdownViews(): void {
|
||||
this.app.workspace.getLeavesOfType('markdown').forEach((leaf) => {
|
||||
const view = leaf.view;
|
||||
if (view instanceof MarkdownView) {
|
||||
this.rerenderMarkdownView(view);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getRepoCacheKey(owner: string, repo: string): string {
|
||||
return `${owner}/${repo}`;
|
||||
}
|
||||
|
|
@ -614,6 +650,67 @@ export default class GitHubStarsPlugin extends Plugin {
|
|||
new Notice('Refreshing GitHub star counts...');
|
||||
}
|
||||
|
||||
private async refreshAllNotes(): Promise<void> {
|
||||
this.activeRefreshRunId += 1;
|
||||
new Notice('Refreshing GitHub star counts for all notes...');
|
||||
|
||||
const markdownFiles = this.app.vault.getMarkdownFiles();
|
||||
const contents = await Promise.all(markdownFiles.map((file) => this.app.vault.read(file)));
|
||||
const uniqueRepos = extractUniqueReposFromContents(contents);
|
||||
|
||||
const refreshResults = new Map<string, StarCountFetchResult>();
|
||||
let hadRefreshFailure = false;
|
||||
|
||||
for (const repo of uniqueRepos) {
|
||||
const result = await this.fetchStarCount(repo.owner, repo.repo, true);
|
||||
refreshResults.set(this.getRepoCacheKey(repo.owner, repo.repo), result);
|
||||
hadRefreshFailure = hadRefreshFailure || result.fetchFailed;
|
||||
}
|
||||
|
||||
let updatedLinks = 0;
|
||||
if (this.settings.updateEmbeddedStarsOnRefresh) {
|
||||
updatedLinks = await this.updateEmbeddedStarCountsForAllNotes(refreshResults);
|
||||
}
|
||||
|
||||
this.rerenderAllOpenMarkdownViews();
|
||||
|
||||
if (hadRefreshFailure) {
|
||||
new Notice('Finished refreshing all notes, but some GitHub star counts could not be refreshed.');
|
||||
}
|
||||
|
||||
if (this.settings.updateEmbeddedStarsOnRefresh) {
|
||||
new Notice(`Refreshed ${uniqueRepos.length} repositories across all notes and updated ${updatedLinks} embedded star counts.`);
|
||||
} else {
|
||||
new Notice(`Refreshed ${uniqueRepos.length} repositories across all notes.`);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateEmbeddedStarCountsForAllNotes(refreshResults: Map<string, StarCountFetchResult>): Promise<number> {
|
||||
let totalUpdatedLinks = 0;
|
||||
|
||||
for (const file of this.app.vault.getMarkdownFiles()) {
|
||||
const content = await this.app.vault.read(file);
|
||||
const matches = findEmbeddedGitHubLinkMatches(content);
|
||||
if (matches.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = rewriteGitHubLinksWithStars(content, matches, (match) => {
|
||||
if (refreshResults.get(this.getRepoCacheKey(match.owner, match.repo))?.refreshedFromGitHub !== true) {
|
||||
return null;
|
||||
}
|
||||
return this.getFormattedCachedStars(match);
|
||||
});
|
||||
|
||||
if (result.updatedCount > 0) {
|
||||
await this.app.vault.modify(file, result.content);
|
||||
totalUpdatedLinks += result.updatedCount;
|
||||
}
|
||||
}
|
||||
|
||||
return totalUpdatedLinks;
|
||||
}
|
||||
|
||||
private async updateEmbeddedStarCounts(refreshResults: Map<string, StarCountFetchResult>): Promise<void> {
|
||||
await this.rewriteActiveNoteStars({
|
||||
findMatches: findEmbeddedGitHubLinkMatches,
|
||||
|
|
@ -657,20 +754,70 @@ export default class GitHubStarsPlugin extends Plugin {
|
|||
return;
|
||||
}
|
||||
|
||||
let content = await this.app.vault.read(file);
|
||||
const content = await this.app.vault.read(file);
|
||||
const result = removeEmbeddedGitHubStars(content);
|
||||
|
||||
// Match GitHub markdown links followed by an embedded star count
|
||||
const starRegex = /(\[[^\]]*\]\(https?:\/\/(?:www\.)?github\.com\/[^)]+\)) ⭐ [\d,.]+[kMB]?/g;
|
||||
const newContent = content.replace(starRegex, '$1');
|
||||
|
||||
if (newContent !== content) {
|
||||
await this.app.vault.modify(file, newContent);
|
||||
if (result.removedCount > 0) {
|
||||
await this.app.vault.modify(file, result.content);
|
||||
new Notice('Removed embedded star counts');
|
||||
} else {
|
||||
new Notice('No embedded star counts found');
|
||||
}
|
||||
}
|
||||
|
||||
async embedStarCountsForAllNotes(): Promise<void> {
|
||||
const markdownFiles = this.app.vault.getMarkdownFiles();
|
||||
let totalUpdatedLinks = 0;
|
||||
|
||||
new Notice(`Embedding star counts across ${markdownFiles.length} notes...`);
|
||||
|
||||
for (const file of markdownFiles) {
|
||||
const content = await this.app.vault.read(file);
|
||||
const matches = findGitHubLinkMatches(content);
|
||||
if (matches.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const match of matches) {
|
||||
await this.getStarCount(match.owner, match.repo);
|
||||
}
|
||||
|
||||
const result = rewriteGitHubLinksWithStars(content, matches, (match) => this.getFormattedCachedStars(match));
|
||||
if (result.updatedCount > 0) {
|
||||
await this.app.vault.modify(file, result.content);
|
||||
totalUpdatedLinks += result.updatedCount;
|
||||
}
|
||||
}
|
||||
|
||||
this.rerenderAllOpenMarkdownViews();
|
||||
new Notice(`Embedded star counts for ${totalUpdatedLinks} GitHub links across all notes.`);
|
||||
}
|
||||
|
||||
async removeEmbeddedStarCountsFromAllNotes(): Promise<void> {
|
||||
const markdownFiles = this.app.vault.getMarkdownFiles();
|
||||
let filesUpdated = 0;
|
||||
let totalRemoved = 0;
|
||||
|
||||
for (const file of markdownFiles) {
|
||||
const content = await this.app.vault.read(file);
|
||||
const result = removeEmbeddedGitHubStars(content);
|
||||
|
||||
if (result.removedCount > 0) {
|
||||
await this.app.vault.modify(file, result.content);
|
||||
filesUpdated += 1;
|
||||
totalRemoved += result.removedCount;
|
||||
}
|
||||
}
|
||||
|
||||
this.rerenderAllOpenMarkdownViews();
|
||||
|
||||
if (filesUpdated > 0) {
|
||||
new Notice(`Removed ${totalRemoved} embedded star counts from ${filesUpdated} notes.`);
|
||||
} else {
|
||||
new Notice('No embedded star counts found in any notes.');
|
||||
}
|
||||
}
|
||||
|
||||
private async rewriteActiveNoteStars(options: {
|
||||
findMatches: (content: string) => GitHubLinkMatch[];
|
||||
noLinksNotice: string | null;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "github-stars",
|
||||
"name": "GitHub Stars",
|
||||
"version": "1.4.0",
|
||||
"version": "1.5.0",
|
||||
"minAppVersion": "1.8.9",
|
||||
"description": "Displays the number of stars for GitHub repositories mentioned in notes.",
|
||||
"author": "Flying Nobita",
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "obsidian-github-stars",
|
||||
"version": "1.4.0",
|
||||
"version": "1.5.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-github-stars",
|
||||
"version": "1.4.0",
|
||||
"version": "1.5.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.37",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "obsidian-github-stars",
|
||||
"version": "1.4.0",
|
||||
"version": "1.5.0",
|
||||
"description": "Obsidian plugin that displays the number of stars for GitHub repositories mentioned in notes",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
@ -18,6 +18,8 @@
|
|||
"author": "Flying Nobita",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.37.1",
|
||||
"@types/node": "^20.19.37",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
|
|
|
|||
2465
pnpm-lock.yaml
Normal file
2465
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -5,5 +5,6 @@
|
|||
"1.2.0": "1.8.9",
|
||||
"1.2.1": "1.8.9",
|
||||
"1.3.0": "1.8.9",
|
||||
"1.4.0": "1.8.9"
|
||||
"1.4.0": "1.8.9",
|
||||
"1.5.0": "1.8.9"
|
||||
}
|
||||
Loading…
Reference in a new issue