From d989a71b3f309ae48c5cdb0e53c4d04c07c13517 Mon Sep 17 00:00:00 2001 From: Vinzent Date: Sun, 13 Oct 2024 18:27:36 +0200 Subject: [PATCH] refactor: better error handling and extra conflict statusbar icon --- src/commands.ts | 19 +- src/gitManager/isomorphicGit.ts | 18 +- src/gitManager/simpleGit.ts | 40 +- src/main.ts | 464 ++++++++++++---------- src/promiseQueue.ts | 6 +- src/statusBar.ts | 49 ++- src/types.ts | 15 +- src/ui/sourceControl/sourceControl.svelte | 4 +- 8 files changed, 338 insertions(+), 277 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 1fd8d688..8c13ea9d 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -5,7 +5,6 @@ import { DIFF_VIEW_CONFIG, } from "./constants"; import { openLineInGitHub, openHistoryInGitHub } from "./openInGitHub"; -import { PluginState } from "./types"; import { ChangedFilesModal } from "./ui/modals/changedFilesModal"; import { GeneralModal } from "./ui/modals/generalModal"; import { IgnoreModal } from "./ui/modals/ignoreModal"; @@ -275,19 +274,22 @@ export function addCommmands(plugin: ObsidianGit) { plugin.addCommand({ id: "edit-remotes", name: "Edit remotes", - callback: async () => plugin.editRemotes(), + callback: () => + plugin.editRemotes().catch((e) => plugin.displayError(e)), }); plugin.addCommand({ id: "remove-remote", name: "Remove remote", - callback: async () => plugin.removeRemote(), + callback: () => + plugin.removeRemote().catch((e) => plugin.displayError(e)), }); plugin.addCommand({ id: "set-upstream-branch", name: "Set upstream branch", - callback: async () => plugin.setUpstreamBranch(), + callback: () => + plugin.setUpstreamBranch().catch((e) => plugin.displayError(e)), }); plugin.addCommand({ @@ -325,13 +327,15 @@ export function addCommmands(plugin: ObsidianGit) { plugin.addCommand({ id: "init-repo", name: "Initialize a new repo", - callback: async () => plugin.createNewRepo(), + callback: () => + plugin.createNewRepo().catch((e) => plugin.displayError(e)), }); plugin.addCommand({ id: "clone-repo", name: "Clone an existing remote repo", - callback: async () => plugin.cloneNewRepo(), + callback: () => + plugin.cloneNewRepo().catch((e) => plugin.displayError(e)), }); plugin.addCommand({ @@ -341,8 +345,7 @@ export function addCommmands(plugin: ObsidianGit) { if (!(await plugin.isAllInitialized())) return; try { - const status = await plugin.gitManager.status(); - plugin.setState(PluginState.idle); + const status = await plugin.updateCachedStatus(); if (status.changed.length + status.staged.length > 500) { plugin.displayError("Too many changes to display"); return; diff --git a/src/gitManager/isomorphicGit.ts b/src/gitManager/isomorphicGit.ts index c2d405da..140dc8b5 100644 --- a/src/gitManager/isomorphicGit.ts +++ b/src/gitManager/isomorphicGit.ts @@ -20,7 +20,7 @@ import type { UnstagedFile, WalkDifference, } from "../types"; -import { PluginState } from "../types"; +import { CurrentGitAction } from "../types"; import { GeneralModal } from "../ui/modals/generalModal"; import { splitRemoteBranch, worthWalking } from "../utils"; import { GitManager } from "./gitManager"; @@ -154,7 +154,7 @@ export class IsomorphicGit extends GitManager { ); }, 20000); try { - this.plugin.setState(PluginState.status); + this.plugin.setPluginState({ gitAction: CurrentGitAction.status }); const status = ( await this.wrapFS(git.statusMatrix({ ...this.getRepo() })) ).map((row) => this.getFileStatusResult(row)); @@ -206,7 +206,7 @@ export class IsomorphicGit extends GitManager { }): Promise { try { await this.checkAuthorInfo(); - this.plugin.setState(PluginState.commit); + this.plugin.setPluginState({ gitAction: CurrentGitAction.commit }); const formatMessage = await this.formatCommitMessage(message); const hadConflict = this.plugin.localStorage.getConflict(); let parent: string[] | undefined = undefined; @@ -240,7 +240,7 @@ export class IsomorphicGit extends GitManager { vaultPath = this.getRelativeVaultPath(filepath); } try { - this.plugin.setState(PluginState.add); + this.plugin.setPluginState({ gitAction: CurrentGitAction.add }); if (await this.app.vault.adapter.exists(vaultPath)) { await this.wrapFS( git.add({ ...this.getRepo(), filepath: gitPath }) @@ -303,7 +303,7 @@ export class IsomorphicGit extends GitManager { async unstage(filepath: string, relativeToVault: boolean): Promise { try { - this.plugin.setState(PluginState.add); + this.plugin.setPluginState({ gitAction: CurrentGitAction.add }); filepath = this.getRelativeRepoPath(filepath, relativeToVault); await this.wrapFS( git.resetIndex({ ...this.getRepo(), filepath: filepath }) @@ -344,7 +344,7 @@ export class IsomorphicGit extends GitManager { async discard(filepath: string): Promise { try { - this.plugin.setState(PluginState.add); + this.plugin.setPluginState({ gitAction: CurrentGitAction.add }); await this.wrapFS( git.checkout({ ...this.getRepo(), @@ -415,7 +415,7 @@ export class IsomorphicGit extends GitManager { async pull(): Promise { const progressNotice = this.showNotice("Initializing pull"); try { - this.plugin.setState(PluginState.pull); + this.plugin.setPluginState({ gitAction: CurrentGitAction.pull }); const localCommit = await this.resolveRef("HEAD"); await this.fetch(); @@ -483,7 +483,7 @@ export class IsomorphicGit extends GitManager { } const progressNotice = this.showNotice("Initializing push"); try { - this.plugin.setState(PluginState.status); + this.plugin.setPluginState({ gitAction: CurrentGitAction.status }); const status = await this.branchInfo(); const trackingBranch = status.tracking; const currentBranch = status.current; @@ -491,7 +491,7 @@ export class IsomorphicGit extends GitManager { await this.getFileChangesCount(currentBranch!, trackingBranch!) ).length; - this.plugin.setState(PluginState.push); + this.plugin.setPluginState({ gitAction: CurrentGitAction.push }); await this.wrapFS( git.push({ diff --git a/src/gitManager/simpleGit.ts b/src/gitManager/simpleGit.ts index 1f0d212d..5d790484 100644 --- a/src/gitManager/simpleGit.ts +++ b/src/gitManager/simpleGit.ts @@ -17,7 +17,7 @@ import type { LogEntry, Status, } from "../types"; -import { NoNetworkError, PluginState } from "../types"; +import { NoNetworkError, CurrentGitAction } from "../types"; import { impossibleBranch, splitRemoteBranch } from "../utils"; import { GitManager } from "./gitManager"; @@ -119,9 +119,9 @@ export class SimpleGit extends GitManager { } async status(): Promise { - this.plugin.setState(PluginState.status); + this.plugin.setPluginState({ gitAction: CurrentGitAction.status }); const status = await this.git.status(); - this.plugin.setState(PluginState.idle); + this.plugin.setPluginState({ gitAction: CurrentGitAction.idle }); const allFilesFormatted = status.files.map((e) => { const res = this.formatPath(e); @@ -280,7 +280,7 @@ export class SimpleGit extends GitManager { async commitAll({ message }: { message: string }): Promise { if (this.plugin.settings.updateSubmodules) { - this.plugin.setState(PluginState.commit); + this.plugin.setPluginState({ gitAction: CurrentGitAction.commit }); const submodulePaths = await this.getSubmodulePaths(); for (const item of submodulePaths) { await this.git.cwd({ path: item, root: false }).add("-A"); @@ -289,11 +289,11 @@ export class SimpleGit extends GitManager { .commit(await this.formatCommitMessage(message)); } } - this.plugin.setState(PluginState.add); + this.plugin.setPluginState({ gitAction: CurrentGitAction.add }); await this.git.add("-A"); - this.plugin.setState(PluginState.commit); + this.plugin.setPluginState({ gitAction: CurrentGitAction.commit }); const res = await this.git.commit( await this.formatCommitMessage(message) @@ -310,7 +310,7 @@ export class SimpleGit extends GitManager { message: string; amend?: boolean; }): Promise { - this.plugin.setState(PluginState.commit); + this.plugin.setPluginState({ gitAction: CurrentGitAction.commit }); const res = ( await this.git.commit( @@ -320,42 +320,42 @@ export class SimpleGit extends GitManager { ).summary.changes; this.app.workspace.trigger("obsidian-git:head-change"); - this.plugin.setState(PluginState.idle); + this.plugin.setPluginState({ gitAction: CurrentGitAction.idle }); return res; } async stage(path: string, relativeToVault: boolean): Promise { - this.plugin.setState(PluginState.add); + this.plugin.setPluginState({ gitAction: CurrentGitAction.add }); path = this.getRelativeRepoPath(path, relativeToVault); await this.git.add(["--", path]); - this.plugin.setState(PluginState.idle); + this.plugin.setPluginState({ gitAction: CurrentGitAction.idle }); } async stageAll({ dir }: { dir?: string }): Promise { - this.plugin.setState(PluginState.add); + this.plugin.setPluginState({ gitAction: CurrentGitAction.add }); await this.git.add(dir ?? "-A"); - this.plugin.setState(PluginState.idle); + this.plugin.setPluginState({ gitAction: CurrentGitAction.idle }); } async unstageAll({ dir }: { dir?: string }): Promise { - this.plugin.setState(PluginState.add); + this.plugin.setPluginState({ gitAction: CurrentGitAction.add }); await this.git.reset(dir != undefined ? ["--", dir] : []); - this.plugin.setState(PluginState.idle); + this.plugin.setPluginState({ gitAction: CurrentGitAction.idle }); } async unstage(path: string, relativeToVault: boolean): Promise { - this.plugin.setState(PluginState.add); + this.plugin.setPluginState({ gitAction: CurrentGitAction.add }); path = this.getRelativeRepoPath(path, relativeToVault); await this.git.reset(["--", path]); - this.plugin.setState(PluginState.idle); + this.plugin.setPluginState({ gitAction: CurrentGitAction.idle }); } async discard(filepath: string): Promise { - this.plugin.setState(PluginState.add); + this.plugin.setPluginState({ gitAction: CurrentGitAction.add }); if (await this.isTracked(filepath)) { await this.git.checkout(["--", filepath]); } else { @@ -364,7 +364,7 @@ export class SimpleGit extends GitManager { true ); } - this.plugin.setState(PluginState.idle); + this.plugin.setPluginState({ gitAction: CurrentGitAction.idle }); } async hashObject(filepath: string): Promise { @@ -388,7 +388,7 @@ export class SimpleGit extends GitManager { } async pull(): Promise { - this.plugin.setState(PluginState.pull); + this.plugin.setPluginState({ gitAction: CurrentGitAction.pull }); try { if (this.plugin.settings.updateSubmodules) await this.git.subModule([ @@ -478,7 +478,7 @@ export class SimpleGit extends GitManager { } async push(): Promise { - this.plugin.setState(PluginState.push); + this.plugin.setPluginState({ gitAction: CurrentGitAction.push }); try { if (this.plugin.settings.updateSubmodules) { const res = await this.git diff --git a/src/main.ts b/src/main.ts index c94e1451..4362e16a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -37,10 +37,15 @@ import { LocalStorageSettings } from "./setting/localStorageSettings"; import type { FileStatusResult, ObsidianGitSettings, + PluginState, Status, UnstagedFile, } from "./types"; -import { mergeSettingsByPriority, NoNetworkError, PluginState } from "./types"; +import { + mergeSettingsByPriority, + NoNetworkError, + CurrentGitAction, +} from "./types"; import DiffView from "./ui/diff/diffView"; import HistoryView from "./ui/history/historyView"; import { BranchModal } from "./ui/modals/branchModal"; @@ -59,12 +64,15 @@ export default class ObsidianGit extends Plugin { settingsTab?: ObsidianGitSettingsTab; statusBar?: StatusBar; branchBar?: BranchStatusBar; - state: PluginState; + state: PluginState = { + gitAction: CurrentGitAction.idle, + loading: false, + offlineMode: false, + }; lastPulledFiles: FileStatusResult[]; gitReady = false; - promiseQueue: PromiseQueue = new PromiseQueue(); + promiseQueue: PromiseQueue = new PromiseQueue(this); autoCommitDebouncer: Debouncer<[], void> | undefined; - offlineMode = false; loading = false; cachedStatus: Status | undefined; // Used to store the path of the file that is currently shown in the diff view. @@ -78,13 +86,21 @@ export default class ObsidianGit extends Plugin { debRefresh: Debouncer<[], void>; - setState(state: PluginState): void { - this.state = state; + setPluginState(state: Partial): void { + this.state = Object.assign(this.state, state); this.statusBar?.display(); } async updateCachedStatus(): Promise { this.cachedStatus = await this.gitManager.status(); + if (this.cachedStatus.conflicted.length > 0) { + this.localStorage.setConflict(true); + await this.branchBar?.display(); + } else { + this.localStorage.setConflict(false); + await this.branchBar?.display(); + } + return this.cachedStatus; } @@ -106,7 +122,7 @@ export default class ObsidianGit extends Plugin { this.loading = true; this.app.workspace.trigger("obsidian-git:view-refresh"); - await this.updateCachedStatus(); + await this.updateCachedStatus().catch((e) => this.displayError(e)); this.loading = false; this.app.workspace.trigger("obsidian-git:view-refresh"); } @@ -414,7 +430,7 @@ export default class ObsidianGit extends Plugin { break; case "valid": this.gitReady = true; - this.setState(PluginState.idle); + this.setPluginState({ gitAction: CurrentGitAction.idle }); this.openEvent = this.app.workspace.on( "active-leaf-change", @@ -471,9 +487,13 @@ export default class ObsidianGit extends Plugin { } async createNewRepo() { - await this.gitManager.init(); - new Notice("Initialized new repo"); - await this.init(); + try { + await this.gitManager.init(); + new Notice("Initialized new repo"); + await this.init(); + } catch (e) { + this.displayError(e); + } } async cloneNewRepo() { @@ -492,80 +512,79 @@ export default class ObsidianGit extends Plugin { "Enter directory for clone. It needs to be empty or not existent.", allowEmpty: this.gitManager instanceof IsomorphicGit, }).openAndGetResult(); - if (dir !== undefined) { - if (dir === confirmOption) { - dir = "."; - } + if (dir == undefined) return; + if (dir === confirmOption) { + dir = "."; + } - dir = normalizePath(dir); - if (dir === "/") { - dir = "."; - } + dir = normalizePath(dir); + if (dir === "/") { + dir = "."; + } - if (dir === ".") { + if (dir === ".") { + const modal = new GeneralModal(this, { + options: ["NO", "YES"], + placeholder: `Does your remote repo contain a ${this.app.vault.configDir} directory at the root?`, + onlySelection: true, + }); + const containsConflictDir = await modal.openAndGetResult(); + if (containsConflictDir === undefined) { + new Notice("Aborted clone"); + return; + } else if (containsConflictDir === "YES") { + const confirmOption = + "DELETE ALL YOUR LOCAL CONFIG AND PLUGINS"; const modal = new GeneralModal(this, { - options: ["NO", "YES"], - placeholder: `Does your remote repo contain a ${this.app.vault.configDir} directory at the root?`, + options: ["Abort clone", confirmOption], + placeholder: `To avoid conflicts, the local ${this.app.vault.configDir} directory needs to be deleted.`, onlySelection: true, }); - const containsConflictDir = await modal.openAndGetResult(); - if (containsConflictDir === undefined) { + const shouldDelete = + (await modal.openAndGetResult()) === confirmOption; + if (shouldDelete) { + await this.app.vault.adapter.rmdir( + this.app.vault.configDir, + true + ); + } else { new Notice("Aborted clone"); return; - } else if (containsConflictDir === "YES") { - const confirmOption = - "DELETE ALL YOUR LOCAL CONFIG AND PLUGINS"; - const modal = new GeneralModal(this, { - options: ["Abort clone", confirmOption], - placeholder: `To avoid conflicts, the local ${this.app.vault.configDir} directory needs to be deleted.`, - onlySelection: true, - }); - const shouldDelete = - (await modal.openAndGetResult()) === confirmOption; - if (shouldDelete) { - await this.app.vault.adapter.rmdir( - this.app.vault.configDir, - true - ); - } else { - new Notice("Aborted clone"); - return; - } } } - const depth = await new GeneralModal(this, { - placeholder: - "Specify depth of clone. Leave empty for full clone.", - allowEmpty: true, - }).openAndGetResult(); - let depthInt = undefined; - if (depth !== "") { - depthInt = parseInt(depth); - if (isNaN(depthInt)) { - new Notice("Invalid depth. Aborting clone."); - return; - } - } - new Notice(`Cloning new repo into "${dir}"`); - const oldBase = this.settings.basePath; - const customDir = dir && dir !== "."; - //Set new base path before clone to ensure proper .git/index file location in isomorphic-git - if (customDir) { - this.settings.basePath = dir; - } - try { - await this.gitManager.clone(url, dir, depthInt); - } catch (error) { - this.settings.basePath = oldBase; - await this.saveSettings(); - throw error; + } + const depth = await new GeneralModal(this, { + placeholder: + "Specify depth of clone. Leave empty for full clone.", + allowEmpty: true, + }).openAndGetResult(); + let depthInt = undefined; + if (depth !== "") { + depthInt = parseInt(depth); + if (isNaN(depthInt)) { + new Notice("Invalid depth. Aborting clone."); + return; } + } + new Notice(`Cloning new repo into "${dir}"`); + const oldBase = this.settings.basePath; + const customDir = dir && dir !== "."; + //Set new base path before clone to ensure proper .git/index file location in isomorphic-git + if (customDir) { + this.settings.basePath = dir; + } + try { + await this.gitManager.clone(url, dir, depthInt); new Notice("Cloned new repo."); new Notice("Please restart Obsidian"); if (customDir) { await this.saveSettings(); } + } catch (error) { + this.displayError(error); + this.settings.basePath = oldBase; + await this.saveSettings(); } } } @@ -595,7 +614,7 @@ export default class ObsidianGit extends Plugin { } if (this.gitManager instanceof SimpleGit) { - const status = await this.gitManager.status(); + const status = await this.updateCachedStatus(); if (status.conflicted.length > 0) { this.displayError( `You have conflicts in ${status.conflicted.length} ${ @@ -607,7 +626,7 @@ export default class ObsidianGit extends Plugin { } this.app.workspace.trigger("obsidian-git:refresh"); - this.setState(PluginState.idle); + this.setPluginState({ gitAction: CurrentGitAction.idle }); } async commitAndSync( @@ -624,13 +643,12 @@ export default class ObsidianGit extends Plugin { await this.pull(); } - if ( - !(await this.commit({ - fromAuto: fromAutoBackup, - requestCustomMessage, - commitMessage, - })) - ) { + const commitSuccessful = await this.commit({ + fromAuto: fromAutoBackup, + requestCustomMessage, + commitMessage, + }); + if (!commitSuccessful) { return; } @@ -652,7 +670,7 @@ export default class ObsidianGit extends Plugin { this.displayMessage("No changes to push"); } } - this.setState(PluginState.idle); + this.setPluginState({ gitAction: CurrentGitAction.idle }); } // Returns true if commit was successfully @@ -670,136 +688,141 @@ export default class ObsidianGit extends Plugin { amend?: boolean; }): Promise { if (!(await this.isAllInitialized())) return false; + try { + let hadConflict = this.localStorage.getConflict(); - let hadConflict = this.localStorage.getConflict(); - - let changedFiles: { vault_path: string }[]; - let status: Status | undefined; - let unstagedFiles: UnstagedFile[] | undefined; + let changedFiles: { vault_path: string }[]; + let status: Status | undefined; + let unstagedFiles: UnstagedFile[] | undefined; - if (this.gitManager instanceof SimpleGit) { - await this.mayDeleteConflictFile(); - status = await this.updateCachedStatus(); + if (this.gitManager instanceof SimpleGit) { + await this.mayDeleteConflictFile(); + status = await this.updateCachedStatus(); - //Should not be necessary, but just in case - if (status.conflicted.length == 0) { - this.localStorage.setConflict(false); - hadConflict = false; - } + //Should not be necessary, but just in case + if (status.conflicted.length == 0) { + hadConflict = false; + } - // check for conflict files on auto backup - if (fromAuto && status.conflicted.length > 0) { + // check for conflict files on auto backup + if (fromAuto && status.conflicted.length > 0) { + this.displayError( + `Did not commit, because you have conflicts in ${ + status.conflicted.length + } ${ + status.conflicted.length == 1 ? "file" : "files" + }. Please resolve them and commit per command.` + ); + await this.handleConflict(status.conflicted); + return false; + } + changedFiles = [...status.changed, ...status.staged]; + } else if (fromAuto && hadConflict) { this.displayError( - `Did not commit, because you have conflicts in ${ - status.conflicted.length - } ${ - status.conflicted.length == 1 ? "file" : "files" - }. Please resolve them and commit per command.` + `Did not commit, because you have conflicts. Please resolve them and commit per command.` ); - await this.handleConflict(status.conflicted); return false; - } - changedFiles = [...status.changed, ...status.staged]; - } else if (fromAuto && hadConflict) { - this.setState(PluginState.conflicted); - this.displayError( - `Did not commit, because you have conflicts. Please resolve them and commit per command.` - ); - return false; - } else if (hadConflict) { - await this.mayDeleteConflictFile(); - status = await this.updateCachedStatus(); - changedFiles = [...status.changed, ...status.staged]; - } else { - if (onlyStaged) { - changedFiles = await ( - this.gitManager as IsomorphicGit - ).getStagedFiles(); + } else if (hadConflict) { + await this.mayDeleteConflictFile(); + status = await this.updateCachedStatus(); + changedFiles = [...status.changed, ...status.staged]; } else { - unstagedFiles = await ( - this.gitManager as IsomorphicGit - ).getUnstagedFiles(); - changedFiles = unstagedFiles.map(({ filepath }) => ({ - vault_path: this.gitManager.getRelativeVaultPath(filepath), - })); + if (onlyStaged) { + changedFiles = await ( + this.gitManager as IsomorphicGit + ).getStagedFiles(); + } else { + unstagedFiles = await ( + this.gitManager as IsomorphicGit + ).getUnstagedFiles(); + changedFiles = unstagedFiles.map(({ filepath }) => ({ + vault_path: + this.gitManager.getRelativeVaultPath(filepath), + })); + } } - } - if (await this.tools.hasTooBigFiles(changedFiles)) { - this.setState(PluginState.idle); - return false; - } - - if (changedFiles.length !== 0 || hadConflict) { - let cmtMessage = (commitMessage ??= fromAuto - ? this.settings.autoCommitMessage - : this.settings.commitMessage); - if ( - (fromAuto && this.settings.customMessageOnAutoBackup) || - requestCustomMessage - ) { - if (!this.settings.disablePopups && fromAuto) { - new Notice( - "Auto backup: Please enter a custom commit message. Leave empty to abort" - ); - } - const tempMessage = await new CustomMessageModal( - this - ).openAndGetResult(); + if (await this.tools.hasTooBigFiles(changedFiles)) { + this.setPluginState({ gitAction: CurrentGitAction.idle }); + return false; + } + if (changedFiles.length !== 0 || hadConflict) { + let cmtMessage = (commitMessage ??= fromAuto + ? this.settings.autoCommitMessage + : this.settings.commitMessage); if ( - tempMessage != undefined && - tempMessage != "" && - tempMessage != "..." + (fromAuto && this.settings.customMessageOnAutoBackup) || + requestCustomMessage ) { - cmtMessage = tempMessage; + if (!this.settings.disablePopups && fromAuto) { + new Notice( + "Auto backup: Please enter a custom commit message. Leave empty to abort" + ); + } + const tempMessage = await new CustomMessageModal( + this + ).openAndGetResult(); + + if ( + tempMessage != undefined && + tempMessage != "" && + tempMessage != "..." + ) { + cmtMessage = tempMessage; + } else { + this.setPluginState({ + gitAction: CurrentGitAction.idle, + }); + return false; + } + } + let committedFiles: number | undefined; + if (onlyStaged) { + committedFiles = await this.gitManager.commit({ + message: cmtMessage, + amend, + }); } else { - this.setState(PluginState.idle); - return false; + committedFiles = await this.gitManager.commitAll({ + message: cmtMessage, + status, + unstagedFiles, + amend, + }); } - } - let committedFiles: number | undefined; - if (onlyStaged) { - committedFiles = await this.gitManager.commit({ - message: cmtMessage, - amend, - }); - } else { - committedFiles = await this.gitManager.commitAll({ - message: cmtMessage, - status, - unstagedFiles, - amend, - }); - } - //Handle resolved conflict after commit - if (this.gitManager instanceof SimpleGit) { - if ((await this.updateCachedStatus()).conflicted.length == 0) { - this.localStorage.setConflict(false); + // Handle eventually resolved conflicts + if (this.gitManager instanceof SimpleGit) { + await this.updateCachedStatus(); } - } - let roughly = false; - if (committedFiles === undefined) { - roughly = true; - committedFiles = changedFiles.length; + let roughly = false; + if (committedFiles === undefined) { + roughly = true; + committedFiles = changedFiles.length; + } + await this.automaticsManager.setUpAutoCommitAndSync(); + this.displayMessage( + `Committed${roughly ? " approx." : ""} ${committedFiles} ${ + committedFiles == 1 ? "file" : "files" + }` + ); + } else { + this.displayMessage("No changes to commit"); } - await this.automaticsManager.setUpAutoCommitAndSync(); - this.displayMessage( - `Committed${roughly ? " approx." : ""} ${committedFiles} ${ - committedFiles == 1 ? "file" : "files" - }` - ); - } else { - this.displayMessage("No changes to commit"); - } - this.app.workspace.trigger("obsidian-git:refresh"); + this.app.workspace.trigger("obsidian-git:refresh"); - this.setState(PluginState.idle); - return true; + return true; + } catch (error) { + this.displayError(error); + return false; + } } + /* + * Returns true if push was successful + */ async push(): Promise { if (!(await this.isAllInitialized())) return false; if (!(await this.remotesAreSet())) { @@ -828,14 +851,12 @@ export default class ObsidianGit extends Plugin { hadConflict ) { this.displayError(`Cannot push. You have conflicts`); - this.setState(PluginState.conflicted); return false; } this.log("Pushing...."); const pushedFiles = await this.gitManager.push(); if (pushedFiles !== undefined) { - this.log("Pushed!", pushedFiles); if (pushedFiles > 0) { this.displayMessage( `Pushed ${pushedFiles} ${ @@ -846,18 +867,17 @@ export default class ObsidianGit extends Plugin { this.displayMessage(`No changes to push`); } } - this.offlineMode = false; - this.setState(PluginState.idle); + this.setPluginState({ offlineMode: false }); this.app.workspace.trigger("obsidian-git:refresh"); + return true; } catch (e) { if (e instanceof NoNetworkError) { this.handleNoNetworkError(e); } else { this.displayError(e); } + return false; } - - return true; } /** Used for internals @@ -868,29 +888,39 @@ export default class ObsidianGit extends Plugin { if (!(await this.remotesAreSet())) { return false; } - const pulledFiles = (await this.gitManager.pull()) || []; - this.offlineMode = false; - - if (pulledFiles.length > 0) { - this.displayMessage( - `Pulled ${pulledFiles.length} ${ - pulledFiles.length == 1 ? "file" : "files" - } from remote` - ); - this.lastPulledFiles = pulledFiles; + try { + const pulledFiles = (await this.gitManager.pull()) || []; + this.setPluginState({ offlineMode: false }); + + if (pulledFiles.length > 0) { + this.displayMessage( + `Pulled ${pulledFiles.length} ${ + pulledFiles.length == 1 ? "file" : "files" + } from remote` + ); + this.lastPulledFiles = pulledFiles; + } + return pulledFiles.length; + } catch (e) { + this.displayError(e); + + return false; } - return pulledFiles.length; } async fetch(): Promise { if (!(await this.remotesAreSet())) { return; } - await this.gitManager.fetch(); + try { + await this.gitManager.fetch(); - this.displayMessage(`Fetched from remote`); - this.offlineMode = false; - this.app.workspace.trigger("obsidian-git:refresh"); + this.displayMessage(`Fetched from remote`); + this.setPluginState({ offlineMode: false }); + this.app.workspace.trigger("obsidian-git:refresh"); + } catch (error) { + this.displayError(error); + } } async mayDeleteConflictFile(): Promise { @@ -916,7 +946,7 @@ export default class ObsidianGit extends Plugin { this.app.workspace.trigger("obsidian-git:refresh"); - this.setState(PluginState.idle); + this.setPluginState({ gitAction: CurrentGitAction.idle }); return true; } @@ -928,7 +958,7 @@ export default class ObsidianGit extends Plugin { this.app.workspace.trigger("obsidian-git:refresh"); - this.setState(PluginState.idle); + this.setPluginState({ gitAction: CurrentGitAction.idle }); return true; } @@ -1033,12 +1063,12 @@ export default class ObsidianGit extends Plugin { if (remoteBranch == undefined) { this.displayError("Aborted. No upstream-branch is set!", 10000); - this.setState(PluginState.idle); + this.setPluginState({ gitAction: CurrentGitAction.idle }); return false; } else { await this.gitManager.updateUpstreamBranch(remoteBranch); this.displayMessage(`Set upstream branch to ${remoteBranch}`); - this.setState(PluginState.idle); + this.setPluginState({ gitAction: CurrentGitAction.idle }); return true; } } @@ -1053,8 +1083,6 @@ export default class ObsidianGit extends Plugin { } async handleConflict(conflicted?: string[]): Promise { - this.setState(PluginState.conflicted); - this.localStorage.setConflict(true); let lines: string[] | undefined; if (conflicted !== undefined) { @@ -1212,7 +1240,7 @@ I strongly recommend to use "Source mode" for viewing the conflicted files. For } handleNoNetworkError(_: NoNetworkError): void { - if (!this.offlineMode) { + if (!this.state.offlineMode) { this.displayError( "Git: Going into offline mode. Future network errors will no longer be displayed.", 2000 @@ -1220,8 +1248,8 @@ I strongly recommend to use "Source mode" for viewing the conflicted files. For } else { this.log("Encountered network error, but already in offline mode"); } - this.offlineMode = true; - this.setState(PluginState.idle); + this.setPluginState({ offlineMode: true }); + this.setPluginState({ gitAction: CurrentGitAction.idle }); } // region: displaying / formatting messages @@ -1251,6 +1279,8 @@ I strongly recommend to use "Source mode" for viewing the conflicted files. For } else { error = new Error(String(data)); } + + this.setPluginState({ gitAction: CurrentGitAction.idle }); new Notice(error.message, timeout); console.error(`${this.manifest.id}:`, error.stack); this.statusBar?.displayMessage(error.message.toLowerCase(), timeout); diff --git a/src/promiseQueue.ts b/src/promiseQueue.ts index 16d8e3be..9b317168 100644 --- a/src/promiseQueue.ts +++ b/src/promiseQueue.ts @@ -1,6 +1,10 @@ +import type ObsidianGit from "./main"; + export class PromiseQueue { tasks: (() => Promise)[] = []; + constructor(private readonly plugin: ObsidianGit) {} + addTask(task: () => Promise) { this.tasks.push(task); if (this.tasks.length === 1) { @@ -11,7 +15,7 @@ export class PromiseQueue { handleTask() { if (this.tasks.length > 0) { this.tasks[0]() - .catch(console.error) + .catch((e) => this.plugin.displayError(e)) .finally(() => { this.tasks.shift(); this.handleTask(); diff --git a/src/statusBar.ts b/src/statusBar.ts index 6b6cf76e..d17df738 100644 --- a/src/statusBar.ts +++ b/src/statusBar.ts @@ -1,6 +1,6 @@ import { setIcon, moment } from "obsidian"; import type ObsidianGit from "./main"; -import { PluginState } from "./types"; +import { CurrentGitAction } from "./types"; interface StatusBarMessage { message: string; @@ -15,6 +15,7 @@ export class StatusBar { public lastMessageTimestamp: number | null; private base = "obsidian-git-statusbar-"; private iconEl: HTMLElement; + private conflictEl: HTMLElement; private textEl: HTMLElement; constructor( @@ -65,47 +66,58 @@ export class StatusBar { ) { this.statusBarEl.empty(); + this.conflictEl = this.statusBarEl.createDiv(); + this.conflictEl.setAttribute("data-tooltip-position", "top"); + this.conflictEl.style.float = "left"; + this.iconEl = this.statusBarEl.createDiv(); + this.iconEl.style.float = "left"; + this.textEl = this.statusBarEl.createDiv(); this.textEl.style.float = "right"; this.textEl.style.marginLeft = "5px"; - this.iconEl.style.float = "left"; } - switch (this.plugin.state) { - case PluginState.idle: + if (this.plugin.localStorage.getConflict()) { + setIcon(this.conflictEl, "alert-circle"); + this.conflictEl.ariaLabel = + "You have merge conflicts. Resolve them and commit afterwards."; + this.conflictEl.style.marginRight = "5px"; + this.conflictEl.addClass(this.base + "conflict"); + } else { + this.conflictEl.empty(); + + this.conflictEl.style.marginRight = ""; + } + switch (this.plugin.state.gitAction) { + case CurrentGitAction.idle: this.displayFromNow(); break; - case PluginState.status: + case CurrentGitAction.status: this.statusBarEl.ariaLabel = "Checking repository status..."; setIcon(this.iconEl, "refresh-cw"); this.statusBarEl.addClass(this.base + "status"); break; - case PluginState.add: + case CurrentGitAction.add: this.statusBarEl.ariaLabel = "Adding files..."; - setIcon(this.iconEl, "refresh-w"); + setIcon(this.iconEl, "archive"); this.statusBarEl.addClass(this.base + "add"); break; - case PluginState.commit: + case CurrentGitAction.commit: this.statusBarEl.ariaLabel = "Committing changes..."; setIcon(this.iconEl, "git-commit"); this.statusBarEl.addClass(this.base + "commit"); break; - case PluginState.push: + case CurrentGitAction.push: this.statusBarEl.ariaLabel = "Pushing changes..."; setIcon(this.iconEl, "upload"); this.statusBarEl.addClass(this.base + "push"); break; - case PluginState.pull: + case CurrentGitAction.pull: this.statusBarEl.ariaLabel = "Pulling changes..."; setIcon(this.iconEl, "download"); this.statusBarEl.addClass(this.base + "pull"); break; - case PluginState.conflicted: - this.statusBarEl.ariaLabel = "You have conflict files..."; - setIcon(this.iconEl, "alert-circle"); - this.statusBarEl.addClass(this.base + "conflict"); - break; default: this.statusBarEl.ariaLabel = "Failed on initialization!"; setIcon(this.iconEl, "alert-triangle"); @@ -116,22 +128,23 @@ export class StatusBar { private displayFromNow(): void { const timestamp = this.lastCommitTimestamp; + const offlineMode = this.plugin.state.offlineMode; if (timestamp) { const fromNow = moment(timestamp).fromNow(); this.statusBarEl.ariaLabel = `${ - this.plugin.offlineMode ? "Offline: " : "" + offlineMode ? "Offline: " : "" }Last Commit: ${fromNow}`; if (this.unPushedCommits ?? 0 > 0) { this.statusBarEl.ariaLabel += `\n(${this.unPushedCommits} unpushed commits)`; } } else { - this.statusBarEl.ariaLabel = this.plugin.offlineMode + this.statusBarEl.ariaLabel = offlineMode ? "Git is offline" : "Git is ready"; } - if (this.plugin.offlineMode) { + if (offlineMode) { setIcon(this.iconEl, "globe"); } else { setIcon(this.iconEl, "check"); diff --git a/src/types.ts b/src/types.ts index 6f30b302..ebe8b584 100644 --- a/src/types.ts +++ b/src/types.ts @@ -72,6 +72,10 @@ export interface Status { all: FileStatusResult[]; changed: FileStatusResult[]; staged: FileStatusResult[]; + + /* + * Only available for `SimpleGit` gitManager + */ conflicted: string[]; } @@ -191,14 +195,21 @@ export interface FileStatusResult { working_dir: string; } -export enum PluginState { +export interface PluginState { + offlineMode: boolean; + gitAction: CurrentGitAction; + /* Currently refreshing the cached status + */ + loading: boolean; +} + +export enum CurrentGitAction { idle, status, pull, add, commit, push, - conflicted, } export interface LogEntry { diff --git a/src/ui/sourceControl/sourceControl.svelte b/src/ui/sourceControl/sourceControl.svelte index c1a1e53e..61a406cb 100644 --- a/src/ui/sourceControl/sourceControl.svelte +++ b/src/ui/sourceControl/sourceControl.svelte @@ -7,7 +7,7 @@ Status, StatusRootTreeItem, } from "src/types"; - import { FileType, PluginState } from "src/types"; + import { CurrentGitAction, FileType } from "src/types"; import { getDisplayPath } from "src/utils"; import { onDestroy } from "svelte"; import { slide } from "svelte/transition"; @@ -62,7 +62,7 @@ loading = true; if (status) { if (await plugin.tools.hasTooBigFiles(status.staged)) { - plugin.setState(PluginState.idle); + plugin.setPluginState({ gitAction: CurrentGitAction.idle }); return false; } plugin.promiseQueue.addTask(() =>