Skip to content
Merged
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
69 changes: 68 additions & 1 deletion main.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Menu, MenuItem, Notice, Plugin, TFile } from 'obsidian';
import { Menu, MenuItem, Notice, Plugin, TAbstractFile, TFile } from 'obsidian';
import { GitHubService } from './src/services/githubService';
import { SyncService, PersistedSyncState, SyncResult } from './src/services/syncService';
import { LoggerService } from './src/services/loggerService';
import { DiffView, DIFF_VIEW_TYPE } from './src/views/DiffView';
import { SyncView, SYNC_VIEW_TYPE } from './src/views/SyncView';
import { GitHubOctokitSettingTab } from './src/ui';
import { GitHubOctokitSettings, DEFAULT_SETTINGS, AdditionalRepoConfig, VaultRepoConfig, VAULT_REPOS_CONFIG_PATH } from './src/types/settings';
import { normalizePath } from './src/utils/fileUtils';

/** Per-repo runtime state for additional repositories */
export interface AdditionalRepoRuntime {
Expand Down Expand Up @@ -188,6 +189,33 @@ export default class GitHubOctokitPlugin extends Plugin {
}
});

// File context menu: Open on GitHub / Copy GitHub link
this.registerEvent(
this.app.workspace.on('file-menu', (menu, file: TAbstractFile) => {
if (!(file instanceof TFile)) return;
const resolved = this.resolveGitHubUrl(file.path);
if (!resolved) return;

menu.addItem((item: MenuItem) => {
item.setTitle('Open on GitHub')
.setIcon('external-link')
.onClick(() => {
window.open(resolved, '_blank');
});
});

menu.addItem((item: MenuItem) => {
item.setTitle('GitHub link')
.setIcon('github')
.setSection('info.copy')
.onClick(() => {
void navigator.clipboard.writeText(resolved);
new Notice('GitHub link copied to clipboard');
});
});
})
);

// This adds a settings tab
this.addSettingTab(new GitHubOctokitSettingTab(this.app, this));

Expand Down Expand Up @@ -272,6 +300,45 @@ export default class GitHubOctokitPlugin extends Plugin {
return patterns;
}

// ========================================================================
// GitHub URL helpers
// ========================================================================

/**
* Convert a local vault path to the corresponding remote repo path,
* accounting for subfolderPath mapping.
*/
toRepoPath(localPath: string, subfolderPath: string): string {
if (!subfolderPath || subfolderPath === '/') {
return localPath;
}
return normalizePath(`${subfolderPath}/${localPath}`);
}

/**
* Resolve the repo config (main or additional) that owns a given vault path.
* Returns the GitHub blob URL, or null if no repo is configured.
*/
resolveGitHubUrl(vaultPath: string): string | null {
if (!this.githubService.isAuthenticated) return null;

// Check additional repos first (more specific paths)
for (const repoConfig of this.settings.additionalRepos) {
if (!repoConfig.enabled || !repoConfig.localPath) continue;
const prefix = repoConfig.localPath + '/';
if (vaultPath === repoConfig.localPath || vaultPath.startsWith(prefix)) {
const relativePath = vaultPath.slice(prefix.length);
const repoPath = this.toRepoPath(relativePath, repoConfig.subfolderPath);
return `https://github.com/${repoConfig.owner}/${repoConfig.repo}/blob/${repoConfig.branch}/${repoPath}`;
}
}

// Fall back to main repo
if (!this.settings.repo) return null;
const repoPath = this.toRepoPath(vaultPath, this.settings.subfolderPath);
return `https://github.com/${this.settings.repo.owner}/${this.settings.repo.name}/blob/${this.settings.repo.branch}/${repoPath}`;
}

/**
* Initialize additional repo services and authenticate them
*/
Expand Down
15 changes: 5 additions & 10 deletions src/views/SyncView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ItemView, WorkspaceLeaf, Notice, TFile } from 'obsidian';
import type GitHubOctokitPlugin from '../../main';
import { FileSyncState } from '../services/syncService';
import { LogEntry } from '../services/loggerService';
import { decodeBase64, normalizePath } from '../utils/fileUtils';
import { decodeBase64 } from '../utils/fileUtils';
import { confirmDestructiveAction } from '../ui/modals/confirmDialog';

export const SYNC_VIEW_TYPE = 'github-octokit-sync-view';
Expand Down Expand Up @@ -34,11 +34,7 @@ export class SyncView extends ItemView {
}

private toRepoPath(path: string): string {
const subfolderPath = this.plugin.settings.subfolderPath;
if (!subfolderPath || subfolderPath === '/') {
return path;
}
return normalizePath(`${subfolderPath}/${path}`);
return this.plugin.toRepoPath(path, this.plugin.settings.subfolderPath);
}

/** Restore persisted UI state from vault-specific localStorage */
Expand Down Expand Up @@ -355,12 +351,11 @@ export class SyncView extends ItemView {
});

// Open in GitHub
if (this.plugin.settings.repo) {
const ghUrl = this.plugin.resolveGitHubUrl(file.path);
if (ghUrl) {
const ghBtn = actions.createEl('button', { text: 'GitHub', cls: 'file-action' });
ghBtn.addEventListener('click', () => {
const repoPath = this.toRepoPath(file.path);
const url = `https://github.com/${this.plugin.settings.repo!.owner}/${this.plugin.settings.repo!.name}/blob/${this.plugin.settings.repo!.branch}/${repoPath}`;
window.open(url, '_blank');
window.open(ghUrl, '_blank');
});
}
}
Expand Down
13 changes: 13 additions & 0 deletions tests/views/SyncView.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { App } from 'obsidian';
import { SyncView } from '../../src/views/SyncView';
import { normalizePath } from '../../src/utils/fileUtils';

describe('SyncView - Branch-aware remote reads', () => {
it('passes the configured branch when loading remote content for diff', async () => {
Expand All @@ -25,6 +26,10 @@ describe('SyncView - Branch-aware remote reads', () => {
},
githubService,
openDiffView: jest.fn().mockResolvedValue(null),
toRepoPath(localPath: string, subfolderPath: string): string {
if (!subfolderPath || subfolderPath === '/') return localPath;
return normalizePath(`${subfolderPath}/${localPath}`);
},
};

const view = Object.create(SyncView.prototype) as SyncView & {
Expand Down Expand Up @@ -68,6 +73,10 @@ describe('SyncView - Branch-aware remote reads', () => {
},
githubService,
openDiffView: jest.fn().mockResolvedValue(null),
toRepoPath(localPath: string, subfolderPath: string): string {
if (!subfolderPath || subfolderPath === '/') return localPath;
return normalizePath(`${subfolderPath}/${localPath}`);
},
};

const view = Object.create(SyncView.prototype) as SyncView & {
Expand Down Expand Up @@ -111,6 +120,10 @@ describe('SyncView - Branch-aware remote reads', () => {
},
githubService,
openDiffView: jest.fn().mockResolvedValue(null),
toRepoPath(localPath: string, subfolderPath: string): string {
if (!subfolderPath || subfolderPath === '/') return localPath;
return normalizePath(`${subfolderPath}/${localPath}`);
},
};

const view = Object.create(SyncView.prototype) as SyncView & {
Expand Down
Loading