Skip to content

Commit 204bd04

Browse files
committed
chore: initial commit
0 parents  commit 204bd04

16 files changed

+6775
-0
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
dist/
2+
node_modules/
3+
coverage/
4+
.cache/

.prettierignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package.json
2+
package-lock.json
3+
dist/
4+
.cache/

.travis.yml

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
language: node_js
2+
node_js: '10'
3+
cache:
4+
directories:
5+
- ~/.npm
6+
env:
7+
global:
8+
- FORCE_COLOR=1
9+
jobs:
10+
include:
11+
- stage: test
12+
script:
13+
- npm run prettier
14+
- npm run tslint
15+
- npm run build
16+
- npm test
17+
- npm run cover
18+
- nyc report --reporter json
19+
- 'bash <(curl -s https://codecov.io/bash)'
20+
stages:
21+
- test
22+
branches:
23+
only:
24+
- master

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2018 Sourcegraph
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# sourcegraph-git-extras
2+
3+
[![build](https://travis-ci.org/sourcegraph/sourcegraph-git-extras.svg?branch=master)](https://travis-ci.org/sourcegraph/sourcegraph-git-extras)
4+
[![codecov](https://codecov.io/gh/sourcegraph/sourcegraph-git-extras/branch/master/graph/badge.svg?token=c3KpMf1MaY)](https://codecov.io/gh/sourcegraph/sourcegraph-git-extras)
5+
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
6+
7+
A [Sourcegraph extension](https://github.com/sourcegraph/sourcegraph-extension-api) that adds useful features when viewing files in a Git repository on [Sourcegraph](https://sourcegraph.com), GitHub, GitLab, and other [supported code hosts](https://docs.sourcegraph.com/extensions):
8+
9+
- **Git: Show/hide blame**: toggles Git blame annotations with each line's last commit, author, date, etc.
10+
11+
[**🗃️ Source code**](https://github.com/sourcegraph/sourcegraph-git-extras)
12+
13+
[**➕ Add to Sourcegraph**](https://sourcegraph.com/extensions/sourcegraph/git-extras)

mocha.opts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
--recursive
2+
--watch-extensions ts
3+
--timeout 200
4+
src/**/*.test.ts

package.json

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
{
2+
"name": "git-extras",
3+
"publisher": "sourcegraph",
4+
"title": "Git extras",
5+
"description": "A Sourcegraph extension that adds useful features when viewing files in a Git repository on Sourcegraph, GitHub, GitLab, and other supported code hosts.",
6+
"activationEvents": [
7+
"*"
8+
],
9+
"contributes": {
10+
"actions": [
11+
{
12+
"id": "git.blame.toggle",
13+
"command": "updateConfiguration",
14+
"commandArguments": [
15+
[
16+
"git.blame.lineDecorations"
17+
],
18+
"${!config.git.blame.lineDecorations}",
19+
null,
20+
"json"
21+
],
22+
"category": "Git",
23+
"title": "${config.git.blame.lineDecorations && \"Hide\" || \"Show\"} blame",
24+
"actionItem": {
25+
"label": "Blame",
26+
"description": "${config.git.blame.lineDecorations && \"Hide\" || \"Show\"} Git blame line annotations"
27+
}
28+
}
29+
],
30+
"menus": {
31+
"editor/title": [
32+
{
33+
"action": "git.blame.toggle",
34+
"when": "resource"
35+
}
36+
],
37+
"commandPalette": [
38+
{
39+
"action": "git.blame.toggle",
40+
"when": "resource"
41+
}
42+
]
43+
},
44+
"configuration": {
45+
"title": "Git extras",
46+
"properties": {
47+
"git.blame.lineDecorations": {
48+
"description": "Whether to show Git blame annotations at the end of each line.",
49+
"type": "boolean",
50+
"default": false
51+
}
52+
}
53+
}
54+
},
55+
"version": "0.0.0-DEVELOPMENT",
56+
"license": "MIT",
57+
"repository": {
58+
"type": "git",
59+
"url": "https://github.com/sourcegraph/sourcegraph-git-extras.git"
60+
},
61+
"files": [
62+
"dist"
63+
],
64+
"main": "dist/sourcegraph-git-extras.js",
65+
"scripts": {
66+
"prettier": "prettier '**/{*.{js?(on),ts?(x),scss},.*.js?(on)}' --write --list-different",
67+
"tslint": "tslint -c tslint.json -p tsconfig.json './src/*.ts?(x)' './*.ts?(x)'",
68+
"test": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha --require ts-node/register --require source-map-support/register --opts mocha.opts",
69+
"cover": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --require ts-node/register --require source-map-support/register --all mocha --opts mocha.opts --timeout 10000",
70+
"build": "parcel build --no-minify --out-file sourcegraph-git-extras.js src/extension.ts",
71+
"serve": "parcel serve --no-hmr --out-file sourcegraph-git-extras.js src/extension.ts",
72+
"sourcegraph:prepublish": "yarn run build"
73+
},
74+
"commitlint": {
75+
"extends": [
76+
"@commitlint/config-conventional"
77+
]
78+
},
79+
"nyc": {
80+
"include": [
81+
"src/**/*.ts?(x)"
82+
],
83+
"exclude": [
84+
"**/*.test.ts?(x)"
85+
],
86+
"extension": [
87+
".tsx",
88+
".ts"
89+
]
90+
},
91+
"browserslist": [
92+
"last 1 Chrome versions",
93+
"last 1 Firefox versions",
94+
"last 1 Edge versions",
95+
"last 1 Safari versions"
96+
],
97+
"devDependencies": {
98+
"@commitlint/cli": "^7.0.0",
99+
"@commitlint/config-conventional": "^7.0.1",
100+
"@sourcegraph/prettierrc": "^2.2.0",
101+
"@sourcegraph/tsconfig": "^3.0.0",
102+
"@sourcegraph/tslint-config": "^12.0.0",
103+
"@types/mocha": "^5.2.5",
104+
"@types/node": "^10.7.1",
105+
"husky": "^1.1.2",
106+
"mocha": "^5.2.0",
107+
"nyc": "^13.1.0",
108+
"parcel-bundler": "^1.9.7",
109+
"prettier": "^1.14.2",
110+
"source-map-support": "^0.5.9",
111+
"sourcegraph": "^18.3.0",
112+
"ts-node": "^7.0.1",
113+
"tslint": "^5.11.0",
114+
"typescript": "^3.0.1"
115+
},
116+
"dependencies": {
117+
"date-fns": "^2.0.0-alpha.24",
118+
"rxjs": "^6.3.2"
119+
},
120+
"husky": {
121+
"hooks": {
122+
"commit-msg": "commitlint -e $HUSKY_GIT_PARAMS"
123+
}
124+
}
125+
}

prettier.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('@sourcegraph/prettierrc')

src/blame.ts

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import formatDistanceStrict from 'date-fns/formatDistanceStrict'
2+
import * as sourcegraph from 'sourcegraph'
3+
import { Settings } from './extension'
4+
import { resolveURI } from './uri'
5+
6+
export async function getBlameDecorations(
7+
uri: string,
8+
settings: Settings
9+
): Promise<sourcegraph.TextDocumentDecoration[]> {
10+
if (!settings['git.blame.lineDecorations']) {
11+
return []
12+
}
13+
const hunks = await queryBlameHunks(uri)
14+
const now = Date.now()
15+
return hunks.map(
16+
hunk =>
17+
({
18+
range: new sourcegraph.Range(hunk.startLine - 1, 0, hunk.startLine - 1, 0),
19+
isWholeLine: true,
20+
after: {
21+
light: {
22+
color: 'rgba(0, 0, 25, 0.55)',
23+
backgroundColor: 'rgba(193, 217, 255, 0.65)',
24+
},
25+
dark: {
26+
color: 'rgba(235, 235, 255, 0.55)',
27+
backgroundColor: 'rgba(15, 43, 89, 0.65)',
28+
},
29+
contentText: `${truncate(hunk.author.person.displayName, 25)}, ${formatDistanceStrict(
30+
hunk.author.date,
31+
now,
32+
{
33+
addSuffix: true,
34+
}
35+
)}: • ${truncate(hunk.message, 45)}`,
36+
hoverMessage: `View commit ${truncate(hunk.rev, 7, '')}: ${truncate(hunk.message, 1000)}`,
37+
linkURL: hunk.commit.url,
38+
},
39+
} as sourcegraph.TextDocumentDecoration)
40+
)
41+
}
42+
43+
interface Hunk {
44+
startLine: number
45+
endLine: number
46+
author: {
47+
person: {
48+
displayName: string
49+
}
50+
date: string
51+
}
52+
rev: string
53+
message: string
54+
commit: {
55+
url: string
56+
}
57+
}
58+
59+
async function queryBlameHunks(uri: string): Promise<Hunk[]> {
60+
const { repo, rev, path } = resolveURI(uri)
61+
const { data, errors } = await sourcegraph.commands.executeCommand(
62+
'queryGraphQL',
63+
`
64+
query GitBlame($repo: String!, $rev: String!, $path: String!) {
65+
repository(name: $repo) {
66+
commit(rev: $rev) {
67+
blob(path: $path) {
68+
blame(startLine: 0, endLine: 0) {
69+
startLine
70+
endLine
71+
author {
72+
person {
73+
displayName
74+
}
75+
date
76+
}
77+
message
78+
rev
79+
commit {
80+
url
81+
}
82+
}
83+
}
84+
}
85+
}
86+
}
87+
`,
88+
{ repo, rev, path }
89+
)
90+
if (errors && errors.length > 0) {
91+
throw new Error(errors.join('\n'))
92+
}
93+
if (!data || !data.repository || !data.repository.commit || !data.repository.commit.blob) {
94+
throw new Error('no blame data is available (repository, commit, or path not found)')
95+
}
96+
return data.repository.commit.blob.blame
97+
}
98+
99+
function truncate(s: string, max: number, omission = '…'): string {
100+
if (s.length <= max) {
101+
return s
102+
}
103+
return `${s.slice(0, max)}${omission}`
104+
}

src/extension.test.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
describe('extension', () => {
2+
it('works', () => void 0)
3+
})

src/extension.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as sourcegraph from 'sourcegraph'
2+
import { getBlameDecorations } from './blame'
3+
4+
export interface Settings {
5+
['git.blame.lineDecorations']?: boolean
6+
}
7+
8+
export function activate(): void {
9+
function activeEditor(): sourcegraph.CodeEditor | undefined {
10+
return sourcegraph.app.activeWindow ? sourcegraph.app.activeWindow.visibleViewComponents[0] : undefined
11+
}
12+
13+
// When the configuration or current file changes, publish new decorations.
14+
//
15+
// TODO: Unpublish decorations on previously (but not currently) open files when settings changes, to avoid a
16+
// brief flicker of the old state when the file is reopened.
17+
async function decorate(editor: sourcegraph.CodeEditor | undefined = activeEditor()): Promise<void> {
18+
if (!editor) {
19+
return
20+
}
21+
const settings = sourcegraph.configuration.get<Settings>().value
22+
try {
23+
editor.setDecorations(null, await getBlameDecorations(editor.document.uri, settings))
24+
} catch (err) {
25+
console.error('Decoration error:', err)
26+
}
27+
}
28+
sourcegraph.configuration.subscribe(() => decorate())
29+
// TODO(sqs): Add a way to get notified when a new editor is opened (because we want to be able to pass an
30+
// `editor` to `updateDecorations`/`updateContext`, but this subscription just gives us a `doc`).
31+
sourcegraph.workspace.onDidOpenTextDocument.subscribe(() => decorate())
32+
}

src/uri.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Resolve a URI of the forms git://github.com/owner/repo?rev#path and file:///path to an absolute reference, using
3+
* the given base (root) URI.
4+
*/
5+
export function resolveURI(uri: string): { repo: string; rev: string; path: string } {
6+
const url = new URL(uri)
7+
if (url.protocol === 'git:') {
8+
return {
9+
repo: (url.host + url.pathname).replace(/^\/*/, '').toLowerCase(),
10+
rev: url.search.slice(1).toLowerCase(),
11+
path: url.hash.slice(1),
12+
}
13+
}
14+
throw new Error(`unrecognized URI: ${JSON.stringify(uri)} (supported URI schemes: git)`)
15+
}

src/util/memoizeAsync.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Creates a function that memoizes the async result of func. If the Promise is rejected, the result will not be
3+
* cached.
4+
*
5+
* @param toKey etermines the cache key for storing the result based on the first argument provided to the memoized
6+
* function
7+
*/
8+
export function memoizeAsync<P, T>(
9+
func: (params: P) => Promise<T>,
10+
toKey: (params: P) => string
11+
): (params: P) => Promise<T> {
12+
const cache = new Map<string, Promise<T>>()
13+
return (params: P) => {
14+
const key = toKey(params)
15+
const hit = cache.get(key)
16+
if (hit) {
17+
return hit
18+
}
19+
const p = func(params)
20+
p.then(null, () => cache.delete(key))
21+
cache.set(key, p)
22+
return p
23+
}
24+
}

0 commit comments

Comments
 (0)