mirror of
https://github.com/hacker-c/obsidian-open-in-github-plugin.git
synced 2026-07-22 12:20:27 +00:00
Compare commits
7 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3869f1aefb | ||
|
|
1ab1be01bb | ||
|
|
f0fc3dcf16 | ||
|
|
ed3c918bd4 | ||
|
|
b2a06f36e9 | ||
|
|
28a8def24d | ||
|
|
43a60393c7 |
7 changed files with 167 additions and 156 deletions
2
LICENSE
2
LICENSE
|
|
@ -1,4 +1,4 @@
|
||||||
Copyright (C) 2020-2025 by Dynalist Inc.
|
Copyright (C) 2025 MurphyChen.
|
||||||
|
|
||||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
|
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
|
||||||
|
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -5,9 +5,15 @@ This is an Obsidian plugin designed to open projects or files directly in GitHub
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## Manually installing the plugin
|
## Installation
|
||||||
|
|
||||||
- Copy over `main.js`,`manifest.json`,`styles.css` to your vault `VaultFolder/.obsidian/plugins/open-in-github`.
|
1. Download Release Files:
|
||||||
|
- main.js
|
||||||
|
- manifest.json
|
||||||
|
2. Copy Files to Plugin Folder:
|
||||||
|
Paste the downloaded files into your vault’s plugin folder: `VaultFolder/.obsidian/plugins/open-in-github`
|
||||||
|
3. Enable the Plugin:
|
||||||
|
Restart Obsidian, go to Settings > Community plugins and enable the plugin.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|
|
||||||
293
main.ts
293
main.ts
|
|
@ -1,7 +1,6 @@
|
||||||
import { Notice, Plugin, addIcon } from 'obsidian'
|
import { Notice, Plugin, addIcon, TFile, Vault } from 'obsidian'
|
||||||
import * as path from 'path'
|
|
||||||
import * as fs from 'fs'
|
|
||||||
|
|
||||||
|
// open github icon
|
||||||
const customIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
const customIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/>
|
<path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/>
|
||||||
<path d="M9 18c-4.51 2-5-2-7-2"/>
|
<path d="M9 18c-4.51 2-5-2-7-2"/>
|
||||||
|
|
@ -18,199 +17,213 @@ const CONSTS = {
|
||||||
TITLE: 'Open in GitHub'
|
TITLE: 'Open in GitHub'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OpenInGitHubPluginSettings {
|
// Define constants for git paths relative to vault root
|
||||||
mySetting: string
|
const GIT_DIR = '.git'
|
||||||
}
|
const GIT_CONFIG_PATH = `${GIT_DIR}/config`
|
||||||
|
const GIT_HEAD_PATH = `${GIT_DIR}/HEAD`
|
||||||
const DEFAULT_SETTINGS: OpenInGitHubPluginSettings = {
|
|
||||||
mySetting: 'default'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class OpenInGitHubPlugin extends Plugin {
|
export default class OpenInGitHubPlugin extends Plugin {
|
||||||
settings: OpenInGitHubPluginSettings
|
vaultAdapter: Vault['adapter']
|
||||||
|
|
||||||
async onload() {
|
async onload() {
|
||||||
await this.loadSettings()
|
// Use vault adapter
|
||||||
|
this.vaultAdapter = this.app.vault.adapter
|
||||||
|
|
||||||
|
// Check if adapter exists (basic check for environment support)
|
||||||
|
if (!this.vaultAdapter) {
|
||||||
|
console.error("OpenInGitHubPlugin: Vault adapter is not available. Plugin may not work correctly.")
|
||||||
|
new Notice("OpenInGitHubPlugin: Vault adapter not found. Some features might be limited.")
|
||||||
|
}
|
||||||
|
|
||||||
// add icon
|
// add icon
|
||||||
addIcon(CONSTS.TITLE_ID, customIcon)
|
addIcon(CONSTS.TITLE_ID, customIcon)
|
||||||
|
|
||||||
// Left open-in-github button
|
// left open-in-github button
|
||||||
const ribbonIconEl = this.addRibbonIcon(CONSTS.TITLE_ID, CONSTS.TITLE, this.openGitHub)
|
this.addRibbonIcon(CONSTS.TITLE_ID, CONSTS.TITLE, (evt) => this.openGitHub(evt)) // Pass event directly
|
||||||
|
|
||||||
// add file menu
|
// add file menu
|
||||||
this.registerEvent(
|
this.registerEvent(
|
||||||
this.app.workspace.on('file-menu', (menu, file) => {
|
this.app.workspace.on('file-menu', (menu, file) => {
|
||||||
menu.addItem((item) => {
|
// Only add if the file is potentially in a git repo
|
||||||
item
|
if (file instanceof TFile) {
|
||||||
.setTitle(CONSTS.TITLE)
|
menu.addItem((item) => {
|
||||||
.setIcon(CONSTS.TITLE_ID)
|
item
|
||||||
.onClick((e) => this.openGitHub(e as MouseEvent, true));
|
.setTitle(CONSTS.TITLE)
|
||||||
});
|
.setIcon(CONSTS.TITLE_ID)
|
||||||
|
.onClick((e) => this.openGitHub(e as MouseEvent, file)) // Pass file context
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
|
|
||||||
// add editormenu
|
// add editor menu
|
||||||
this.registerEvent(
|
this.registerEvent(
|
||||||
this.app.workspace.on("editor-menu", (menu, editor, view) => {
|
this.app.workspace.on("editor-menu", (menu, editor, view) => {
|
||||||
menu.addItem((item) => {
|
// Only add if the file is potentially in a git repo
|
||||||
item
|
if (view?.file) {
|
||||||
.setTitle(CONSTS.TITLE)
|
menu.addItem((item) => {
|
||||||
.setIcon(CONSTS.TITLE_ID)
|
item
|
||||||
.onClick((e) => this.openGitHub(e as MouseEvent, true));
|
.setTitle(CONSTS.TITLE)
|
||||||
});
|
.setIcon(CONSTS.TITLE_ID)
|
||||||
|
.onClick((e) => this.openGitHub(e as MouseEvent, view.file as TFile)) // Pass file context
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
|
|
||||||
// Perform additional things with the ribbon
|
|
||||||
ribbonIconEl.addClass('my-plugin-ribbon-class')
|
|
||||||
|
|
||||||
// Bottom right open-file-in-github button
|
|
||||||
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
|
|
||||||
// const statusBarItemEl = this.addStatusBarItem();
|
|
||||||
// const parser = new DOMParser()
|
|
||||||
// const svgDoc = parser.parseFromString(customIcon(24, 80), 'image/svg+xml')
|
|
||||||
// const svgElement = svgDoc.documentElement as unknown as HTMLElement
|
|
||||||
// svgElement.setAttribute('width', '16');
|
|
||||||
// svgElement.setAttribute('height', '16');
|
|
||||||
// statusBarItemEl.appendChild(svgElement);
|
|
||||||
// statusBarItemEl.addEventListener('click', e => openGitHub(e, true))
|
|
||||||
|
|
||||||
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
|
|
||||||
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
onunload() { }
|
|
||||||
|
|
||||||
async loadSettings() {
|
|
||||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData())
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveSettings() {
|
|
||||||
await this.saveData(this.settings)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* open github in browser
|
* open github in browser
|
||||||
* @param evt
|
* @param evt The mouse event that triggered the action
|
||||||
* @param openFile
|
* @param file The file context (optional), if triggered from file/editor menu
|
||||||
*/
|
*/
|
||||||
openGitHub = async (evt: MouseEvent, openFile?: boolean) => {
|
openGitHub = async (evt: MouseEvent, file?: TFile) => {
|
||||||
try {
|
try {
|
||||||
|
// Determine the file path to use
|
||||||
|
const targetFile = file // this.app.workspace.getActiveFile()
|
||||||
|
const openFileInRepo = !!targetFile // True if we have a file context
|
||||||
|
|
||||||
const repoUrl = await this.getGitHubRepoUrl()
|
const repoUrl = await this.getGitHubRepoUrl()
|
||||||
if (repoUrl) {
|
if (!repoUrl) {
|
||||||
const fileRelativePath = this.app.workspace.getActiveFile()
|
// Notice is shown within getGitHubRepoUrl if needed
|
||||||
if (openFile) {
|
return
|
||||||
if (fileRelativePath?.path) {
|
|
||||||
let branch = await this.getCurrentBranch(fileRelativePath?.path)
|
|
||||||
if (!branch) {
|
|
||||||
branch = 'main'
|
|
||||||
}
|
|
||||||
const fileUrl = `${repoUrl}/blob/${branch}/${fileRelativePath?.path}`
|
|
||||||
window.open(fileUrl, '_blank')
|
|
||||||
} else {
|
|
||||||
new Notice('Not found file relative path!')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
window.open(repoUrl, '_blank')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
new Notice('Could not determine GitHub repository URL.')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (openFileInRepo && targetFile) {
|
||||||
|
// Use targetFile.path which is relative to the vault root
|
||||||
|
const filePathRelative = targetFile.path
|
||||||
|
let branch = await this.getCurrentBranch()
|
||||||
|
if (!branch) {
|
||||||
|
// Default to 'main' if branch detection fails
|
||||||
|
branch = 'main'
|
||||||
|
new Notice("Could not detect current branch, defaulting to 'main'.")
|
||||||
|
}
|
||||||
|
const fileUrl = `${repoUrl}/blob/${branch}/${filePathRelative}`
|
||||||
|
window.open(fileUrl, '_blank')
|
||||||
|
|
||||||
|
} else if (!openFileInRepo) {
|
||||||
|
// If no file context (e.g., ribbon click), open the repo root
|
||||||
|
window.open(repoUrl, '_blank')
|
||||||
|
} else {
|
||||||
|
// Case where we intended to open a file, but targetFile is null/undefined
|
||||||
|
new Notice('No active file selected to open in GitHub.')
|
||||||
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
new Notice('Failed to open GitHub repository.')
|
new Notice('Failed to open GitHub repository.')
|
||||||
console.error(err)
|
console.error("OpenInGitHubPlugin Error:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the GitHub repository URL for the current vault.
|
* Get the GitHub repository URL for the current vault using Vault adapter.
|
||||||
*/
|
*/
|
||||||
async getGitHubRepoUrl(): Promise<string | null> {
|
async getGitHubRepoUrl(): Promise<string | null> {
|
||||||
const vaultPath = this.getVaultBasePath()
|
if (!this.vaultAdapter) {
|
||||||
if (!vaultPath) {
|
new Notice('Vault adapter not available. Cannot access file system.')
|
||||||
new Notice('This feature is only supported in the desktop app.')
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const gitDir = path.join(vaultPath, '.git')
|
try {
|
||||||
if (!fs.existsSync(gitDir)) {
|
// Check if .git directory exists at the vault root
|
||||||
new Notice('No .git directory found in the vault.')
|
const gitDirExists = await this.vaultAdapter.exists(GIT_DIR)
|
||||||
|
if (!gitDirExists) {
|
||||||
|
new Notice('No .git directory found in the vault root.')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if .git/config file exists
|
||||||
|
const configPathExists = await this.vaultAdapter.exists(GIT_CONFIG_PATH)
|
||||||
|
if (!configPathExists) {
|
||||||
|
new Notice('No .git/config file found.')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the config file content
|
||||||
|
const configContent = await this.vaultAdapter.read(GIT_CONFIG_PATH)
|
||||||
|
|
||||||
|
// Find the remote "origin" URL
|
||||||
|
const remoteMatch = configContent.match(/\[remote\s+"origin"\][^[]*?\n\s*url\s*=\s*(.*)/)
|
||||||
|
if (!remoteMatch || !remoteMatch[1]) {
|
||||||
|
new Notice('No remote "origin" URL found in .git/config.')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteUrl = remoteMatch[1].trim()
|
||||||
|
const githubUrl = this.convertToGitHubUrl(remoteUrl)
|
||||||
|
|
||||||
|
if (!githubUrl) {
|
||||||
|
new Notice('Could not parse GitHub URL from remote "origin".')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return githubUrl
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("OpenInGitHubPlugin: Error accessing git config:", error)
|
||||||
|
new Notice("Error accessing git configuration.")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const configPath = path.join(gitDir, 'config')
|
|
||||||
if (!fs.existsSync(configPath)) {
|
|
||||||
new Notice('No .git/config file found.')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const configContent = fs.readFileSync(configPath, 'utf-8')
|
|
||||||
const remoteMatch = configContent.match(/\[remote "origin"\][\s\S]*?url\s*=\s*(.*)/)
|
|
||||||
if (!remoteMatch) {
|
|
||||||
new Notice('No remote "origin" found in .git/config.')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const remoteUrl = remoteMatch[1].trim()
|
|
||||||
return this.convertToGitHubUrl(remoteUrl)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a Git remote URL to a GitHub URL.
|
* Convert a Git remote URL to a GitHub web URL.
|
||||||
*/
|
*/
|
||||||
convertToGitHubUrl(remoteUrl: string): string | null {
|
convertToGitHubUrl(remoteUrl: string): string | null {
|
||||||
// Handle HTTPS URLs (e.g., https://github.com/user/repo.git)
|
// Handle HTTPS URLs (e.g., https://github.com/user/repo.git)
|
||||||
if (remoteUrl.startsWith('https://')) {
|
let match = remoteUrl.match(/^https?:\/\/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/)
|
||||||
return remoteUrl.replace(/\.git$/, '')
|
if (match) {
|
||||||
|
return `https://github.com/${match[1]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle SSH URLs (e.g., git@github.com:user/repo.git)
|
// Handle SSH URLs (e.g., git@github.com:user/repo.git)
|
||||||
if (remoteUrl.startsWith('git@')) {
|
match = remoteUrl.match(/^git@github\.com:([^/]+\/[^/]+?)(?:\.git)?$/)
|
||||||
return remoteUrl.replace(':', '/').replace('git@', 'https://').replace(/\.git$/, '')
|
if (match) {
|
||||||
|
return `https://github.com/${match[1]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null // Return null if format is not recognized
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the base path of the vault (only works in desktop app).
|
* Get the current branch of the Git repository using Vault adapter.
|
||||||
*/
|
*/
|
||||||
getVaultBasePath(): string | null {
|
async getCurrentBranch(): Promise<string | null> {
|
||||||
// @ts-ignore
|
if (!this.vaultAdapter) {
|
||||||
if (this.app.vault.adapter?.getBasePath) {
|
new Notice('Vault adapter not available. Cannot access file system.')
|
||||||
// @ts-ignore
|
|
||||||
return this.app.vault.adapter.getBasePath()
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current branch of the Git repository.
|
|
||||||
*/
|
|
||||||
async getCurrentBranch(filePath: string): Promise<string | null> {
|
|
||||||
const vaultPath = this.getVaultBasePath()
|
|
||||||
if (!vaultPath) {
|
|
||||||
new Notice('This feature is only supported in the desktop app.')
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const gitDir = path.join(vaultPath, '.git')
|
try {
|
||||||
if (!fs.existsSync(gitDir)) {
|
// Check if .git directory exists
|
||||||
new Notice('No .git directory found in the vault.')
|
const gitDirExists = await this.vaultAdapter.exists(GIT_DIR)
|
||||||
|
if (!gitDirExists) {
|
||||||
|
// No notice here, as getGitHubRepoUrl would likely fail first
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if .git/HEAD file exists
|
||||||
|
const headPathExists = await this.vaultAdapter.exists(GIT_HEAD_PATH)
|
||||||
|
if (!headPathExists) {
|
||||||
|
new Notice('No .git/HEAD file found. Cannot determine branch.')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the HEAD file content
|
||||||
|
const headContent = (await this.vaultAdapter.read(GIT_HEAD_PATH)).trim()
|
||||||
|
|
||||||
|
// Example HEAD content: "ref: refs/heads/main" or "ref: refs/heads/feature/branch"
|
||||||
|
const branchMatch = headContent.match(/^ref:\s+refs\/heads\/(.*)/)
|
||||||
|
if (branchMatch && branchMatch[1]) {
|
||||||
|
return branchMatch[1] // Return the branch name
|
||||||
|
}
|
||||||
|
|
||||||
|
new Notice('Could not parse branch name from .git/HEAD.')
|
||||||
|
return null
|
||||||
|
} catch (error) {
|
||||||
|
console.error("OpenInGitHubPlugin: Error reading git HEAD:", error)
|
||||||
|
new Notice("Error reading git branch information.")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const headPath = path.join(gitDir, 'HEAD');
|
|
||||||
const headContent = fs.readFileSync(headPath, 'utf-8').trim();
|
|
||||||
|
|
||||||
// Example: ref: refs/heads/main
|
|
||||||
const branchMatch = headContent.match(/ref: refs\/heads\/(.*)/);
|
|
||||||
if (branchMatch) {
|
|
||||||
return branchMatch[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "open-in-github",
|
"id": "open-in-github",
|
||||||
"name": "Open in GitHub",
|
"name": "Open in GitHub",
|
||||||
"version": "1.0.0",
|
"version": "1.0.3",
|
||||||
"minAppVersion": "0.15.0",
|
"minAppVersion": "0.15.0",
|
||||||
"description": "Open the current project or file in github.com.",
|
"description": "Open the current project or file in github.com.",
|
||||||
"author": "Murphy Chen",
|
"author": "Murphy Chen",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "obsidian-sample-plugin",
|
"name": "obsidian-open-in-github-plugin",
|
||||||
"version": "1.0.0",
|
"version": "1.0.3",
|
||||||
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
"description": "This is an Obsidian plugin designed to open project or files directly in GitHub via your browser.",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node esbuild.config.mjs",
|
"dev": "node esbuild.config.mjs",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
/*
|
|
||||||
|
|
||||||
This CSS file will be included with your plugin, and
|
|
||||||
available in the app when your plugin is enabled.
|
|
||||||
|
|
||||||
If your plugin does not need CSS, delete this file.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"1.0.0": "0.15.0"
|
"1.0.3": "0.15.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue