mirror of
https://github.com/firstsun-dev/git-files-sync.git
synced 2026-07-22 17:20:30 +00:00
The ribbon button used 'list-checks' while the Sync Status view itself uses 'git-compare' (getIcon). Use 'git-compare' for the ribbon too so the "Open sync status" icon matches the sync status view icon. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
277 lines
8 KiB
TypeScript
277 lines
8 KiB
TypeScript
import { Plugin, TFile, MarkdownView, Notice, Platform } from 'obsidian';
|
|
import { DEFAULT_SETTINGS, GitLabFilesPushSettings, GitLabSyncSettingTab, getServiceName } from "./settings";
|
|
import { GitLabService } from './services/gitlab-service';
|
|
import { GitHubService } from './services/github-service';
|
|
import { GitServiceInterface } from './services/git-service-interface';
|
|
import { SyncManager } from './logic/sync-manager';
|
|
import { SyncStatusView, SYNC_STATUS_VIEW_TYPE } from './ui/SyncStatusView';
|
|
import { GitignoreManager } from './logic/gitignore-manager';
|
|
import { logger } from './utils/logger';
|
|
import { ConfirmModal } from './ui/ConfirmModal';
|
|
|
|
export default class GitLabFilesPush extends Plugin {
|
|
settings: GitLabFilesPushSettings;
|
|
gitService: GitServiceInterface;
|
|
sync: SyncManager;
|
|
gitignoreManager: GitignoreManager;
|
|
|
|
async onload() {
|
|
await this.loadSettings();
|
|
this.addSettingTab(new GitLabSyncSettingTab(this.app, this));
|
|
|
|
this.registerView(
|
|
SYNC_STATUS_VIEW_TYPE,
|
|
(leaf) => new SyncStatusView(leaf, this)
|
|
);
|
|
|
|
this.addRibbonIcon('git-compare', 'Open sync status', async () => {
|
|
await this.activateSyncStatusView();
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'open-sync-status',
|
|
name: 'Open sync status',
|
|
callback: async () => {
|
|
await this.activateSyncStatusView();
|
|
}
|
|
});
|
|
|
|
this.initializeGitService();
|
|
this.gitignoreManager = new GitignoreManager(this.app, this.gitService, this.settings.branch, this.settings.rootPath, this.settings.vaultFolder);
|
|
this.sync = new SyncManager(this.app, this.gitService, this.settings, this.saveSettings.bind(this));
|
|
|
|
this.addRibbonIcon('upload-cloud', Platform.isMobile ? `Push` : `Push to ${this.serviceName}`, async () => {
|
|
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
if (activeView && activeView.file instanceof TFile) {
|
|
await this.sync.pushFile(activeView.file);
|
|
} else {
|
|
new Notice('No active note to push');
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'push-current-file',
|
|
name: `Push current file to ${this.serviceName}`,
|
|
callback: async () => {
|
|
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
if (activeView && activeView.file instanceof TFile) {
|
|
await this.sync.pushFile(activeView.file);
|
|
}
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'pull-current-file',
|
|
name: `Pull current file from ${this.serviceName}`,
|
|
callback: async () => {
|
|
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
if (activeView && activeView.file instanceof TFile) {
|
|
await this.sync.pullFile(activeView.file);
|
|
}
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'push-all-files',
|
|
name: 'Push all files',
|
|
callback: async () => {
|
|
await this.pushAllFiles();
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: 'pull-all-files',
|
|
name: 'Pull all files',
|
|
callback: async () => {
|
|
await this.pullAllFiles();
|
|
}
|
|
});
|
|
|
|
this.registerEvent(
|
|
this.app.workspace.on('file-menu', (menu, file) => {
|
|
if (file instanceof TFile) {
|
|
menu.addItem((item) => {
|
|
item.setTitle(`Push to ${this.serviceName}`)
|
|
.setIcon('upload-cloud')
|
|
.onClick(async () => { await this.sync.pushFile(file); });
|
|
});
|
|
menu.addItem((item) => {
|
|
item.setTitle(`Pull from ${this.serviceName}`)
|
|
.setIcon('download-cloud')
|
|
.onClick(async () => { await this.sync.pullFile(file); });
|
|
});
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
private get serviceName(): string {
|
|
return getServiceName(this.settings);
|
|
}
|
|
|
|
async activateSyncStatusView(): Promise<void> {
|
|
const { workspace } = this.app;
|
|
|
|
let leaf = workspace.getLeavesOfType(SYNC_STATUS_VIEW_TYPE)[0];
|
|
|
|
if (!leaf) {
|
|
const rightLeaf = workspace.getRightLeaf(false);
|
|
if (rightLeaf) {
|
|
await rightLeaf.setViewState({
|
|
type: SYNC_STATUS_VIEW_TYPE,
|
|
active: true,
|
|
});
|
|
leaf = rightLeaf;
|
|
}
|
|
}
|
|
|
|
if (leaf) {
|
|
await workspace.revealLeaf(leaf);
|
|
}
|
|
}
|
|
|
|
async pushAllFiles(): Promise<void> {
|
|
await this.runAllFiles('push');
|
|
}
|
|
|
|
async pullAllFiles(): Promise<void> {
|
|
await this.runAllFiles('pull');
|
|
}
|
|
|
|
private async listAllFilesFromAdapter(dirPath: string): Promise<string[]> {
|
|
const results: string[] = [];
|
|
try {
|
|
const { files, folders } = await this.app.vault.adapter.list(dirPath || '');
|
|
results.push(...files);
|
|
for (const folder of folders) {
|
|
const sub = await this.listAllFilesFromAdapter(folder);
|
|
results.push(...sub);
|
|
}
|
|
} catch { /* ignore inaccessible dirs */ }
|
|
return results;
|
|
}
|
|
|
|
private async runAllFiles(op: 'push' | 'pull'): Promise<void> {
|
|
const startPath = this.settings.vaultFolder || '';
|
|
const allPaths = await this.listAllFilesFromAdapter(startPath);
|
|
|
|
await this.gitService.listFiles(this.settings.branch);
|
|
await this.gitignoreManager.loadGitignores();
|
|
const files = allPaths.filter(p => !this.gitignoreManager.isIgnored(this.getNormalizedPath(p)));
|
|
|
|
if (files.length === 0) {
|
|
new Notice(`No files to ${op} in the configured vault folder`);
|
|
return;
|
|
}
|
|
|
|
const msg = op === 'push'
|
|
? `Push ${files.length} file(s) to ${this.serviceName}?`
|
|
: `Pull ${files.length} file(s) from ${this.serviceName}? This will overwrite local changes.`;
|
|
|
|
const confirmed = await this.showConfirmDialog(msg);
|
|
if (!confirmed) return;
|
|
|
|
const progressNotice = new Notice(`${op === 'push' ? 'Pushing' : 'Pulling'} 0/${files.length} files...`, 0);
|
|
|
|
try {
|
|
const results = op === 'push'
|
|
? await this.sync.pushAllFiles(files, (current, total, fileName) => {
|
|
progressNotice.setMessage(`Pushing ${current}/${total}: ${fileName}`);
|
|
})
|
|
: await this.sync.pullAllFiles(files, (current, total, fileName) => {
|
|
progressNotice.setMessage(`Pulling ${current}/${total}: ${fileName}`);
|
|
});
|
|
|
|
progressNotice.hide();
|
|
|
|
if (results.errors.length > 0) {
|
|
logger.error(`${op} errors:`, results.errors);
|
|
}
|
|
} catch (e) {
|
|
progressNotice.hide();
|
|
logger.error(String(e));
|
|
new Notice(`${op === 'push' ? 'Push' : 'Pull'} failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
}
|
|
}
|
|
|
|
filterFilesByVaultFolder(files: TFile[]): TFile[] {
|
|
if (!this.settings.vaultFolder) {
|
|
return files;
|
|
}
|
|
|
|
const folderPath = this.settings.vaultFolder + '/';
|
|
return files.filter(file => file.path.startsWith(folderPath) || file.path === this.settings.vaultFolder);
|
|
}
|
|
|
|
filterPathByVaultFolder(path: string): boolean {
|
|
if (!this.settings.vaultFolder) return true;
|
|
const folderPath = this.settings.vaultFolder + '/';
|
|
return path.startsWith(folderPath) || path === this.settings.vaultFolder;
|
|
}
|
|
|
|
getNormalizedPath(path: string): string {
|
|
if (!this.settings.vaultFolder) return path;
|
|
const folderPath = this.settings.vaultFolder + '/';
|
|
if (path.startsWith(folderPath)) {
|
|
return path.substring(folderPath.length);
|
|
}
|
|
if (path === this.settings.vaultFolder) return '';
|
|
return path;
|
|
}
|
|
|
|
getVaultPath(normalizedPath: string): string {
|
|
if (!this.settings.vaultFolder) return normalizedPath;
|
|
if (!normalizedPath) return this.settings.vaultFolder;
|
|
return this.settings.vaultFolder + '/' + normalizedPath;
|
|
}
|
|
|
|
initializeGitService(): void {
|
|
if (this.settings.serviceType === 'gitlab') {
|
|
const service = new GitLabService();
|
|
service.updateConfig(
|
|
this.settings.gitlabBaseUrl,
|
|
this.settings.gitlabToken,
|
|
this.settings.projectId,
|
|
this.settings.rootPath
|
|
);
|
|
this.gitService = service;
|
|
} else {
|
|
const service = new GitHubService();
|
|
service.updateConfig(
|
|
this.settings.githubToken,
|
|
this.settings.githubOwner,
|
|
this.settings.githubRepo,
|
|
this.settings.rootPath
|
|
);
|
|
this.gitService = service;
|
|
}
|
|
|
|
if (this.sync) {
|
|
this.sync.updateGitService(this.gitService);
|
|
}
|
|
}
|
|
|
|
private showConfirmDialog(message: string): Promise<boolean> {
|
|
return new Promise((resolve) => {
|
|
new ConfirmModal(
|
|
this.app,
|
|
message,
|
|
() => resolve(true),
|
|
() => resolve(false)
|
|
).open();
|
|
});
|
|
}
|
|
|
|
onunload() {
|
|
// Cleanup is handled by Obsidian for registered components
|
|
}
|
|
|
|
async loadSettings() {
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData() as Partial<GitLabFilesPushSettings>);
|
|
}
|
|
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
this.initializeGitService();
|
|
}
|
|
}
|