Skip to content

Commit a3f859b

Browse files
committed
wip
1 parent 2afb758 commit a3f859b

9 files changed

Lines changed: 633 additions & 178 deletions

File tree

.vscode-test.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
// .vscode-test.mjs
22
import { defineConfig } from "@vscode/test-cli"
33

4-
export default defineConfig({ files: "out/test/**/*.test.js" })
4+
export default defineConfig({
5+
files: "out/test/**/*.test.js",
6+
workspaceFolder: ".",
7+
})

images/logo.sketch

999 Bytes
Binary file not shown.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,9 @@
472472
"lodash": "^4.17.21",
473473
"mz": "^2.7.0"
474474
},
475+
"extensionDependencies": [
476+
"vscode.git"
477+
],
475478
"packageManager": "yarn@1.22.22",
476479
"volta": {
477480
"node": "18.20.3",

src/extension.ts

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as git from "./git"
33
import { providers, IUrlInfo, createSha, createBranch } from "./providers"
44
import { getRelativeFilePath } from "./utils"
55
import { openFileFromGitHubUrl } from "./openfromUrl"
6+
import { Repository } from "./vscode-git"
67

78
const COMMANDS: [string, IGithubinator][] = [
89
[
@@ -77,18 +78,23 @@ export interface IGithubinatorConfig {
7778
}
7879
}
7980

81+
export let outputChannel: vscode.OutputChannel
82+
8083
export function activate(context: vscode.ExtensionContext) {
8184
console.log("githubinator.active.start")
85+
outputChannel = vscode.window.createOutputChannel("GitHubinator")
86+
context.subscriptions.push(outputChannel)
8287
COMMANDS.forEach(([cmd, args]) => {
8388
const disposable = vscode.commands.registerCommand(cmd, () =>
8489
githubinator(args),
8590
)
8691
context.subscriptions.push(disposable)
8792
})
88-
vscode.commands.registerCommand(
93+
const openFromUrlDisposable = vscode.commands.registerCommand(
8994
"githubinator.githubinatorOpenFromUrl",
9095
openFileFromGitHubUrl,
9196
)
97+
context.subscriptions.push(openFromUrlDisposable)
9298

9399
console.log("githubinator.active.complete")
94100
}
@@ -127,11 +133,15 @@ function mainBranches() {
127133
.get<string[]>("mainBranches", ["main"])
128134
}
129135

136+
/**
137+
* Search default main branch names for the first one that exists.
138+
*/
130139
async function findShaForBranches(
131-
gitDir: string,
140+
gitRepository: Repository,
141+
fileUri: vscode.Uri,
132142
): Promise<[string, string] | null> {
133143
for (let branch of mainBranches()) {
134-
const sha = await git.getSHAForBranch(gitDir, branch)
144+
const sha = await git.getSHAForBranch(gitRepository, branch)
135145
if (sha == null) {
136146
continue
137147
}
@@ -151,41 +161,41 @@ interface IGithubinator {
151161
openPR?: boolean
152162
compare?: boolean
153163
}
154-
async function githubinator({
155-
openUrl,
156-
copyToClipboard,
157-
blame,
158-
mainBranch,
159-
openRepo,
160-
permalink,
161-
history,
162-
openPR,
163-
compare,
164-
}: IGithubinator) {
165-
console.log("githubinator.call")
164+
async function githubinator(options: IGithubinator) {
165+
const {
166+
openUrl,
167+
copyToClipboard,
168+
blame,
169+
mainBranch,
170+
openRepo,
171+
permalink,
172+
history,
173+
openPR,
174+
compare,
175+
} = options
176+
outputChannel.appendLine(
177+
"githubinator called with options: " + JSON.stringify(options),
178+
)
166179
const editorConfig = getEditorInfo()
167180
if (!editorConfig.uri) {
168-
return err("could not find file")
181+
return err("Could not find file for current editor.")
169182
}
183+
const fileUri = editorConfig.uri
170184

171-
const gitDirectories = git.dir(editorConfig.uri.fsPath)
172-
173-
if (gitDirectories == null) {
174-
return err("Could not find .git directory.")
185+
const gitRepository = await git.getRepo(fileUri)
186+
if (!gitRepository) {
187+
return err("Could not find git repository for file.")
175188
}
176189

177-
const gitDir = gitDirectories.git
178-
const repoDir = gitDirectories.repository
179-
180190
let headBranch: [string, string | null] | null = null
181191
if (mainBranch) {
182-
const res = await findShaForBranches(gitDir)
192+
const res = await findShaForBranches(gitRepository, fileUri)
183193
if (res == null) {
184194
return err(`Could not find SHA for branch in ${mainBranches()}`)
185195
}
186196
headBranch = res
187197
} else {
188-
headBranch = await git.head(gitDir)
198+
headBranch = await git.head(gitRepository)
189199
}
190200
if (headBranch == null) {
191201
return err("Could not find HEAD.")
@@ -209,7 +219,7 @@ async function githubinator({
209219
const parsedUrl = await new provider(
210220
providersConfig,
211221
globalDefaultRemote,
212-
(remote) => git.origin(gitDir, remote),
222+
(remote) => git.origin(gitRepository, remote),
213223
).getUrls({
214224
selection,
215225
// priority: permalink > branch > branch from HEAD
@@ -220,7 +230,7 @@ async function githubinator({
220230
? createSha(head)
221231
: createBranch(branchName),
222232
relativeFilePath: editorConfig.fileName
223-
? getRelativeFilePath(repoDir, editorConfig.fileName)
233+
? getRelativeFilePath(gitRepository.rootUri, editorConfig.fileName)
224234
: null,
225235
})
226236
if (parsedUrl != null) {
@@ -232,7 +242,7 @@ async function githubinator({
232242
}
233243

234244
if (urls == null) {
235-
return err("Could not find provider for repo.")
245+
return err("Could not find remote for repository.")
236246
}
237247

238248
let url = compare

src/git.ts

Lines changed: 56 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,77 @@
1-
import * as path from "path"
2-
import * as fs from "mz/fs"
3-
import * as ini from "ini"
1+
import * as vscode from "vscode"
2+
import { outputChannel } from "./extension"
3+
import { GitExtension, Remote, Repository } from "./vscode-git"
44

5-
interface IRemote {
6-
fetch: string
7-
url?: string
5+
async function getGitAPI() {
6+
const gitExtension =
7+
vscode.extensions.getExtension<GitExtension>("vscode.git")
8+
if (!gitExtension) return null
9+
const exports = await gitExtension.activate()
10+
return exports.getAPI(1)
811
}
912

10-
interface IGitDirectories {
11-
git: string
12-
repository: string
13+
export async function getRepo(fileUri: vscode.Uri) {
14+
const api = await getGitAPI()
15+
if (!api) {
16+
vscode.window.showErrorMessage("Git API not available")
17+
return null
18+
}
19+
const existing = api.getRepository(fileUri)
20+
if (existing) return existing
21+
outputChannel.appendLine("No repository found, triggering git refresh...")
22+
await vscode.commands.executeCommand("git.refresh")
23+
const afterRefresh = api.getRepository(fileUri)
24+
if (afterRefresh) return afterRefresh
25+
// Repos are discovered asynchronously after activation; wait for one to open
26+
return new Promise<ReturnType<typeof api.getRepository>>((resolve) => {
27+
const timeout = setTimeout(() => {
28+
disposable.dispose()
29+
resolve(null)
30+
outputChannel.appendLine("Hit 5 second timeout for repository to load.")
31+
}, 5000)
32+
const disposable = api.onDidOpenRepository(() => {
33+
const repo = api.getRepository(fileUri)
34+
if (repo) {
35+
outputChannel.appendLine("Found repository via onDidOpenRepository.")
36+
clearTimeout(timeout)
37+
disposable.dispose()
38+
resolve(repo)
39+
}
40+
})
41+
})
1342
}
1443

1544
export async function origin(
16-
gitDir: string,
45+
repository: Repository,
1746
remote: string,
1847
): Promise<string | null> {
19-
const configPath = path.resolve(gitDir, "config")
20-
if (!(await fs.exists(configPath))) {
21-
return null
22-
}
23-
const configFileData = await fs.readFile(configPath, { encoding: "utf-8" })
24-
const parsedConfig = ini.parse(configFileData)
25-
for (const [key, value] of Object.entries(parsedConfig)) {
26-
if (key.startsWith('remote "')) {
27-
const origin = key.replace(/^remote "/, "").replace(/"$/, "")
28-
if (origin === remote) {
29-
const url = (value as IRemote).url
30-
return url || null
31-
}
32-
}
33-
}
34-
35-
return null
48+
const found = repository.state.remotes.find((r: Remote) => r.name === remote)
49+
return found?.fetchUrl ?? found?.pushUrl ?? null
3650
}
3751

38-
/** Get the SHA for a ref */
3952
export async function getSHAForBranch(
40-
gitDir: string,
53+
repository: Repository,
4154
branchName: string,
4255
): Promise<string | null> {
43-
const refName = `refs/heads/${branchName}`
44-
// check for normal ref
45-
const refPath = path.resolve(gitDir, refName)
46-
if (await fs.exists(refPath)) {
47-
return await fs.readFile(refPath, {
48-
encoding: "utf-8",
49-
})
50-
}
51-
// check packed-refs
52-
const packedRefPath = path.resolve(gitDir, "packed-refs")
53-
if (await fs.exists(packedRefPath)) {
54-
const packRefs = await fs.readFile(packedRefPath, {
55-
encoding: "utf-8",
56-
})
57-
58-
for (const x of packRefs.split("\n")) {
59-
const [sha, refPath] = x.split(" ") as [
60-
string | undefined,
61-
string | undefined,
62-
]
63-
if (sha && refPath && refPath.trim() === refName.trim()) {
64-
return sha
65-
}
66-
}
56+
try {
57+
const branch = await repository.getBranch(branchName)
58+
return branch.commit ?? null
59+
} catch {
60+
return null
6761
}
68-
return null
6962
}
7063

71-
/** Get the current SHA and branch from HEAD for a git directory */
7264
export async function head(
73-
gitDir: string,
65+
repository: Repository,
7466
): Promise<[string, string | null] | null> {
75-
const headPath = path.resolve(gitDir, "HEAD")
76-
if (!(await fs.exists(headPath))) {
77-
return null
78-
}
79-
const headFileData = await fs.readFile(headPath, { encoding: "utf-8" })
80-
if (!headFileData) {
81-
return null
82-
}
83-
// If we're not on a branch, headFileData will be of the form:
84-
// `3c0cc80bbdb682f6e9f65b4c9659ca21924aad4`
85-
// If we're on a branch, it will be `ref: refs/heads/my_branch_name`
86-
const [maybeSha, maybeHeadInfo] = headFileData.split(" ") as [
87-
string,
88-
string | undefined,
89-
]
90-
if (maybeHeadInfo == null) {
91-
return [maybeSha.trim(), null]
92-
}
93-
const branchName = maybeHeadInfo.trim().replace("refs/heads/", "")
94-
const sha = await getSHAForBranch(gitDir, branchName)
95-
if (sha == null) {
96-
return null
97-
}
98-
return [sha.trim(), branchName]
99-
}
100-
101-
export function dir(filePath: string) {
102-
return walkUpDirectories(filePath, ".git")
67+
const repoHead = repository.state.HEAD
68+
if (!repoHead?.commit) return null
69+
return [repoHead.commit, repoHead.name ?? null]
10370
}
10471

105-
function walkUpDirectories(
106-
file_path: string,
107-
file_or_folder: string,
108-
): IGitDirectories | null {
109-
let directory = file_path
110-
while (true) {
111-
const newPath = path.resolve(directory, file_or_folder)
112-
if (fs.existsSync(newPath)) {
113-
if (fs.lstatSync(newPath).isFile()) {
114-
const submoduleMatch = fs
115-
.readFileSync(newPath, "utf8")
116-
.match(/gitdir: (.+)/)
117-
118-
if (submoduleMatch) {
119-
return {
120-
git: path.resolve(path.join(directory, submoduleMatch[1])),
121-
repository: directory,
122-
}
123-
} else {
124-
return null
125-
}
126-
} else {
127-
return {
128-
git: newPath,
129-
repository: directory,
130-
}
131-
}
132-
}
133-
const newDirectory = path.dirname(directory)
134-
if (newDirectory === directory) {
135-
return null
136-
}
137-
directory = newDirectory
138-
}
72+
export async function dir(
73+
repository: Repository,
74+
): Promise<{ git: string; repository: string } | null> {
75+
const root = repository.rootUri.fsPath
76+
return { git: root, repository: root }
13977
}

0 commit comments

Comments
 (0)