Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cody: Add support for more git clone urls and remove reload on codebase update #51274

Merged
merged 2 commits into from
May 2, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions client/cody/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ All notable changes to Sourcegraph Cody will be documented in this file.

### Added

- Updating `cody.codebase` does not require reloading VS Code

### Fixed

- Fixes an issue where code blocks were unexpectedly escaped [pull/51247](https://github.com/sourcegraph/sourcegraph/pull/51247)

### Changed

- Replace `Cody: Set Access Token` command with `Cody: Sign in`

## [0.0.9]

### Added
Expand Down
54 changes: 24 additions & 30 deletions client/cody/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,84 +99,69 @@
},
{
"command": "cody.recipe.explain-code",
"title": "Ask Cody: Explain Code in Detail",
"when": "cody.activated"
"title": "Ask Cody: Explain Code in Detail"
},
{
"command": "cody.recipe.explain-code-high-level",
"title": "Ask Cody: Explain Code at a High Level",
"when": "cody.activated"
"title": "Ask Cody: Explain Code at a High Level"
},
{
"command": "cody.recipe.generate-unit-test",
"title": "Ask Cody: Generate Unit Test",
"when": "cody.activated"
"title": "Ask Cody: Generate Unit Test"
},
{
"command": "cody.recipe.generate-docstring",
"title": "Ask Cody: Generate Docstring",
"when": "cody.activated"
"title": "Ask Cody: Generate Docstring"
},
{
"command": "cody.recipe.translate-to-language",
"title": "Ask Cody: Translate to Language",
"when": "cody.activated"
"title": "Ask Cody: Translate to Language"
},
{
"command": "cody.recipe.git-history",
"title": "Ask Cody: Summarize Recent Code Changes",
"when": "cody.activated"
"title": "Ask Cody: Summarize Recent Code Changes"
},
{
"command": "cody.recipe.improve-variable-names",
"title": "Ask Cody: Improve Variable Names",
"when": "cody.activated"
"title": "Ask Cody: Improve Variable Names"
},
{
"command": "cody.recipe.fixup",
"title": "Cody: Fixup",
"when": "cody.activated"
"title": "Cody: Fixup"
},
{
"command": "cody.set-access-token",
"title": "Cody: Set Access Token",
"when": "!cody.activated"
"title": "Cody: Set Access Token"
},
{
"command": "cody.delete-access-token",
"title": "Cody: Delete Access Token",
"when": "cody.activated"
"title": "Cody: Sign out"
},
{
"command": "cody.experimental.suggest",
"title": "Cody: View Suggestions",
"when": "cody.activated"
"title": "Cody: View Suggestions"
},
{
"command": "cody.settings",
"title": "Cody: Settings",
"group": "Cody",
"icon": "$(gear)",
"when": "cody.activated"
"icon": "$(gear)"
},
{
"command": "cody.focus",
"title": "Cody: Sign In to Use Cody",
"when": "!cody.activated"
"title": "Cody: Sign In"
},
{
"command": "cody.interactive.clear",
"title": "Cody: Clear & Restart Chat Session",
"group": "Cody",
"icon": "$(clear-all)",
"when": "cody.activated"
"icon": "$(clear-all)"
},
{
"command": "cody.history",
"title": "Cody: Chat History",
"group": "Cody",
"icon": "$(history)",
"when": "cody.activated"
"icon": "$(history)"
}
],
"keybindings": [
Expand Down Expand Up @@ -227,6 +212,15 @@
{
"command": "cody.recipe.fixup",
"when": "cody.activated"
},
{
"command": "cody.set-access-token",
"when": "false"
},
{
"command": "cody.focus",
"title": "Cody: Sign In",
"when": "!cody.activated"
}
],
"editor/context": [
Expand Down
30 changes: 30 additions & 0 deletions client/cody/src/chat/ChatViewProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ describe('convertGitCloneURLToCodebaseName', () => {
)
})

test('converts Bitbucket HTTPS URL', () => {
expect(convertGitCloneURLToCodebaseName('https://username@bitbucket.org/sourcegraph/sourcegraph.git')).toEqual(
'bitbucket.org/sourcegraph/sourcegraph'
)
})

test('converts Bitbucket SSH URL', () => {
expect(convertGitCloneURLToCodebaseName('git@bitbucket.sgdev.org:sourcegraph/sourcegraph.git')).toEqual(
'bitbucket.sgdev.org/sourcegraph/sourcegraph'
)
})

