├── .editorconfig
├── .eslintrc.js
├── .gitignore
├── LICENSE
├── README.md
├── assets
├── Bitbucket-blue.svg
├── GitHub-Mark.png
├── bitbucket-neutral.svg
└── global.scss
├── nuxt.config.js
├── nuxt.gh-pages.config.js
├── package-lock.json
├── package.json
├── pages
└── index.js
├── src
├── Commit.js
├── Diff.vue
├── GitHistory.vue
├── Header.vue
├── Prism.js
└── api
│ ├── Bitbucket.js
│ ├── GitHub.js
│ └── index.js
└── static
└── favicon.ico
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | browser: true,
5 | node: true
6 | },
7 | parserOptions: {
8 | parser : 'babel-eslint'
9 | },
10 | extends: [
11 | 'plugin:vue/recommended',
12 | 'standard'
13 | ],
14 | plugins: [
15 | 'vue',
16 | 'import'
17 | ],
18 | // add your custom rules here
19 | rules: {
20 | 'prefer-const': 'error',
21 | 'no-var': 'error',
22 | 'vue/no-v-html': 'off',
23 | 'prefer-template': 'error',
24 | 'vue/require-prop-types': 'off',
25 | 'vue/require-default-prop':'off',
26 | 'vue/html-closing-bracket-newline':'off',
27 | 'vue/max-attributes-per-line': ['error', {
28 | 'singleline': 4,
29 | 'multiline': {
30 | 'max': 4,
31 | 'allowFirstLine': true
32 | }
33 | }],
34 | 'vue/singleline-html-element-content-newline': 'off',
35 | 'vue/component-name-in-template-casing':['error', 'PascalCase', { 'ignores': [] }]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Node template
3 | # Logs
4 | logs
5 | *.log
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 |
10 | # Runtime data
11 | pids
12 | *.pid
13 | *.seed
14 | *.pid.lock
15 |
16 | # Directory for instrumented libs generated by jscoverage/JSCover
17 | lib-cov
18 |
19 | # Coverage directory used by tools like istanbul
20 | coverage
21 |
22 | # nyc test coverage
23 | .nyc_output
24 |
25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
26 | .grunt
27 |
28 | # Bower dependency directory (https://bower.io/)
29 | bower_components
30 |
31 | # node-waf configuration
32 | .lock-wscript
33 |
34 | # Compiled binary addons (https://nodejs.org/api/addons.html)
35 | build/Release
36 |
37 | # Dependency directories
38 | node_modules/
39 | jspm_packages/
40 |
41 | # TypeScript v1 declaration files
42 | typings/
43 |
44 | # Optional npm cache directory
45 | .npm
46 |
47 | # Optional eslint cache
48 | .eslintcache
49 |
50 | # Optional REPL history
51 | .node_repl_history
52 |
53 | # Output of 'npm pack'
54 | *.tgz
55 |
56 | # Yarn Integrity file
57 | .yarn-integrity
58 |
59 | # dotenv environment variables file
60 | .env
61 |
62 | # parcel-bundler cache (https://parceljs.org/)
63 | .cache
64 |
65 | # next.js build output
66 | .next
67 |
68 | # nuxt.js build output
69 | .nuxt
70 |
71 | # Nuxt generate
72 | dist
73 |
74 | # vuepress build output
75 | .vuepress/dist
76 |
77 | # Serverless directories
78 | .serverless
79 |
80 | # IDE
81 | .idea
82 |
83 | # Service worker
84 | sw.*
85 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Git History [](https://standardjs.com)
2 | Simple implementation of `pomber/git-history`
3 | using css transition animations **based on Vue** components
4 | [Try it](https://elevista.github.io/git-history)
5 |
6 |
7 |
8 |
9 |
10 | ## Usage
11 | Paste or edit the repository file url in input above and press Enter
12 | or add query string to url `?url=https://github.com/...`
13 | Only GitHub public repositories are supported for now.
14 |
15 | ## Code highlighter supported languages
16 | |Language|Extensions|
17 | |-|-|
18 | |javascript|.js|
19 | |jsx|.jsx|
20 | |typescript|.ts|
21 | |tsx|.tsx|
22 | |json|.json, .babelrc|
23 | |yaml|.yaml, .yml|
24 | |bash|.sh|
25 | |python|.py|
26 | |dart|.dart|
27 | |perl|.pl, .pm|
28 | |groovy|.groovy|
29 | |sql|.sql, .mysql|
30 | |css|.css|
31 | |less|.less|
32 | |scss|.scss|
33 | |ini|.ini, .editorconfig|
34 | |markup|.xml, .html, .htm, .svg, .mathml, .vue|
35 | |batch|.bat|
36 | |clojure|.clj|
37 | |coffeescript|.coffee|
38 | |cpp|.cpp, .cc|
39 | |csharp|.cs|
40 | |csp|.csp|
41 | |diff|.diff|
42 | |docker|dockerfile|
43 | |fsharp|.fsharp|
44 | |go|.go|
45 | |handlebars|.hbs|
46 | |haskell|.hs|
47 | |java|.java|
48 | |kotlin|.kt|
49 | |lua|.lua|
50 | |markdown|.md|
51 | |objectivec|.objc|
52 | |php|.php|
53 | |powershell|.ps|
54 | |pug|.pug|
55 | |r|.r|
56 | |reason|.re|
57 | |ruby|.rb|
58 | |rust|.rs|
59 | |scala|.scala|
60 | |scheme|.scheme|
61 | |swift|.swift|
62 | |visual-basic|.vb|
63 | |wasm|.wasm|
64 |
65 | ## License
66 | MIT
--------------------------------------------------------------------------------
/assets/Bitbucket-blue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/GitHub-Mark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Elevista/git-history/2a6b74e8b07c84ae0d3f038778dc993b77d95258/assets/GitHub-Mark.png
--------------------------------------------------------------------------------
/assets/bitbucket-neutral.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/global.scss:
--------------------------------------------------------------------------------
1 | html, body, #__nuxt, #__layout, #__layout>div {
2 | margin:0;
3 | padding:0;
4 | width: 100%;
5 | height: 100%;
6 | min-width: 100%;
7 | min-height: 100%;
8 | font-family: 'Noto Sans', sans-serif;
9 | }
10 |
--------------------------------------------------------------------------------
/nuxt.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | server: { host: '0.0.0.0' },
3 | css: [
4 | '~/assets/global.scss',
5 | 'normalize.css'
6 | ],
7 | head: {
8 | title: 'Git History',
9 | meta: [
10 | { charset: 'utf-8' },
11 | { name: 'viewport', content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no' }
12 | ],
13 | link: [
14 | { rel: 'icon', type: 'image/x-icon', href: 'favicon.ico' },
15 | { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Noto+Sans' }
16 | ]
17 | },
18 | build: {
19 | babel: {
20 | presets ({ isServer }) {
21 | const targets = isServer ? { node: '10' } : { ie: '10' }
22 | return [
23 | [ require.resolve('@nuxt/babel-preset-app'), { targets } ]
24 | ]
25 | }
26 | },
27 | extend (config, { isDev, isClient, isServer }) {
28 | if (isDev && isClient) config.devtool = 'inline-source-map'
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/nuxt.gh-pages.config.js:
--------------------------------------------------------------------------------
1 | import config from './nuxt.config'
2 |
3 | export default Object.assign(config, {
4 | router: { base: '/git-history/' }
5 | })
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "git-history",
3 | "version": "1.0.0",
4 | "description": "Quickly browse the history of a file from public github repository",
5 | "author": "Elevista",
6 | "scripts": {
7 | "start": "nuxt",
8 | "gh-pages": "nuxt generate -c nuxt.gh-pages.config.js && push-dir --dir=dist --branch=gh-pages --cleanup",
9 | "lint": "eslint --format ./node_modules/eslint-friendly-formatter --ext .js,.vue .",
10 | "lint:fix": "eslint --format ./node_modules/eslint-friendly-formatter --ext .js,.vue . --fix"
11 | },
12 | "dependencies": {
13 | "axios": "^0.18.1",
14 | "diff": "^4.0.1",
15 | "js-base64": "^2.5.1",
16 | "lodash": "^4.17.15",
17 | "moment": "^2.24.0",
18 | "netlify-auth-providers": "^1.0.0-alpha5",
19 | "normalize.css": "^8.0.1",
20 | "nuxt": "^2.6.1",
21 | "prismjs": "^1.16.0"
22 | },
23 | "devDependencies": {
24 | "babel-eslint": "^10.0.1",
25 | "eslint-friendly-formatter": "^4.0.1",
26 | "eslint-plugin-vue": "^5.2.2",
27 | "node-sass": "^4.12.0",
28 | "push-dir": "^0.4.1",
29 | "sass-loader": "^7.1.0",
30 | "standard": "^12.0.1"
31 | },
32 | "eslintIgnore": [
33 | "node_modules",
34 | "static"
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | export { default } from '../src/GitHistory.vue'
2 |
--------------------------------------------------------------------------------
/src/Commit.js:
--------------------------------------------------------------------------------
1 |
2 | export default class Commit {
3 | constructor ({ sha, author: { name, avatar }, date, message, code, url, fileName }) {
4 | Object.assign(this, { sha, author: { name, avatar }, date, message, code, url, fileName })
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/Diff.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
45 |
75 |
--------------------------------------------------------------------------------
/src/GitHistory.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
7 |
8 |

9 |
10 |
11 |
85 |
103 |
115 |
--------------------------------------------------------------------------------
/src/Header.vue:
--------------------------------------------------------------------------------
1 |
2 |
38 |
39 |
68 |
137 |
--------------------------------------------------------------------------------
/src/Prism.js:
--------------------------------------------------------------------------------
1 | import Prism from 'prismjs'
2 | import 'prismjs/themes/prism.css'
3 | import _ from 'lodash'
4 |
5 | const dep = {
6 | 'cpp': ['c'],
7 | 'objectivec': ['c'],
8 | 'handlebars': ['markup-templating']
9 | }
10 | const langExts = {
11 | 'javascript': ['.js'],
12 | 'jsx': ['.jsx'],
13 | 'typescript': ['.ts'],
14 | 'tsx': ['.tsx'],
15 | 'json': ['.json', '.babelrc'],
16 | 'yaml': ['.yaml', '.yml'],
17 | 'bash': ['.sh'],
18 | 'python': ['.py'],
19 | 'dart': ['.dart'],
20 | 'perl': ['.pl', '.pm'],
21 | 'groovy': ['.groovy'],
22 | 'sql': ['.sql', '.mysql'],
23 | 'css': ['.css'],
24 | 'less': ['.less'],
25 | 'scss': ['.scss'],
26 | 'ini': ['.ini', '.editorconfig'],
27 | 'markup': ['.xml', '.html', '.htm', '.svg', '.mathml', '.vue'],
28 | 'batch': ['.bat'],
29 | 'clojure': ['.clj'],
30 | 'coffeescript': ['.coffee'],
31 | 'cpp': ['.cpp', '.cc'],
32 | 'csharp': ['.cs'],
33 | 'csp': ['.csp'],
34 | 'diff': ['.diff'],
35 | 'docker': ['dockerfile'],
36 | 'fsharp': ['.fsharp'],
37 | 'go': ['.go'],
38 | 'handlebars': ['.hbs'],
39 | 'haskell': ['.hs'],
40 | 'java': ['.java'],
41 | 'kotlin': ['.kt'],
42 | 'lua': ['.lua'],
43 | 'markdown': ['.md'],
44 | 'objectivec': ['.objc'],
45 | 'php': ['.php'],
46 | 'powershell': ['.ps'],
47 | 'pug': ['.pug'],
48 | 'r': ['.r'],
49 | 'reason': ['.re'],
50 | 'ruby': ['.rb'],
51 | 'rust': ['.rs'],
52 | 'scala': ['.scala'],
53 | 'scheme': ['.scheme'],
54 | 'swift': ['.swift'],
55 | 'visual-basic': ['.vb'],
56 | 'wasm': ['.wasm']
57 | }
58 | const extLang = {}
59 | _.forEach(langExts, (exts, lang) => exts.forEach(ext => { extLang[ext] = lang }))
60 |
61 | async function loadDep (lang = 'javascript') {
62 | try {
63 | if (dep[lang]) await Promise.all(dep[lang].map(loadDep))
64 | if (!Prism.languages[lang]) await import(`prismjs/components/prism-${lang}`)
65 | return lang
66 | } catch (e) {
67 | console.error(e)
68 | return 'javascript'
69 | }
70 | }
71 | function detectLang (fileName = '') {
72 | const [ext] = fileName.match(/(\.[a-z]+|dockerfile)$/) || []
73 | return extLang[ext] || 'javascript'
74 | }
75 |
76 | export default {
77 | highlight (code = '', lang = 'javascript') {
78 | return Prism.highlight(code, Prism.languages[lang], lang)
79 | },
80 | loadDep,
81 | detectLang
82 | }
83 |
--------------------------------------------------------------------------------
/src/api/Bitbucket.js:
--------------------------------------------------------------------------------
1 | import Axios from 'axios'
2 | import Commit from '../Commit'
3 | import _ from 'lodash'
4 | import { basename } from 'path'
5 | import Netlify from 'netlify-auth-providers'
6 | import icon from '~/assets/Bitbucket-blue.svg'
7 | import neutralIcon from '~/assets/bitbucket-neutral.svg'
8 | const siteId = 'e118565a-2bcd-40be-b5ba-a76411cb721c'
9 | const apiURL = 'https://api.bitbucket.org/2.0/repositories'
10 |
11 | const rec = (path, value) => {
12 | if (value instanceof Array) return value.map(x => rec(path, x))
13 | if (value instanceof Object) return _.map(value, (v, k) => rec(`${path}.${k}`, v))
14 | return `${path}.${value}`.slice(1)
15 | }
16 | const fields = {
17 | values: {
18 | commit: [
19 | 'date', 'message', 'hash',
20 | { author: [
21 | 'raw',
22 | { user: ['display_name', 'links.avatar.href'] }
23 | ] }
24 | ]
25 | }
26 | }
27 | const fieldsPath = _.flattenDeep(rec('', fields))
28 | const params = { fields: fieldsPath.join(',') }
29 |
30 | async function authenticate () {
31 | const authenticator = new Netlify({ site_id: siteId })
32 | const { token } = await new Promise((resolve, reject) => authenticator.authenticate({ provider: 'bitbucket', scope: 'repo' }, (err, data) => err ? reject(err) : resolve(data)))
33 | return token
34 | }
35 |
36 | async function fetch (pathname, token) {
37 | try {
38 | const [, owner, repo, , sha, ...rest] = pathname.split('/')
39 | const axios = Axios.create({
40 | baseURL: `${apiURL}/${owner}/${repo}`,
41 | ...token && { headers: { common: { Authorization: `Bearer ${token}` } } }
42 | })
43 | const path = rest.join('/')
44 | const { data: { values: commits } } = await axios.get(`/filehistory/${sha}/${path}`, { params })
45 | return Promise.all(commits.map(async commit => {
46 | const [date, message, sha, rawName, name = rawName, avatar = neutralIcon] = _.map(fieldsPath, x => _.get({ values: commit }, x))
47 | const rest = {
48 | url: `https://bitbucket.org/${owner}/${repo}/src/${sha}/${path}`,
49 | fileName: basename(path),
50 | code: await axios.get(`/src/${sha}/${path}`, { transformResponse: x => x }).then(x => x.data)
51 | }
52 | return new Commit({ sha, author: { name, avatar }, date, message, ...rest })
53 | }))
54 | } catch (e) {
55 | if (_.get(e, 'response.status') === 401) throw Object.assign(e, { tokenExpired: true })
56 | throw new Error(_.get(e, 'response.data.error.message', `~/api/Bitbucket Error`))
57 | }
58 | }
59 |
60 | export default {
61 | hostname: 'bitbucket.org',
62 | icon,
63 | authenticate,
64 | async getCommits ({ pathname }, token) {
65 | return { commits: await fetch(pathname, token) }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/api/GitHub.js:
--------------------------------------------------------------------------------
1 | import Axios from 'axios'
2 | import Commit from '../Commit'
3 | import _ from 'lodash'
4 | import { Base64 } from 'js-base64'
5 | import Netlify from 'netlify-auth-providers'
6 | import icon from '~/assets/GitHub-Mark.png'
7 | const siteId = 'e118565a-2bcd-40be-b5ba-a76411cb721c'
8 | const apiURL = 'https://api.github.com/repos'
9 |
10 | async function authenticate () {
11 | const authenticator = new Netlify({ site_id: siteId })
12 | const { token } = await new Promise((resolve, reject) => authenticator.authenticate({ provider: 'github', scope: 'repo' }, (err, data) => err ? reject(err) : resolve(data)))
13 | return token
14 | }
15 |
16 | async function fetch (pathname, token) {
17 | try {
18 | const [, owner, repo, , sha, ...rest] = pathname.split('/')
19 | const axios = Axios.create({
20 | baseURL: `${apiURL}/${owner}/${repo}`,
21 | ...token && { headers: { common: { Authorization: `bearer ${token}` } } }
22 | })
23 | const path = rest.join('/')
24 | const { data: commits } = await axios.get(`/commits`, { params: { sha, path } })
25 | return Promise.all(commits.map(async commit => {
26 | const { sha, author, commit: { author: { name, date } = {}, message } = {} } = commit
27 | const { avatar_url: avatar = icon } = author || {} // author can be null
28 | const { name: fileName, content, html_url: url } = await axios.get(`/contents/${path}`, { params: { ref: sha } }).then(x => x.data)
29 | const code = Base64.decode(content)
30 | return new Commit({ sha, author: { name, avatar }, date, message, code, url, fileName })
31 | }))
32 | } catch (e) {
33 | if (_.get(e, 'response.status') === 401) throw Object.assign(e, { tokenExpired: true })
34 | throw new Error(_.get(e, 'response.data.message', '~/api/GitHub Error'))
35 | }
36 | }
37 |
38 | export default {
39 | hostname: 'github.com',
40 | icon,
41 | authenticate,
42 | async getCommits ({ pathname }, token) {
43 | return { commits: await fetch(pathname, token) }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/api/index.js:
--------------------------------------------------------------------------------
1 | export { default as GitHub } from './GitHub'
2 | export { default as Bitbucket } from './Bitbucket'
3 |
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Elevista/git-history/2a6b74e8b07c84ae0d3f038778dc993b77d95258/static/favicon.ico
--------------------------------------------------------------------------------