├── .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 [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg?style=flat-square)](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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Bitbucket-blue 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /assets/GitHub-Mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Elevista/git-history/2a6b74e8b07c84ae0d3f038778dc993b77d95258/assets/GitHub-Mark.png -------------------------------------------------------------------------------- /assets/bitbucket-neutral.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9B400751-3294-48C2-A771-3D6A0BF17C4F 5 | Created with sketchtool. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | 10 | 45 | 75 | -------------------------------------------------------------------------------- /src/GitHistory.vue: -------------------------------------------------------------------------------- 1 | 11 | 85 | 103 | 115 | -------------------------------------------------------------------------------- /src/Header.vue: -------------------------------------------------------------------------------- 1 | 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 --------------------------------------------------------------------------------