├── 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 |
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 | |