test('converts GitLab SSH URL', () => {
expect(convertGitCloneURLToCodebaseName('git@gitlab.com:sourcegraph/sourcegraph.git')).toEqual(
'gitlab.com/sourcegraph/sourcegraph'
Expand All @@ -31,6 +43,24 @@ describe('convertGitCloneURLToCodebaseName', () => {
)
})

test('converts GitHub SSH URL with Git', () => {
expect(convertGitCloneURLToCodebaseName('git@github.com:sourcegraph/sourcegraph.git')).toEqual(
'github.com/sourcegraph/sourcegraph'
)
})

test('converts Eriks SSH Alias URL', () => {
expect(convertGitCloneURLToCodebaseName('github:sourcegraph/sourcegraph')).toEqual(
'github.com/sourcegraph/sourcegraph'
)
})

test('converts HTTP URL', () => {
expect(convertGitCloneURLToCodebaseName('http://github.com/sourcegraph/sourcegraph')).toEqual(
'github.com/sourcegraph/sourcegraph'
)
})

test('returns null for invalid URL', () => {
expect(convertGitCloneURLToCodebaseName('invalid')).toEqual(null)
})
Expand Down
95 changes: 45 additions & 50 deletions client/cody/src/chat/ChatViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { isError } from '@sourcegraph/cody-shared/src/utils'

import { View } from '../../webviews/NavBar'
import { LocalStorage } from '../command/LocalStorageProvider'
import { updateConfiguration } from '../configuration'
import { getFullConfig, updateConfiguration } from '../configuration'
import { logEvent } from '../event-logger'
import { LocalKeywordContextFetcher } from '../keyword-context/local-keyword-context-fetcher'
import { CODY_ACCESS_TOKEN_SECRET, SecretStorage } from '../secret-storage'
Expand Down Expand Up @@ -97,6 +97,13 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
this.disposables.push(
vscode.window.onDidChangeActiveTextEditor(async () => {
await this.updateCodebaseContext()
}),
vscode.workspace.onDidChangeConfiguration(async () => {
this.config = await getFullConfig(this.secretStorage)
const newCodebaseContext = await getCodebaseContext(this.config, this.rgPath, this.editor)
if (newCodebaseContext) {
this.codebaseContext = newCodebaseContext
}
})
)
}
Expand Down Expand Up @@ -378,20 +385,10 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
return Promise.resolve()
},
onTurnComplete: () => {
console.log({ text })
console.log(text.split('\n'))
console.log(text.split('\n').slice(0, 3))
console.log(
text
.split('\n')
.slice(3)
.map(line => line.trim().replace(/^-/, '').trim())
)
const suggestions = text
.split('\n')
.slice(0, 3)
.map(line => line.trim().replace(/^-/, '').trim())
console.log({ suggestions })
this.sendSuggestions(suggestions)
return Promise.resolve()
},
Expand Down Expand Up @@ -516,6 +513,8 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
*/
private publishConfig(): void {
const send = async (): Promise<void> => {
// update codebase context on configuration change
void this.updateCodebaseContext()
// check if new configuration change is valid or not
// log user out if config is invalid
const isAuthed = await isValidLogin({
Expand Down Expand Up @@ -677,42 +676,46 @@ async function fileExists(filePath: string): Promise<boolean> {
}

// Converts a git clone URL to the codebase name that includes the slash-separated code host, owner, and repository name
// This should captures:
// - "github:sourcegraph/sourcegraph"
abeatrix marked this conversation as resolved.
Show resolved Hide resolved
// - "https://github.com/sourcegraph/deploy-sourcegraph-k8s.git"
// - "git@github.com:sourcegraph/sourcegraph.git"
export function convertGitCloneURLToCodebaseName(cloneURL: string): string | null {
let parsed: URL | null = null
if (cloneURL.startsWith('git@')) {
// Handle Git SSH URL format
if (!cloneURL) {
console.error(`Unable to determine the git clone URL for this workspace.\ngit output: ${cloneURL}`)
return null
}
try {
const uri = new URL(cloneURL.replace('git@', ''))
// Handle common Git SSH URL format
const match = cloneURL.match(/git@([^:]+):([\w-]+)\/([\w-]+)(\.git)?/)
if (match) {
if (cloneURL.startsWith('git@') && match) {
const host = match[1]
const owner = match[2]
const repo = match[3]
return `${host}/${owner}/${repo}`
}
} else {
// Handle HTTP/HTTPS URL format
try {
parsed = new URL(cloneURL)
} catch {
return null
// Handle GitHub URLs
if (uri.protocol.startsWith('github') || uri.href.startsWith('github')) {
return `github.com/${uri.pathname.replace('.git', '')}`
}
// Handle GitLab URLs
if (uri.protocol.startsWith('gitlab') || uri.href.startsWith('gitlab')) {
return `gitlab.com/${uri.pathname.replace('.git', '')}`
}
// Handle HTTPS URLs
if (uri.protocol.startsWith('http') && uri.hostname && uri.pathname) {
return `${uri.hostname}${uri.pathname.replace('.git', '')}`
}
// Generic URL
if (uri.hostname && uri.pathname) {
return `${uri.hostname}${uri.pathname.replace('.git', '')}`
}
}

if (!parsed) {
return null
} catch (error) {
console.error(`Cody could not extract repo name from clone URL ${cloneURL}:`, error)
return null
}

const host = parsed.host
const path = parsed.pathname

// Handle GitHub/GitLab URL format
if (host.includes('gitlab.com') || host.includes('github.com')) {
const [owner, repo] = path.slice(1).split('/')
return `${host}/${owner}/${repo}`.replace(/.git$/, '')
}

// Generic fallback - assume URL path contains owner/repo
const [owner, repo] = path.slice(1).split('/')
return `${host}/${owner}/${repo}`
}

async function getCodebaseContext(config: Config, rgPath: string, editor: Editor): Promise<CodebaseContext | null> {
Expand All @@ -721,26 +724,18 @@ async function getCodebaseContext(config: Config, rgPath: string, editor: Editor
if (!workspaceRoot) {
return null
}

const gitCommand = spawnSync('git', ['remote', 'get-url', 'origin'], { cwd: workspaceRoot })
const gitOutput = gitCommand.stdout.toString().trim()
if (!gitOutput) {
void vscode.window.showErrorMessage(
`Unable to determine the git clone URL for this workspace.\ngit output: ${gitOutput}`
)
return null
}

// Get repository name from git clone URL
const codebase = convertGitCloneURLToCodebaseName(gitOutput)
// Get codebase from config or fallback to getting repository name from git clone URL
const codebase = config.codebase || convertGitCloneURLToCodebaseName(gitOutput)
if (!codebase) {
void vscode.window.showErrorMessage(`could not extract repo name from clone URL ${gitOutput}`)
return null
}
// Check if repo is embedded in endpoint
const repoId = await client.getRepoIdIfEmbeddingExists(codebase)
if (isError(repoId)) {
const errorMessage = `Cody could not find embeddings for '${codebase}' on your Sourcegraph instance.\n`
void vscode.window.showErrorMessage(errorMessage)
const infoMessage = `Cody could not find embeddings for '${codebase}' on your Sourcegraph instance.\n`
console.info(infoMessage)
return null
}

Expand Down
13 changes: 12 additions & 1 deletion client/cody/src/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import * as vscode from 'vscode'

import type { ConfigurationUseContext, Configuration } from '@sourcegraph/cody-shared/src/configuration'
import type {
ConfigurationUseContext,
Configuration,
ConfigurationWithAccessToken,
} from '@sourcegraph/cody-shared/src/configuration'

import { SecretStorage, getAccessToken } from './secret-storage'

/**
* All configuration values, with some sanitization performed.
Expand Down Expand Up @@ -39,3 +45,8 @@ const codyConfiguration = vscode.workspace.getConfiguration('cody')
export async function updateConfiguration(configKey: string, configValue: string): Promise<void> {
await codyConfiguration.update(configKey, configValue, vscode.ConfigurationTarget.Global)
}

export const getFullConfig = async (secretStorage: SecretStorage): Promise<ConfigurationWithAccessToken> => {
const config = getConfiguration(vscode.workspace.getConfiguration())
return { ...config, accessToken: await getAccessToken(secretStorage) }
}
7 changes: 2 additions & 5 deletions client/cody/src/external-services.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { window } from 'vscode'

import { ChatClient } from '@sourcegraph/cody-shared/src/chat/chat'
import { CodebaseContext } from '@sourcegraph/cody-shared/src/codebase-context'
import { ConfigurationWithAccessToken } from '@sourcegraph/cody-shared/src/configuration'
Expand Down Expand Up @@ -39,11 +37,10 @@ export async function configureExternalServices(

const repoId = initialConfig.codebase ? await client.getRepoId(initialConfig.codebase) : null
if (isError(repoId)) {
const errorMessage =
const infoMessage =
`Cody could not find the '${initialConfig.codebase}' repository on your Sourcegraph instance.\n` +
'Please check that the repository exists and is entered correctly in the cody.codebase setting.'
console.error(errorMessage)
void window.showErrorMessage(errorMessage)
console.info(infoMessage)
}
const embeddingsSearch = repoId && !isError(repoId) ? new SourcegraphEmbeddingsSearchClient(client, repoId) : null

Expand Down
Loading
Loading