diff --git a/package.json b/package.json index 1df3567..52e5aec 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { - "name": "logseq-plugin-git", - "version": "1.7.0", + "name": "logseq-git-with-sync", + "version": "1.8.0", "main": "dist/index.html", "logseq": { - "id": "logseq-git", + "id": "logseq-git-with-sync", + "title": "Git with Sync", "icon": "logo.png" }, "scripts": { diff --git a/src/helper/constants.ts b/src/helper/constants.ts index 1de3088..13e7d31 100644 --- a/src/helper/constants.ts +++ b/src/helper/constants.ts @@ -109,43 +109,90 @@ export const BUTTONS = [ { key: "commit", title: "Commit", event: "commit" }, { key: "push", title: "Push", event: "push" }, { key: "commitAndPush", title: "Commit & Push", event: "commitAndPush" }, + { key: "initRepo", title: "Initialize Repository", event: "initRepo" }, ]; export const SETTINGS_SCHEMA: SettingSchemaDesc[] = [ { - key: "buttons", - title: "Buttons", - type: "enum", - default: ["Check Status", "Show Log", "Pull Rebase", "Commit & Push"], - description: "Select buttons to show", - enumPicker: "checkbox", - enumChoices: BUTTONS.map(({ title }) => title), + key: "repoInitHeader", + title: "Repository Initialization", + type: "heading", + default: "", + description: "", }, { - key: "checkWhenDBChanged", - title: "Check Status when DB Changed", - type: "boolean", - default: true, - description: "Check status when DB changed, restart logseq to take effect", + key: "gitHostDomain", + title: "Git Host Domain", + type: "string", + default: "github.com", + description: "Git hosting domain (e.g., github.com, gitlab.com, codeberg.org)", }, { - key: "autoCheckSynced", - title: "Auto Check If Synced", - type: "boolean", - default: false, - description: - "Automatically check if the local version is the same as the remote", + key: "gitUsername", + title: "Git Username/Email", + type: "string", + default: "", + description: "Your git username or email for authentication", }, { - key: "autoPush", - title: "Auto Push", + key: "gitPassword", + title: "Password/Token", + type: "string", + default: "", + description: "Your git password or personal access token (stored locally)", + }, + { + key: "gitRepoOwner", + title: "Repository Owner", + type: "string", + default: "", + description: "Repository owner username (appears in repository URL path)", + }, + { + key: "gitRepoName", + title: "Repository Name", + type: "string", + default: "", + description: "Name of the repository (e.g., logseq-notes, my-vault)", + }, + { + key: "gitBranch", + title: "Default Branch", + type: "string", + default: "main", + description: "Default branch name (typically main or master)", + }, + { + key: "gitUserEmail", + title: "Git Commit Email", + type: "string", + default: "", + description: "Email address to use for git commits", + }, + { + key: "gitUserName", + title: "Git Commit Name", + type: "string", + default: "", + description: "Display name to use for git commits", + }, + { + key: "testCredentialsButton", + title: "Test Credentials", type: "boolean", default: false, - description: "Auto push when logseq hide", + description: "Click to test if credentials can connect to the remote repository", + }, + { + key: "commitSettingsHeader", + title: "Commit Settings", + type: "heading", + default: "", + description: "", }, { key: "typeCommitMessage", - title: "Type Commit Message", + title: "Commit Message Type", type: "enum", default: "Default Message With Date", description: "Type of commit message to use", @@ -157,6 +204,71 @@ export const SETTINGS_SCHEMA: SettingSchemaDesc[] = [ title: "Custom Commit Message", type: "string", default: "", - description: "Custom commit message for plugin (valid only if commit message is set to Custom Message)", + description: "Custom commit message (only used if Commit Message Type is set to Custom)", + }, + { + key: "automationHeader", + title: "Automation & Behavior", + type: "heading", + default: "", + description: "", + }, + { + key: "checkWhenDBChanged", + title: "Check Status on DB Changes", + type: "boolean", + default: true, + description: "Check git status when database changes (restart logseq to take effect)", + }, + { + key: "autoCheckSynced", + title: "Auto Check If Synced", + type: "boolean", + default: false, + description: "Automatically check if local version matches remote", + }, + { + key: "autoPush", + title: "Auto Push on Hide", + type: "boolean", + default: false, + description: "Automatically push changes when Logseq is hidden/minimized", + }, + { + key: "autoPullBeforePush", + title: "Auto Pull Before Push", + type: "boolean", + default: true, + description: "Automatically pull-rebase before committing and pushing to prevent conflicts", + }, + { + key: "checkRemotePeriodically", + title: "Check Remote for Changes", + type: "boolean", + default: false, + description: "Check remote repository every 5 minutes for new changes", + }, + { + key: "autoSyncOnRemoteChanges", + title: "Auto Pull on Remote Changes", + type: "boolean", + default: false, + description: "Automatically pull when remote changes are detected (requires Check Remote for Changes)", + }, + { + key: "uiHeader", + title: "UI Settings", + type: "heading", + default: "", + description: "", + }, + { + key: "buttons", + title: "Toolbar Buttons", + type: "enum", + default: ["Check Status", "Show Log", "Pull Rebase", "Commit & Push"], + description: "Select which buttons to show in the toolbar dropdown", + enumPicker: "checkbox", + enumChoices: BUTTONS.map(({ title }) => title), } ]; diff --git a/src/helper/git.ts b/src/helper/git.ts index a3e0468..f95b975 100644 --- a/src/helper/git.ts +++ b/src/helper/git.ts @@ -147,7 +147,7 @@ export const push = async (showRes = true): Promise => { * @returns The commit message. */ export const commitMessage = () : string => { - + let defaultMessage = "[logseq-plugin-git:commit]"; switch (logseq.settings?.typeCommitMessage as string) { @@ -173,3 +173,159 @@ export const commitMessage = () : string => { return defaultMessage; } } + +/** + * Test git credentials by attempting to ls-remote + * @returns Promise + */ +export const testCredentials = async (showRes = true): Promise => { + const domain = logseq.settings?.gitHostDomain as string; + const username = logseq.settings?.gitUsername as string; + const password = logseq.settings?.gitPassword as string; + const repoOwner = logseq.settings?.gitRepoOwner as string; + const repoName = logseq.settings?.gitRepoName as string; + + // Validate required fields + if (!domain || !username || !password || !repoOwner || !repoName) { + const errorMsg = "Missing required fields. Please fill in Git Host Domain, Username, Password/Token, Repository Owner, and Repository Name."; + if (showRes) logseq.UI.showMsg(errorMsg, 'error'); + return { exitCode: 1, stdout: '', stderr: errorMsg }; + } + + try { + // URL encode username and password to handle special characters like @, :, etc. + const encodedUsername = encodeURIComponent(username); + const encodedPassword = encodeURIComponent(password); + const remoteUrl = `https://${encodedUsername}:${encodedPassword}@${domain}/${repoOwner}/${repoName}.git`; + + // Test credentials with ls-remote + const res = await execGitCommand(['ls-remote', remoteUrl, 'HEAD']); + + if (res.exitCode === 0) { + if (showRes) logseq.UI.showMsg('✓ Credentials valid! Successfully connected to remote repository.', 'success'); + console.log('[logseq-plugin-git] === Credentials test successful'); + return { exitCode: 0, stdout: 'Credentials valid', stderr: '' }; + } else { + if (showRes) logseq.UI.showMsg(`✗ Credential test failed: ${res.stderr || 'Repository not found or invalid credentials'}`, 'error'); + return res; + } + } catch (error) { + const errorMsg = `Credential test failed: ${error}`; + if (showRes) logseq.UI.showMsg(errorMsg, 'error'); + return { exitCode: 1, stdout: '', stderr: errorMsg }; + } +} + +/** + * Check if remote repository has new changes (is ahead of local) + * @returns Promise - true if remote has changes + */ +export const checkRemoteChanges = async (): Promise => { + try { + // Fetch from remote without merging + await execGitCommand(['fetch']); + + // Check if remote is ahead + const res = await execGitCommand(['rev-list', 'HEAD..@{u}', '--count']); + + if (res.exitCode === 0) { + const count = parseInt(res.stdout.trim()); + return count > 0; + } + return false; + } catch (error) { + console.log('[logseq-plugin-git] === Error checking remote changes', error); + return false; + } +} + +/** + * Initialize a git repository with HTTPS remote + * @returns Promise + */ +export const initRepo = async (showRes = true): Promise => { + const domain = logseq.settings?.gitHostDomain as string; + const username = logseq.settings?.gitUsername as string; + const password = logseq.settings?.gitPassword as string; + const repoOwner = logseq.settings?.gitRepoOwner as string; + const repoName = logseq.settings?.gitRepoName as string; + const branch = (logseq.settings?.gitBranch as string) || "main"; + const userEmail = logseq.settings?.gitUserEmail as string; + const userName = logseq.settings?.gitUserName as string; + + // Validate required fields + if (!domain || !username || !password || !repoOwner || !repoName) { + const errorMsg = "Missing required fields. Please fill in Git Host Domain, Username, Password/Token, Repository Owner, and Repository Name in settings."; + if (showRes) logseq.UI.showMsg(errorMsg, 'error'); + return { exitCode: 1, stdout: '', stderr: errorMsg }; + } + + if (!userEmail || !userName) { + const errorMsg = "Missing git user configuration. Please fill in Git User Email and Git User Name in settings."; + if (showRes) logseq.UI.showMsg(errorMsg, 'error'); + return { exitCode: 1, stdout: '', stderr: errorMsg }; + } + + try { + // Check if already initialized + const statusCheck = await execGitCommand(['status']); + if (statusCheck.exitCode === 0) { + const msg = "Repository already initialized."; + if (showRes) logseq.UI.showMsg(msg, 'warning'); + return { exitCode: 1, stdout: '', stderr: msg }; + } + + // Initialize git repository + let res = await execGitCommand(['init']); + if (res.exitCode !== 0) { + if (showRes) logseq.UI.showMsg(`Git init failed: ${res.stderr}`, 'error'); + return res; + } + + // Configure user + await execGitCommand(['config', 'user.email', userEmail]); + await execGitCommand(['config', 'user.name', userName]); + + // Create HTTPS remote URL with encoded credentials + const encodedUsername = encodeURIComponent(username); + const encodedPassword = encodeURIComponent(password); + const remoteUrl = `https://${encodedUsername}:${encodedPassword}@${domain}/${repoOwner}/${repoName}.git`; + + // Add remote origin + res = await execGitCommand(['remote', 'add', 'origin', remoteUrl]); + if (res.exitCode !== 0) { + if (showRes) logseq.UI.showMsg(`Failed to add remote: ${res.stderr}`, 'error'); + return res; + } + + // Set default branch + res = await execGitCommand(['branch', '-M', branch]); + if (res.exitCode !== 0) { + if (showRes) logseq.UI.showMsg(`Failed to set branch: ${res.stderr}`, 'error'); + return res; + } + + // Create initial commit + await execGitCommand(['add', '.']); + res = await execGitCommand(['commit', '-m', 'Initial commit from Logseq']); + if (res.exitCode !== 0) { + if (showRes) logseq.UI.showMsg(`Failed to create initial commit: ${res.stderr}`, 'error'); + return res; + } + + // Set upstream and push + res = await execGitCommand(['push', '-u', 'origin', branch]); + if (res.exitCode !== 0) { + if (showRes) logseq.UI.showMsg(`Repository initialized locally, but push failed: ${res.stderr}. You may need to create the repository on ${domain} first.`, 'warning'); + return res; + } + + if (showRes) logseq.UI.showMsg('Repository initialized successfully!', 'success'); + console.log('[logseq-plugin-git] === Repository initialized'); + return { exitCode: 0, stdout: 'Repository initialized successfully', stderr: '' }; + } catch (error) { + const errorMsg = `Initialization failed: ${error}`; + if (showRes) logseq.UI.showMsg(errorMsg, 'error'); + return { exitCode: 1, stdout: '', stderr: errorMsg }; + } +} diff --git a/src/main.tsx b/src/main.tsx index bcdd691..8b49a3b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -12,6 +12,9 @@ import { pullRebase, push, status, + initRepo, + testCredentials, + checkRemoteChanges, } from "./helper/git"; import { checkStatus, @@ -91,6 +94,15 @@ if (isDevelopment) { setPluginStyle(LOADING_STYLE); hidePopup(); + // Auto pull-rebase before push if enabled + if (logseq.settings?.autoPullBeforePush) { + const hasRemoteChanges = await checkRemoteChanges(); + if (hasRemoteChanges) { + logseq.UI.showMsg('Remote has changes, pulling before push...', 'info'); + await pullRebase(false); + } + } + const status = await checkStatus(); const changed = status?.stdout !== ""; if (changed) { @@ -116,6 +128,13 @@ if (isDevelopment) { console.log("[faiz:] === hidePopup click"); hidePopup(); }), + initRepo: debounce(async function () { + console.log("[faiz:] === initRepo click"); + setPluginStyle(LOADING_STYLE); + hidePopup(); + await initRepo(true); + checkStatus(); + }), }; logseq.provideModel(operations); @@ -126,6 +145,48 @@ if (isDevelopment) { '
', }); logseq.useSettingsSchema(SETTINGS_SCHEMA); + + // Periodic remote check (every 5 minutes) + let remoteCheckInterval: NodeJS.Timeout | null = null; + const startRemoteCheck = () => { + if (remoteCheckInterval) clearInterval(remoteCheckInterval); + + if (logseq.settings?.checkRemotePeriodically) { + remoteCheckInterval = setInterval(async () => { + const hasChanges = await checkRemoteChanges(); + if (hasChanges) { + logseq.UI.showMsg('⚠️ Remote repository has new changes. Pull to sync.', 'warning', { timeout: 10000 }); + + // Auto pull if enabled + if (logseq.settings?.autoSyncOnRemoteChanges) { + logseq.UI.showMsg('Auto-pulling remote changes...', 'info'); + await pullRebase(true); + checkStatus(); + } + } + }, 5 * 60 * 1000); // 5 minutes + } + }; + + // Listen for settings changes to handle button clicks + const settingsChangeHandler = (newSettings, oldSettings) => { + // Test Credentials button + if (newSettings.testCredentialsButton && !oldSettings.testCredentialsButton) { + testCredentials(true); + // Reset the button after a short delay + setTimeout(() => { + logseq.updateSettings({ testCredentialsButton: false }); + }, 100); + } + + // Remote check setting changed + if (newSettings.checkRemotePeriodically !== oldSettings.checkRemotePeriodically) { + startRemoteCheck(); + } + }; + + logseq.onSettingsChanged(settingsChangeHandler); + setTimeout(() => { const buttons = (logseq.settings?.buttons as string[]) ?.map((title) => BUTTONS.find((b) => b.title === title)) @@ -177,6 +238,9 @@ if (isDevelopment) { if (logseq.settings?.autoCheckSynced) checkIsSynced(); checkStatusWithDebounce(); + // Start remote check if enabled + startRemoteCheck(); + if (top) { top.document?.addEventListener("visibilitychange", async () => { const visibilityState = top?.document?.visibilityState;