├── project ├── build.properties └── plugins.sbt ├── .babelrc ├── screenshot.png ├── scripts ├── index.js └── components │ ├── File.js │ ├── FileNodes.js │ ├── Directory.js │ └── Root.js ├── .gitignore ├── .eslintrc.json ├── .github └── workflows │ └── build.yml ├── package.json ├── src └── main │ ├── resources │ └── explorer │ │ └── assets │ │ └── plugin-explorer.css │ └── scala │ ├── Plugin.scala │ └── io │ └── github │ └── gitbucket │ └── explorer │ └── controllers │ └── ExplorerController.scala └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.5.0 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"] 3 | } -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitbucket/gitbucket-explorer-plugin/master/screenshot.png -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | logLevel := Level.Warn 2 | addSbtPlugin("io.github.gitbucket" % "sbt-gitbucket-plugin" % "1.5.1") 3 | -------------------------------------------------------------------------------- /scripts/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import Root from './components/Root'; 4 | 5 | render( 6 | , 7 | document.evaluate('//div[@class="main-sidebar"]/div[@class="sidebar"]/ul[@class="sidebar-menu"]/li[contains(.,"Files")]', 8 | document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue 9 | ); -------------------------------------------------------------------------------- /scripts/components/File.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | export default class File extends React.Component { 4 | 5 | static get propTypes() { 6 | return { 7 | url: PropTypes.string.isRequired, 8 | name: PropTypes.string.isRequired, 9 | }; 10 | } 11 | 12 | render() { 13 | return ( 14 |
  • 15 | 16 | 17 | {this.props.name} 18 | 19 |
  • 20 | ); 21 | } 22 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bundle.js 2 | node_modules/ 3 | *.class 4 | *.log 5 | .vscode 6 | package-lock.json 7 | 8 | # sbt specific 9 | dist/* 10 | target/ 11 | lib_managed/ 12 | src_managed/ 13 | project/boot/ 14 | project/plugins/project/ 15 | 16 | # Scala-IDE specific 17 | .scala_dependencies 18 | .classpath 19 | .project 20 | .cache 21 | .settings 22 | 23 | # IntelliJ specific 24 | .idea/ 25 | .idea_modules/ 26 | 27 | # Ensime 28 | .ensime 29 | .ensime_cache/ 30 | 31 | # Metals 32 | .metals 33 | .bloop 34 | .vscode 35 | **/metals.sbt 36 | -------------------------------------------------------------------------------- /scripts/components/FileNodes.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import File from './File'; 3 | import Directory from './Directory'; 4 | 5 | export default class FileNodes extends React.Component { 6 | 7 | static get propTypes() { 8 | return { 9 | data: PropTypes.array.isRequired, 10 | }; 11 | } 12 | 13 | render() { 14 | const nodes = this.props.data.map(node => ( 15 | node.isDirectory ? 16 | 17 | : 18 | )); 19 | return ( 20 |
      21 | {nodes} 22 |
    23 | ); 24 | } 25 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "env": { 4 | "browser": true, 5 | "es6": true 6 | }, 7 | "plugins": [ 8 | "react" 9 | ], 10 | "parserOptions": { 11 | "ecmaVersion": 6, 12 | "sourceType": "module", 13 | "ecmaFeatures": { 14 | "jsx": true 15 | } 16 | }, 17 | "rules": { 18 | "indent": ["error", 2], 19 | "semi": "error", 20 | "eol-last": "off", 21 | "react/jsx-uses-vars": "error", 22 | "react/jsx-uses-react": "error", 23 | "comma-dangle": "off", 24 | "react/jsx-filename-extension": ["error", {"extensions": [".js", ".jsx"]}], 25 | "react/prefer-stateless-function": "off" 26 | } 27 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Cache 11 | uses: actions/cache@v2 12 | env: 13 | cache-name: cache-sbt-libs 14 | with: 15 | path: | 16 | ~/.ivy2/cache 17 | ~/.sbt 18 | ~/.coursier 19 | key: build-${{ env.cache-name }}-${{ hashFiles('build.sbt') }} 20 | - name: Set up JDK 21 | uses: actions/setup-java@v2 22 | with: 23 | java-version: '8' 24 | distribution: 'adopt' 25 | - name: Run tests 26 | run: | 27 | git clone https://github.com/gitbucket/gitbucket.git 28 | cd gitbucket 29 | sbt publishLocal 30 | cd ../ 31 | sbt test 32 | - name: Assembly 33 | run: | 34 | npm install 35 | npm run-script release 36 | - name: Upload artifacts 37 | uses: actions/upload-artifact@v2 38 | with: 39 | name: gitbucket-explorer-plugin-${{ github.sha }} 40 | path: ./target/scala-2.13/*.jar 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitbucket-explorer-plugin", 3 | "version": "5.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "browserify -t babelify ./scripts/index.js -o ./src/main/resources/explorer/assets/bundle.js && sbt assembly", 9 | "watch": "watchify -t babelify ./scripts/index.js -o ./src/main/resources/explorer/assets/bundle.js", 10 | "release": "set NODE_ENV=production && browserify ./scripts/index.js -t babelify -t envify | uglifyjs > ./src/main/resources/explorer/assets/bundle.js && sbt clean assembly" 11 | }, 12 | "author": "tomoki1207", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "babel-preset-es2015": "^6.22.0", 16 | "babel-preset-react": "^6.22.0", 17 | "babelify": "^7.3.0", 18 | "browserify": "^14.0.0", 19 | "envify": "^4.0.0", 20 | "eslint": "^4.0.0", 21 | "eslint-config-airbnb": "^15.0.0", 22 | "eslint-plugin-import": "^2.7.0", 23 | "eslint-plugin-jsx-a11y": "^5.1.1", 24 | "eslint-plugin-react": "^7.1.0", 25 | "uglify-js": "^3.0.28", 26 | "watchify": "^3.11.1" 27 | }, 28 | "dependencies": { 29 | "react": "^15.4.2", 30 | "react-dom": "^15.4.2", 31 | "react-localstorage": "^0.3.0", 32 | "react-mixin": "^3.0.5", 33 | "superagent": "^3.4.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /scripts/components/Directory.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import ReactMixin from 'react-mixin'; 3 | import LocalStorageMixin from 'react-localstorage'; 4 | import request from 'superagent'; 5 | import FileNodes from './FileNodes'; 6 | 7 | export default class Directory extends React.Component { 8 | 9 | static get propTypes() { 10 | return { 11 | url: PropTypes.string, 12 | name: PropTypes.string 13 | }; 14 | } 15 | static get getDefaultProps() { 16 | return { 17 | url: '', 18 | name: '' 19 | }; 20 | } 21 | 22 | constructor(props) { 23 | super(props); 24 | this.state = { 25 | children: [], 26 | expanded: false, 27 | }; 28 | } 29 | 30 | getLocalStorageKey() { 31 | return this.props.url; 32 | } 33 | 34 | toggleFolder(path) { 35 | if (this.state.expanded) { 36 | this.setState({ 37 | expanded: false, 38 | children: [] 39 | }); 40 | } else { 41 | this.setState({ expanded: true }); 42 | request 43 | .get(path) 44 | .end((err, res) => { 45 | if (err) { 46 | return; 47 | } 48 | this.setState({ children: JSON.parse(res.text) }); 49 | }); 50 | } 51 | } 52 | 53 | render() { 54 | const arrow = this.state.expanded ? 'octicon octicon-chevron-down' : 'octicon octicon-chevron-right'; 55 | return ( 56 |
  • 57 | 62 | 63 |
  • 64 | ); 65 | } 66 | } 67 | ReactMixin(Directory.prototype, LocalStorageMixin); -------------------------------------------------------------------------------- /scripts/components/Root.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FileNodes from './FileNodes'; 3 | import Directory from './Directory'; 4 | 5 | export default class Root extends Directory { 6 | 7 | componentWillMount() { 8 | // obtain repository URI 9 | // - localhost:8080/owner/repo -> /owner/repo 10 | // - localhost:8080/gitbucket/owner/repo -> /owner/repo 11 | const baseUrl = document.querySelector('header.main-header a.logo').getAttribute('href').replace(`${document.location.protocol}//${document.location.host}`, ''); 12 | const relPath = document.location.pathname.replace(baseUrl, ''); 13 | let rootPath; 14 | let branch; 15 | if (['tree', 'blob'].some(s => relPath.includes(`/${s}/`))) { 16 | const m = relPath.match(/(^\/?[^/]+\/[^/]+)\/(?:tree|blob)\/([^/]+)/); 17 | rootPath = m[1]; 18 | branch = m[2]; 19 | } else { 20 | rootPath = relPath.match(/^\/?[^/]+\/[^/]+/); 21 | branch = ''; 22 | } 23 | this.setState({ 24 | rootPath: `${baseUrl}${rootPath}`, 25 | branch, 26 | }); 27 | } 28 | 29 | getLocalStorageKey() { 30 | return `${this.state.rootPath}/${this.state.branch}`; 31 | } 32 | 33 | render() { 34 | const arrow = this.state.expanded ? 'octicon octicon-chevron-down' : 'octicon octicon-chevron-right'; 35 | return ( 36 |
    37 | 40 | 41 | 42 | Files 43 | 44 |
    45 | 46 |
    47 |
    48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/resources/explorer/assets/plugin-explorer.css: -------------------------------------------------------------------------------- 1 | /* simulate AdminLTE */ 2 | .submenu-files:hover, 3 | .submenu-files:active, 4 | li.active .submenu-files { 5 | color: #fff; 6 | border-left-color: #3c8dbc; 7 | text-decoration: none; 8 | } 9 | .submenu-files { 10 | color: #b8c7cc; 11 | background: #1e282c; 12 | border-left: 3px solid transparent; 13 | width: auto; 14 | padding: 12px 5px 12px 15px; 15 | display: block; 16 | } 17 | .file-tree { 18 | overflow: auto; 19 | width: 100%; 20 | max-height: calc(100vh - 94px); 21 | padding-bottom: 10px; 22 | background-color: #fff; 23 | border-left: 3px solid #1e282c; 24 | border-right: 2px solid #1e282c; 25 | } 26 | li.active .file-tree { 27 | border-left: 3px solid #3c8dbc; 28 | } 29 | .file-tree ul { 30 | list-style: none; 31 | padding: 0; 32 | margin-left: 2em; 33 | } 34 | .file-tree ul li.folder-node { 35 | margin-left: -1em; 36 | } 37 | .tree-node i.octicon-file, 38 | .tree-node i.octicon-file-directory { 39 | margin-right: 0.2em; 40 | } 41 | .folder-expander octicon { 42 | font-size: 0.8em; 43 | } 44 | .root-expander { 45 | position: absolute; 46 | padding: 2px 1px 0px 5px; 47 | width: 21px; 48 | right: 15px; 49 | top: 10px; 50 | box-shadow: none; 51 | line-height: 1.5; 52 | text-align: center; 53 | background-color: #f4f4f4; 54 | border: 1px solid #ddd; 55 | border-radius: 3px; 56 | outline: none !important; 57 | } 58 | .root-expander > .octicon-chevron-right { 59 | width: 12px; 60 | } 61 | .root-expander > .octicon-chevron-down { 62 | width: 14px; 63 | } 64 | .root-expander:hover > .octicon { 65 | color: rgb(60, 141, 188); 66 | } 67 | .file-tree button { 68 | border: none; 69 | padding-left: 0; 70 | background: inherit; 71 | color: inherit; 72 | } 73 | .file-tree button, 74 | .file-node > a { 75 | color: #337ab7 !important;; 76 | } 77 | .file-tree button:hover, 78 | .file-node > a:hover { 79 | opacity: 0.6; 80 | } 81 | -------------------------------------------------------------------------------- /src/main/scala/Plugin.scala: -------------------------------------------------------------------------------- 1 | import javax.servlet.ServletContext 2 | 3 | import gitbucket.core.plugin.PluginRegistry 4 | import gitbucket.core.service.SystemSettingsService.SystemSettings 5 | import io.github.gitbucket.explorer.controllers.ExplorerController 6 | import io.github.gitbucket.solidbase.model.Version 7 | 8 | /** 9 | * Created by t_maruyama on 2017/01/31. 10 | */ 11 | class Plugin extends gitbucket.core.plugin.Plugin { 12 | override val pluginId: String = "explorer" 13 | override val pluginName: String = "Project explorer Plugin" 14 | override val description: String = "Explore Files from the file tree in the repository" 15 | override val versions: List[Version] = List( 16 | new Version("1.0.0"), 17 | new Version("1.0.1"), 18 | new Version("1.0.2"), 19 | new Version("1.0.3"), 20 | new Version("2.0.0"), 21 | new Version("3.0.0"), 22 | new Version("4.0.0"), 23 | new Version("5.0.0"), 24 | new Version("6.0.0"), 25 | new Version("6.1.0"), 26 | new Version("7.0.0"), 27 | new Version("8.0.0"), 28 | new Version("9.0.0") 29 | ) 30 | 31 | override val controllers = Seq( 32 | "/*" -> new ExplorerController() 33 | ) 34 | 35 | override val assetsMappings = Seq("/explorer" -> "explorer/assets") 36 | 37 | override def javaScripts(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(String, String)] = { 38 | val path = settings.baseUrl.getOrElse(context.getContextPath) 39 | Seq( 40 | ".*/(?!.*(signin|dashboard|admin)).+/.+" -> s""" 41 | | 42 | | 49 | | 50 | |