mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-05-09 04:26:32 +02:00
Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0085cb24ad | ||
|
|
6a758902ef | ||
|
|
0d81a9a9b6 | ||
|
|
6e4f6da633 | ||
|
|
15118ca5c1 | ||
|
|
8161560757 | ||
|
|
9ba564c864 | ||
|
|
06b5b92673 | ||
|
|
b9b6589bd7 | ||
|
|
b79f6a5fa0 | ||
|
|
bd046da3d0 | ||
|
|
a889ed7c46 | ||
|
|
e24684cb2b | ||
|
|
5f939c18b4 | ||
|
|
d412dd5009 | ||
|
|
8643bfeb37 | ||
|
|
31b6adf0e5 | ||
|
|
f1ac2b3507 | ||
|
|
135e1ef73d | ||
|
|
da55bf6af3 | ||
|
|
883a9c8b17 | ||
|
|
7da89940e3 | ||
|
|
3233b0ae3c | ||
|
|
4c2ed09915 | ||
|
|
256b6c480f | ||
|
|
dc311837f9 | ||
|
|
92aec48c99 | ||
|
|
a6ada8c457 | ||
|
|
dcc601502e | ||
|
|
dd58d8c804 | ||
|
|
2ade54b7e3 | ||
|
|
136c5854f3 | ||
|
|
c597238d9c | ||
|
|
2552a58e08 | ||
|
|
74ad5872a3 | ||
|
|
485d502bd3 | ||
|
|
47bc8d030e | ||
|
|
48fe7133f7 | ||
|
|
5d962dc5e4 | ||
|
|
31e8e5a951 | ||
|
|
858373c628 | ||
|
|
7f142d2c0d | ||
|
|
08b86232a8 | ||
|
|
6bf4f42fdb | ||
|
|
f3c7de36d8 | ||
|
|
19f556de57 | ||
|
|
e4467df411 | ||
|
|
8d305a1fb1 | ||
|
|
b47153e645 | ||
|
|
c71766c84b | ||
|
|
23e4d679ae | ||
|
|
182acb2e02 | ||
|
|
b255b15006 | ||
|
|
b458f88161 | ||
|
|
398d8f2f1c | ||
|
|
85c1a56cbf | ||
|
|
da216c6960 | ||
|
|
bc91b153bf | ||
|
|
bc50b47d3a | ||
|
|
aed15a7f25 | ||
|
|
a1f09117b0 | ||
|
|
0a4a4a51ca | ||
|
|
f7fd53bf09 | ||
|
|
cbfb863a54 | ||
|
|
9d56d72611 | ||
|
|
527c91ff9d | ||
|
|
c58c2d6700 | ||
|
|
5518eca952 | ||
|
|
6e2b67ec0b | ||
|
|
837b1e44a7 | ||
|
|
e04c230c6e | ||
|
|
a01b5a4a59 | ||
|
|
427b6ce846 | ||
|
|
b7b5af2b72 | ||
|
|
39fec57f72 | ||
|
|
238dedb6df | ||
|
|
af091117b7 | ||
|
|
ddea4e12f0 | ||
|
|
9767903252 | ||
|
|
bc75f9f8a2 | ||
|
|
63627fc1d0 | ||
|
|
c23985c1a7 | ||
|
|
af58e99dcf | ||
|
|
676670e9e3 | ||
|
|
823c52e941 | ||
|
|
7f42007648 | ||
|
|
7214ef21d2 | ||
|
|
18a4492975 | ||
|
|
99f73b1016 | ||
|
|
0c1ce6a088 | ||
|
|
ae6291ab83 | ||
|
|
617fcf7c99 | ||
|
|
9df4a74837 | ||
|
|
966d4251be | ||
|
|
84b2e9cdcd | ||
|
|
e29d63c91a | ||
|
|
805d2b8e79 | ||
|
|
9983fd1292 | ||
|
|
1de202e927 | ||
|
|
4eb9f4a485 | ||
|
|
a8801e4e41 | ||
|
|
ee1c84dbf2 | ||
|
|
e40e1fa6cd | ||
|
|
055f648ea2 | ||
|
|
37a399c3a2 | ||
|
|
bc0b11b60a | ||
|
|
65a1ca7146 | ||
|
|
2293030d4e | ||
|
|
2848f07b83 | ||
|
|
55224ddcd8 | ||
|
|
054ae75b6b | ||
|
|
c83fab611e | ||
|
|
29baf1223c | ||
|
|
ff4052f097 | ||
|
|
a10188260c |
3
.travis.yml
Normal file
3
.travis.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
language: scala
|
||||||
|
scala:
|
||||||
|
- 2.11.2
|
||||||
24
README.md
24
README.md
@@ -1,7 +1,7 @@
|
|||||||
GitBucket [](https://gitter.im/takezoe/gitbucket) [](https://buildhive.cloudbees.com/job/takezoe/job/gitbucket/)
|
GitBucket [](https://gitter.im/takezoe/gitbucket) [](https://travis-ci.org/takezoe/gitbucket)
|
||||||
=========
|
=========
|
||||||
|
|
||||||
GitBucket is the easily installable Github clone written with Scala.
|
GitBucket is the easily installable GitHub clone powered by Scala.
|
||||||
|
|
||||||
|
|
||||||
Features
|
Features
|
||||||
@@ -23,7 +23,6 @@ The current version of GitBucket provides a basic features below:
|
|||||||
|
|
||||||
Following features are not implemented, but we will make them in the future release!
|
Following features are not implemented, but we will make them in the future release!
|
||||||
|
|
||||||
- Comment for the changeset
|
|
||||||
- Network graph
|
- Network graph
|
||||||
- Statistics
|
- Statistics
|
||||||
- Watch / Star
|
- Watch / Star
|
||||||
@@ -80,6 +79,25 @@ Run the following commands in `Terminal` to
|
|||||||
|
|
||||||
Release Notes
|
Release Notes
|
||||||
--------
|
--------
|
||||||
|
### 2.8 - 1 Feb 2015
|
||||||
|
- New logo and icons
|
||||||
|
- New system setting options to control visibility
|
||||||
|
- Comment on side-by-side diff
|
||||||
|
- Information message on sign-in page
|
||||||
|
- Fork repository by group account
|
||||||
|
|
||||||
|
### 2.7 - 29 Dec 2014
|
||||||
|
- Comment for commit and diff
|
||||||
|
- Fix security issue in markdown rendering
|
||||||
|
- Some bug fix and improvements
|
||||||
|
|
||||||
|
### 2.6 - 24 Nov 2014
|
||||||
|
- Search box at issues and pull requests
|
||||||
|
- Information from administrator
|
||||||
|
- Pull request UI has been updated
|
||||||
|
- Move to TravisCI from Buildhive
|
||||||
|
- Some bug fix and improvements
|
||||||
|
|
||||||
### 2.5 - 4 Nov 2014
|
### 2.5 - 4 Nov 2014
|
||||||
- New Dashboard
|
- New Dashboard
|
||||||
- Change datetime format
|
- Change datetime format
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ Common scripts are in this directory.
|
|||||||
This version of scripts has so far only been tested on Ubuntu and Mac. Someone else will have to test on RedHat.
|
This version of scripts has so far only been tested on Ubuntu and Mac. Someone else will have to test on RedHat.
|
||||||
|
|
||||||
To run:
|
To run:
|
||||||
|
|
||||||
1. Edit `gitbucket.conf` to suit.
|
1. Edit `gitbucket.conf` to suit.
|
||||||
2. Type: `install`
|
2. Type: `install`
|
||||||
|
|
||||||
|
|||||||
15
contrib/linux/redhat/README.md
Normal file
15
contrib/linux/redhat/README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Contrib Notes #
|
||||||
|
|
||||||
|
RPM spec file and init script for Red Hat Enterprise Linux 6.x.
|
||||||
|
|
||||||
|
To create RPM:
|
||||||
|
1. Edit `../../gitbucket.conf` to suit.
|
||||||
|
2. Edit `gitbucket.init` to suit.
|
||||||
|
3. Edit `gitbucket.spec` to suit.
|
||||||
|
4. Place `gitbucket.spec` to rpm/SPECS/.
|
||||||
|
5. Place `gitbucket.init` and `gitbucket.war` to rpm/SOURCES/.
|
||||||
|
6. Execute `rpmbuild -ba rpm/SPECS/gitbucket.spec`
|
||||||
|
|
||||||
|
This rpm runs gitbucket not as root user but as gitbucket user.
|
||||||
|
This rpm creates user and group named `gitbucket` at installation.
|
||||||
|
This rpm make chkconfig of gitbucket to be on.
|
||||||
108
contrib/linux/redhat/gitbucket.init
Normal file
108
contrib/linux/redhat/gitbucket.init
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# RedHat: /etc/rc.d/init.d/gitbucket
|
||||||
|
#
|
||||||
|
# Starts the GitBucket server
|
||||||
|
#
|
||||||
|
# chkconfig: 345 60 40
|
||||||
|
# description: Run GitBucket server
|
||||||
|
# processname: java
|
||||||
|
|
||||||
|
[ -f /etc/rc.d/init.d/functions ] && source /etc/rc.d/init.d/functions # RedHat
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
GITBUCKET_HOME=/var/lib/gitbucket
|
||||||
|
GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
|
||||||
|
|
||||||
|
# Pull in cq settings
|
||||||
|
[ -f /etc/sysconfig/gitbucket ] && source /etc/sysconfig/gitbucket # RedHat
|
||||||
|
[ -f gitbucket.conf ] && source gitbucket.conf # For all systems
|
||||||
|
|
||||||
|
# Location of the log and PID file
|
||||||
|
LOG_FILE=$GITBUCKET_LOG_DIR/run.log
|
||||||
|
|
||||||
|
RED='\033[1m\E[37;41m'
|
||||||
|
GREEN='\033[1m\E[37;42m'
|
||||||
|
OFF='\E[0m'
|
||||||
|
|
||||||
|
RETVAL=0
|
||||||
|
|
||||||
|
start() {
|
||||||
|
echo -n $"Starting GitBucket server: "
|
||||||
|
|
||||||
|
START_OPTS=
|
||||||
|
if [ $GITBUCKET_PORT ]; then
|
||||||
|
START_OPTS="${START_OPTS} --port=${GITBUCKET_PORT}"
|
||||||
|
fi
|
||||||
|
if [ $GITBUCKET_PREFIX ]; then
|
||||||
|
START_OPTS="${START_OPTS} --prefix=${GITBUCKET_PREFIX}"
|
||||||
|
fi
|
||||||
|
if [ $GITBUCKET_HOST ]; then
|
||||||
|
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
GITBUCKET_HOME="${GITBUCKET_HOME}" daemon --user=gitbucket java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &
|
||||||
|
sleep 3
|
||||||
|
pgrep -f $GITBUCKET_WAR_FILE >> $LOG_FILE 2>&1
|
||||||
|
RETVAL=$?
|
||||||
|
|
||||||
|
if [ $RETVAL -eq 0 ] ; then
|
||||||
|
success "Success"
|
||||||
|
else
|
||||||
|
failure "Exit code $RETVAL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
return $RETVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
echo -n $"Stopping GitBucket server: "
|
||||||
|
|
||||||
|
# Run the Java process
|
||||||
|
pkill -f $GITBUCKET_WAR_FILE >>$LOG_FILE 2>&1
|
||||||
|
RETVAL=$?
|
||||||
|
|
||||||
|
if [ $RETVAL -eq 0 ] ; then
|
||||||
|
success "GitBucket stopping"
|
||||||
|
else
|
||||||
|
failure "GitBucket stopping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
return $RETVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
restart() {
|
||||||
|
stop
|
||||||
|
start
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
start)
|
||||||
|
start
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
restart
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
pgrep -f $GITBUCKET_WAR_FILE >> $LOG_FILE 2>&1
|
||||||
|
RETVAL=$?
|
||||||
|
if [ $RETVAL -eq 0 ]; then
|
||||||
|
echo $"GitBucket is running...."
|
||||||
|
else
|
||||||
|
echo $"GitBucket is stopped"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo $"Usage: $0 [start|stop|restart|status]"
|
||||||
|
RETVAL=2
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit $RETVAL
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
Name: gitbucket
|
Name: gitbucket
|
||||||
Summary: GitHub clone written with Scala.
|
Summary: GitHub clone written with Scala.
|
||||||
Version: 1.7
|
Version: 2.6
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
License: Apache
|
License: Apache
|
||||||
URL: https://github.com/takezoe/gitbucket
|
URL: https://github.com/takezoe/gitbucket
|
||||||
@@ -26,6 +26,25 @@ GitBucket is the easily installable GitHub clone written with Scala.
|
|||||||
%{__install} -m 0644 %{SOURCE2} %{buildroot}%{_sysconfdir}/sysconfig/%{name}
|
%{__install} -m 0644 %{SOURCE2} %{buildroot}%{_sysconfdir}/sysconfig/%{name}
|
||||||
touch %{buildroot}%{_localstatedir}/log/%{name}/run.log
|
touch %{buildroot}%{_localstatedir}/log/%{name}/run.log
|
||||||
|
|
||||||
|
%pre
|
||||||
|
/usr/sbin/groupadd -r gitbucket &> /dev/null || :
|
||||||
|
/usr/sbin/useradd -g gitbucket -s /bin/false -r -c "GitBucket GitHub clone" -d %{_sharedstatedir}/%{name} gitbucket &> /dev/null || :
|
||||||
|
|
||||||
|
%post
|
||||||
|
/sbin/chkconfig --add gitbucket
|
||||||
|
|
||||||
|
%preun
|
||||||
|
if [ "$1" = 0 ]; then
|
||||||
|
/sbin/service gitbucket stop > /dev/null 2>&1
|
||||||
|
/sbin/chkconfig --del gitbucket
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
|
||||||
|
%postun
|
||||||
|
if [ "$1" -ge 1 ]; then
|
||||||
|
/sbin/service gitbucket restart > /dev/null 2>&1
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
|
||||||
%clean
|
%clean
|
||||||
[ "%{buildroot}" != / ] && %{__rm} -rf "%{buildroot}"
|
[ "%{buildroot}" != / ] && %{__rm} -rf "%{buildroot}"
|
||||||
@@ -34,12 +53,28 @@ touch %{buildroot}%{_localstatedir}/log/%{name}/run.log
|
|||||||
%files
|
%files
|
||||||
%defattr(-,root,root,-)
|
%defattr(-,root,root,-)
|
||||||
%{_datarootdir}/%{name}/lib/%{name}.war
|
%{_datarootdir}/%{name}/lib/%{name}.war
|
||||||
%{_sysconfdir}/init.d/%{name}
|
%config %{_sysconfdir}/init.d/%{name}
|
||||||
%config %{_sysconfdir}/sysconfig/%{name}
|
%config(noreplace) %{_sysconfdir}/sysconfig/%{name}
|
||||||
%{_localstatedir}/log/%{name}/run.log
|
%attr(0755,gitbucket,gitbucket) %{_sharedstatedir}/%{name}
|
||||||
|
%attr(0750,gitbucket,gitbucket) %{_localstatedir}/log/%{name}
|
||||||
|
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Mon Nov 24 2014 Toru Takahashi <torutk at gmail.com>
|
||||||
|
- Version bump to v2.6
|
||||||
|
|
||||||
|
* Sun Nov 09 2014 Toru Takahashi <torutk at gmail.com>
|
||||||
|
- Version bump to v2.5
|
||||||
|
|
||||||
|
* Sun Oct 26 2014 Toru Takahashi <torutk at gmail.com>
|
||||||
|
- Version bump to v2.4.1
|
||||||
|
|
||||||
|
* Mon Jul 21 2014 Toru Takahashi <torutk at gmail.com>
|
||||||
|
- execute as gitbucket user
|
||||||
|
|
||||||
|
* Sun Jul 20 2014 Toru Takahashi <torutk at gmail.com>
|
||||||
|
- Version bump to v2.1.
|
||||||
|
|
||||||
* Mon Oct 28 2013 Jiri Tyr <jiri_DOT_tyr at gmail.com>
|
* Mon Oct 28 2013 Jiri Tyr <jiri_DOT_tyr at gmail.com>
|
||||||
- Version bump to v1.7.
|
- Version bump to v1.7.
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ object MyBuild extends Build {
|
|||||||
"org.apache.sshd" % "apache-sshd" % "0.11.0",
|
"org.apache.sshd" % "apache-sshd" % "0.11.0",
|
||||||
"com.typesafe.slick" %% "slick" % "2.1.0",
|
"com.typesafe.slick" %% "slick" % "2.1.0",
|
||||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||||
"org.quartz-scheduler" % "quartz" % "2.2.1",
|
|
||||||
"com.h2database" % "h2" % "1.4.180",
|
"com.h2database" % "h2" % "1.4.180",
|
||||||
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
|
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
|
||||||
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
|
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
|
||||||
|
|||||||
18
src/main/resources/update/2_7.sql
Normal file
18
src/main/resources/update/2_7.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
CREATE TABLE COMMIT_COMMENT (
|
||||||
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
|
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
|
COMMIT_ID VARCHAR(100) NOT NULL,
|
||||||
|
COMMENT_ID INT AUTO_INCREMENT,
|
||||||
|
COMMENTED_USER_NAME VARCHAR(100) NOT NULL,
|
||||||
|
CONTENT TEXT NOT NULL,
|
||||||
|
FILE_NAME NVARCHAR(100),
|
||||||
|
OLD_LINE_NUMBER INT,
|
||||||
|
NEW_LINE_NUMBER INT,
|
||||||
|
REGISTERED_DATE TIMESTAMP NOT NULL,
|
||||||
|
UPDATED_DATE TIMESTAMP NOT NULL,
|
||||||
|
PULL_REQUEST BOOLEAN NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_PK PRIMARY KEY (COMMENT_ID);
|
||||||
|
ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
||||||
|
ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_1 UNIQUE (USER_NAME, REPOSITORY_NAME, COMMIT_ID, COMMENT_ID);
|
||||||
1
src/main/resources/update/2_8.sql
Normal file
1
src/main/resources/update/2_8.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE COMMIT_COMMENT ALTER COLUMN FILE_NAME NVARCHAR(260);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import _root_.servlet.{PluginActionInvokeFilter, BasicAuthenticationFilter, TransactionFilter}
|
import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter}
|
||||||
import app._
|
import app._
|
||||||
//import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
|
//import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
|
||||||
import org.scalatra._
|
import org.scalatra._
|
||||||
@@ -10,12 +10,11 @@ class ScalatraBootstrap extends LifeCycle {
|
|||||||
// Register TransactionFilter and BasicAuthenticationFilter at first
|
// Register TransactionFilter and BasicAuthenticationFilter at first
|
||||||
context.addFilter("transactionFilter", new TransactionFilter)
|
context.addFilter("transactionFilter", new TransactionFilter)
|
||||||
context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
|
context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
|
||||||
context.addFilter("pluginActionInvokeFilter", new PluginActionInvokeFilter)
|
|
||||||
context.getFilterRegistration("pluginActionInvokeFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
|
|
||||||
context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter)
|
context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter)
|
||||||
context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
|
context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
|
||||||
|
|
||||||
// Register controllers
|
// Register controllers
|
||||||
|
context.mount(new AnonymousAccessController, "/*")
|
||||||
context.mount(new IndexController, "/")
|
context.mount(new IndexController, "/")
|
||||||
context.mount(new SearchController, "/")
|
context.mount(new SearchController, "/")
|
||||||
context.mount(new FileUploadController, "/upload")
|
context.mount(new FileUploadController, "/upload")
|
||||||
|
|||||||
@@ -88,6 +88,12 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
"name" -> trim(label("Repository name", text(required)))
|
"name" -> trim(label("Repository name", text(required)))
|
||||||
)(ForkRepositoryForm.apply)
|
)(ForkRepositoryForm.apply)
|
||||||
|
|
||||||
|
case class AccountForm(accountName: String)
|
||||||
|
|
||||||
|
val accountForm = mapping(
|
||||||
|
"account" -> trim(label("Group/User name", text(required, validAccountName)))
|
||||||
|
)(AccountForm.apply)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays user information.
|
* Displays user information.
|
||||||
*/
|
*/
|
||||||
@@ -129,8 +135,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
get("/:userName/_avatar"){
|
get("/:userName/_avatar"){
|
||||||
val userName = params("userName")
|
val userName = params("userName")
|
||||||
getAccountByUserName(userName).flatMap(_.image).map { image =>
|
getAccountByUserName(userName).flatMap(_.image).map { image =>
|
||||||
contentType = FileUtil.getMimeType(image)
|
RawData(FileUtil.getMimeType(image), new java.io.File(getUserUploadDir(userName), image))
|
||||||
new java.io.File(getUserUploadDir(userName), image)
|
|
||||||
} getOrElse {
|
} getOrElse {
|
||||||
contentType = "image/png"
|
contentType = "image/png"
|
||||||
Thread.currentThread.getContextClassLoader.getResourceAsStream("noimage.png")
|
Thread.currentThread.getContextClassLoader.getResourceAsStream("noimage.png")
|
||||||
@@ -285,7 +290,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
* Show the new repository form.
|
* Show the new repository form.
|
||||||
*/
|
*/
|
||||||
get("/new")(usersOnly {
|
get("/new")(usersOnly {
|
||||||
account.html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
|
account.html.newrepo(getGroupsByUserName(context.loginAccount.get.userName), context.settings.isCreateRepoOptionPublic)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -354,11 +359,31 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
|
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
|
||||||
val loginAccount = context.loginAccount.get
|
val loginAccount = context.loginAccount.get
|
||||||
val loginUserName = loginAccount.userName
|
val loginUserName = loginAccount.userName
|
||||||
|
val groups = getGroupsByUserName(loginUserName)
|
||||||
|
groups match {
|
||||||
|
case _: List[String] =>
|
||||||
|
val managerPermissions = groups.map { group =>
|
||||||
|
val members = getGroupMembers(group)
|
||||||
|
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })
|
||||||
|
}
|
||||||
|
_root_.helper.html.forkrepository(
|
||||||
|
repository,
|
||||||
|
(groups zip managerPermissions).toMap
|
||||||
|
)
|
||||||
|
case _ => redirect(s"/${loginUserName}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
LockUtil.lock(s"${loginUserName}/${repository.name}"){
|
post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) =>
|
||||||
if(repository.owner == loginUserName || getRepository(loginAccount.userName, repository.name, baseUrl).isDefined){
|
val loginAccount = context.loginAccount.get
|
||||||
|
val loginUserName = loginAccount.userName
|
||||||
|
val accountName = form.accountName
|
||||||
|
|
||||||
|
LockUtil.lock(s"${accountName}/${repository.name}"){
|
||||||
|
if(getRepository(accountName, repository.name, baseUrl).isDefined ||
|
||||||
|
(accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){
|
||||||
// redirect to the repository if repository already exists
|
// redirect to the repository if repository already exists
|
||||||
redirect(s"/${loginUserName}/${repository.name}")
|
redirect(s"/${accountName}/${repository.name}")
|
||||||
} else {
|
} else {
|
||||||
// Insert to the database at first
|
// Insert to the database at first
|
||||||
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
|
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
|
||||||
@@ -366,7 +391,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
|
|
||||||
createRepository(
|
createRepository(
|
||||||
repositoryName = repository.name,
|
repositoryName = repository.name,
|
||||||
userName = loginUserName,
|
userName = accountName,
|
||||||
description = repository.repository.description,
|
description = repository.repository.description,
|
||||||
isPrivate = repository.repository.isPrivate,
|
isPrivate = repository.repository.isPrivate,
|
||||||
originRepositoryName = Some(originRepositoryName),
|
originRepositoryName = Some(originRepositoryName),
|
||||||
@@ -376,22 +401,22 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Insert default labels
|
// Insert default labels
|
||||||
insertDefaultLabels(loginUserName, repository.name)
|
insertDefaultLabels(accountName, repository.name)
|
||||||
|
|
||||||
// clone repository actually
|
// clone repository actually
|
||||||
JGitUtil.cloneRepository(
|
JGitUtil.cloneRepository(
|
||||||
getRepositoryDir(repository.owner, repository.name),
|
getRepositoryDir(repository.owner, repository.name),
|
||||||
getRepositoryDir(loginUserName, repository.name))
|
getRepositoryDir(accountName, repository.name))
|
||||||
|
|
||||||
// Create Wiki repository
|
// Create Wiki repository
|
||||||
JGitUtil.cloneRepository(
|
JGitUtil.cloneRepository(
|
||||||
getWikiRepositoryDir(repository.owner, repository.name),
|
getWikiRepositoryDir(repository.owner, repository.name),
|
||||||
getWikiRepositoryDir(loginUserName, repository.name))
|
getWikiRepositoryDir(accountName, repository.name))
|
||||||
|
|
||||||
// Record activity
|
// Record activity
|
||||||
recordForkActivity(repository.owner, repository.name, loginUserName)
|
recordForkActivity(repository.owner, repository.name, loginUserName, accountName)
|
||||||
// redirect to the repository
|
// redirect to the repository
|
||||||
redirect(s"/${loginUserName}/${repository.name}")
|
redirect(s"/${accountName}/${repository.name}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -431,4 +456,13 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
case None => Some("Key is invalid.")
|
case None => Some("Key is invalid.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def validAccountName: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||||
|
getAccountByUserName(value) match {
|
||||||
|
case Some(_) => None
|
||||||
|
case None => Some("Invalid Group/User Account.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/main/scala/app/AnonymousAccessController.scala
Normal file
14
src/main/scala/app/AnonymousAccessController.scala
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
class AnonymousAccessController extends AnonymousAccessControllerBase
|
||||||
|
|
||||||
|
trait AnonymousAccessControllerBase extends ControllerBase {
|
||||||
|
get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) {
|
||||||
|
if(!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
|
||||||
|
!context.currentPath.startsWith("/register")) {
|
||||||
|
Unauthorized()
|
||||||
|
} else {
|
||||||
|
pass()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -134,6 +134,18 @@ abstract class ControllerBase extends ScalatraFilter
|
|||||||
if (path.startsWith("http")) path
|
if (path.startsWith("http")) path
|
||||||
else baseUrl + super.url(path, params, false, false, false)
|
else baseUrl + super.url(path, params, false, false, false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this method to response the raw data against XSS.
|
||||||
|
*/
|
||||||
|
protected def RawData[T](contentType: String, rawData: T): T = {
|
||||||
|
if(contentType.split(";").head.trim.toLowerCase.startsWith("text/html")){
|
||||||
|
this.contentType = "text/plain"
|
||||||
|
} else {
|
||||||
|
this.contentType = contentType
|
||||||
|
}
|
||||||
|
response.addHeader("X-Content-Type-Options", "nosniff")
|
||||||
|
rawData
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import service._
|
import service._
|
||||||
import util.{UsersAuthenticator, Keys}
|
import util.{StringUtil, UsersAuthenticator, Keys}
|
||||||
import util.Implicits._
|
import util.Implicits._
|
||||||
|
import service.IssuesService.IssueSearchCondition
|
||||||
|
|
||||||
class DashboardController extends DashboardControllerBase
|
class DashboardController extends DashboardControllerBase
|
||||||
with IssuesService with PullRequestService with RepositoryService with AccountService
|
with IssuesService with PullRequestService with RepositoryService with AccountService
|
||||||
@@ -12,8 +13,21 @@ trait DashboardControllerBase extends ControllerBase {
|
|||||||
self: IssuesService with PullRequestService with RepositoryService with AccountService
|
self: IssuesService with PullRequestService with RepositoryService with AccountService
|
||||||
with UsersAuthenticator =>
|
with UsersAuthenticator =>
|
||||||
|
|
||||||
get("/dashboard/issues/repos")(usersOnly {
|
get("/dashboard/issues")(usersOnly {
|
||||||
|
val q = request.getParameter("q")
|
||||||
|
val account = context.loginAccount.get
|
||||||
|
Option(q).map { q =>
|
||||||
|
val condition = IssueSearchCondition(q, Map[String, Int]())
|
||||||
|
q match {
|
||||||
|
case q if(q.contains("is:pr")) => redirect(s"/dashboard/pulls?q=${StringUtil.urlEncode(q)}")
|
||||||
|
case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/issues/created_by${condition.toURL}")
|
||||||
|
case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/issues/assigned${condition.toURL}")
|
||||||
|
case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/issues/mentioned${condition.toURL}")
|
||||||
|
case _ => searchIssues("created_by")
|
||||||
|
}
|
||||||
|
} getOrElse {
|
||||||
searchIssues("created_by")
|
searchIssues("created_by")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/dashboard/issues/assigned")(usersOnly {
|
get("/dashboard/issues/assigned")(usersOnly {
|
||||||
@@ -29,70 +43,92 @@ trait DashboardControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
get("/dashboard/pulls")(usersOnly {
|
get("/dashboard/pulls")(usersOnly {
|
||||||
searchPullRequests("created_by", None)
|
val q = request.getParameter("q")
|
||||||
|
val account = context.loginAccount.get
|
||||||
|
Option(q).map { q =>
|
||||||
|
val condition = IssueSearchCondition(q, Map[String, Int]())
|
||||||
|
q match {
|
||||||
|
case q if(q.contains("is:issue")) => redirect(s"/dashboard/issues?q=${StringUtil.urlEncode(q)}")
|
||||||
|
case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/pulls/created_by${condition.toURL}")
|
||||||
|
case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/pulls/assigned${condition.toURL}")
|
||||||
|
case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/pulls/mentioned${condition.toURL}")
|
||||||
|
case _ => searchPullRequests("created_by")
|
||||||
|
}
|
||||||
|
} getOrElse {
|
||||||
|
searchPullRequests("created_by")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/dashboard/pulls/owned")(usersOnly {
|
get("/dashboard/pulls/created_by")(usersOnly {
|
||||||
searchPullRequests("created_by", None)
|
searchPullRequests("created_by")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/dashboard/pulls/assigned")(usersOnly {
|
||||||
|
searchPullRequests("assigned")
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/dashboard/pulls/mentioned")(usersOnly {
|
get("/dashboard/pulls/mentioned")(usersOnly {
|
||||||
searchPullRequests("mentioned", None)
|
searchPullRequests("mentioned")
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/dashboard/pulls/public")(usersOnly {
|
private def getOrCreateCondition(key: String, filter: String, userName: String) = {
|
||||||
searchPullRequests("not_created_by", None)
|
val condition = session.putAndGet(key, if(request.hasQueryString){
|
||||||
})
|
val q = request.getParameter("q")
|
||||||
|
if(q == null){
|
||||||
|
IssueSearchCondition(request)
|
||||||
|
} else {
|
||||||
|
IssueSearchCondition(q, Map[String, Int]())
|
||||||
|
}
|
||||||
|
} else session.getAs[IssueSearchCondition](key).getOrElse(IssueSearchCondition()))
|
||||||
|
|
||||||
get("/dashboard/pulls/for/:owner/:repository")(usersOnly {
|
filter match {
|
||||||
searchPullRequests("all", Some(params("owner") + "/" + params("repository")))
|
case "assigned" => condition.copy(assigned = Some(userName), author = None , mentioned = None)
|
||||||
})
|
case "mentioned" => condition.copy(assigned = None , author = None , mentioned = Some(userName))
|
||||||
|
case _ => condition.copy(assigned = None , author = Some(userName), mentioned = None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private def searchIssues(filter: String) = {
|
private def searchIssues(filter: String) = {
|
||||||
import IssuesService._
|
import IssuesService._
|
||||||
|
|
||||||
// condition
|
|
||||||
val condition = session.putAndGet(Keys.Session.DashboardIssues,
|
|
||||||
if(request.hasQueryString) IssueSearchCondition(request)
|
|
||||||
else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition())
|
|
||||||
)
|
|
||||||
|
|
||||||
val userName = context.loginAccount.get.userName
|
val userName = context.loginAccount.get.userName
|
||||||
|
val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName)
|
||||||
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
|
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
|
||||||
val filterUser = Map(filter -> userName)
|
|
||||||
val page = IssueSearchCondition.page(request)
|
val page = IssueSearchCondition.page(request)
|
||||||
|
|
||||||
dashboard.html.issues(
|
dashboard.html.issues(
|
||||||
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
|
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
|
||||||
page,
|
page,
|
||||||
countIssue(condition.copy(state = "open" ), filterUser, false, userRepos: _*),
|
countIssue(condition.copy(state = "open" ), false, userRepos: _*),
|
||||||
countIssue(condition.copy(state = "closed"), filterUser, false, userRepos: _*),
|
countIssue(condition.copy(state = "closed"), false, userRepos: _*),
|
||||||
condition,
|
filter match {
|
||||||
|
case "assigned" => condition.copy(assigned = Some(userName))
|
||||||
|
case "mentioned" => condition.copy(mentioned = Some(userName))
|
||||||
|
case _ => condition.copy(author = Some(userName))
|
||||||
|
},
|
||||||
filter,
|
filter,
|
||||||
getGroupNames(userName))
|
getGroupNames(userName))
|
||||||
}
|
}
|
||||||
|
|
||||||
private def searchPullRequests(filter: String, repository: Option[String]) = {
|
private def searchPullRequests(filter: String) = {
|
||||||
import IssuesService._
|
import IssuesService._
|
||||||
import PullRequestService._
|
import PullRequestService._
|
||||||
|
|
||||||
// condition
|
|
||||||
val condition = session.putAndGet(Keys.Session.DashboardPulls, {
|
|
||||||
if(request.hasQueryString) IssueSearchCondition(request)
|
|
||||||
else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition())
|
|
||||||
}.copy(repo = repository))
|
|
||||||
|
|
||||||
val userName = context.loginAccount.get.userName
|
val userName = context.loginAccount.get.userName
|
||||||
|
val condition = getOrCreateCondition(Keys.Session.DashboardPulls, filter, userName)
|
||||||
val allRepos = getAllRepositories(userName)
|
val allRepos = getAllRepositories(userName)
|
||||||
val filterUser = Map(filter -> userName)
|
|
||||||
val page = IssueSearchCondition.page(request)
|
val page = IssueSearchCondition.page(request)
|
||||||
|
|
||||||
dashboard.html.pulls(
|
dashboard.html.pulls(
|
||||||
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
|
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
|
||||||
page,
|
page,
|
||||||
countIssue(condition.copy(state = "open" ), filterUser, true, allRepos: _*),
|
countIssue(condition.copy(state = "open" ), true, allRepos: _*),
|
||||||
countIssue(condition.copy(state = "closed"), filterUser, true, allRepos: _*),
|
countIssue(condition.copy(state = "closed"), true, allRepos: _*),
|
||||||
condition,
|
filter match {
|
||||||
|
case "assigned" => condition.copy(assigned = Some(userName))
|
||||||
|
case "mentioned" => condition.copy(mentioned = Some(userName))
|
||||||
|
case _ => condition.copy(author = Some(userName))
|
||||||
|
},
|
||||||
filter,
|
filter,
|
||||||
getGroupNames(userName))
|
getGroupNames(userName))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import util.Implicits._
|
|||||||
import util.ControlUtil._
|
import util.ControlUtil._
|
||||||
import org.scalatra.Ok
|
import org.scalatra.Ok
|
||||||
import model.Issue
|
import model.Issue
|
||||||
import plugin.PluginSystem
|
|
||||||
|
|
||||||
class IssuesController extends IssuesControllerBase
|
class IssuesController extends IssuesControllerBase
|
||||||
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
||||||
@@ -50,7 +49,12 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
)(IssueStateForm.apply)
|
)(IssueStateForm.apply)
|
||||||
|
|
||||||
get("/:owner/:repository/issues")(referrersOnly { repository =>
|
get("/:owner/:repository/issues")(referrersOnly { repository =>
|
||||||
|
val q = request.getParameter("q")
|
||||||
|
if(Option(q).exists(_.contains("is:pr"))){
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/pulls?q=" + StringUtil.urlEncode(q))
|
||||||
|
} else {
|
||||||
searchIssues(repository)
|
searchIssues(repository)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
|
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
|
||||||
@@ -288,8 +292,7 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
(Directory.getAttachedDir(repository.owner, repository.name) match {
|
(Directory.getAttachedDir(repository.owner, repository.name) match {
|
||||||
case dir if(dir.exists && dir.isDirectory) =>
|
case dir if(dir.exists && dir.isDirectory) =>
|
||||||
dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
|
dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
|
||||||
contentType = FileUtil.getMimeType(file.getName)
|
RawData(FileUtil.getMimeType(file.getName), file)
|
||||||
file
|
|
||||||
}
|
}
|
||||||
case _ => None
|
case _ => None
|
||||||
}) getOrElse NotFound
|
}) getOrElse NotFound
|
||||||
@@ -390,19 +393,25 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
// retrieve search condition
|
// retrieve search condition
|
||||||
val condition = session.putAndGet(sessionKey,
|
val condition = session.putAndGet(sessionKey,
|
||||||
if(request.hasQueryString) IssueSearchCondition(request)
|
if(request.hasQueryString){
|
||||||
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
|
val q = request.getParameter("q")
|
||||||
|
if(q == null){
|
||||||
|
IssueSearchCondition(request)
|
||||||
|
} else {
|
||||||
|
IssueSearchCondition(q, getMilestones(owner, repoName).map(x => (x.title, x.milestoneId)).toMap)
|
||||||
|
}
|
||||||
|
} else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
|
||||||
)
|
)
|
||||||
|
|
||||||
issues.html.list(
|
issues.html.list(
|
||||||
"issues",
|
"issues",
|
||||||
searchIssue(condition, Map.empty, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
|
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
|
||||||
page,
|
page,
|
||||||
(getCollaborators(owner, repoName) :+ owner).sorted,
|
(getCollaborators(owner, repoName) :+ owner).sorted,
|
||||||
getMilestones(owner, repoName),
|
getMilestones(owner, repoName),
|
||||||
getLabels(owner, repoName),
|
getLabels(owner, repoName),
|
||||||
countIssue(condition.copy(state = "open" ), Map.empty, false, owner -> repoName),
|
countIssue(condition.copy(state = "open" ), false, owner -> repoName),
|
||||||
countIssue(condition.copy(state = "closed"), Map.empty, false, owner -> repoName),
|
countIssue(condition.copy(state = "closed"), false, owner -> repoName),
|
||||||
condition,
|
condition,
|
||||||
repository,
|
repository,
|
||||||
hasWritePermission(owner, repoName, context.loginAccount))
|
hasWritePermission(owner, repoName, context.loginAccount))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier, Keys}
|
import util._
|
||||||
import util.Directory._
|
import util.Directory._
|
||||||
import util.Implicits._
|
import util.Implicits._
|
||||||
import util.ControlUtil._
|
import util.ControlUtil._
|
||||||
@@ -12,20 +12,21 @@ import scala.collection.JavaConverters._
|
|||||||
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
|
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
|
||||||
import service.IssuesService._
|
import service.IssuesService._
|
||||||
import service.PullRequestService._
|
import service.PullRequestService._
|
||||||
import util.JGitUtil.DiffInfo
|
|
||||||
import util.JGitUtil.CommitInfo
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.eclipse.jgit.merge.MergeStrategy
|
import org.eclipse.jgit.merge.MergeStrategy
|
||||||
import org.eclipse.jgit.errors.NoMergeBaseException
|
import org.eclipse.jgit.errors.NoMergeBaseException
|
||||||
import service.WebHookService.WebHookPayload
|
import service.WebHookService.WebHookPayload
|
||||||
|
import util.JGitUtil.DiffInfo
|
||||||
|
import util.JGitUtil.CommitInfo
|
||||||
|
|
||||||
|
|
||||||
class PullRequestsController extends PullRequestsControllerBase
|
class PullRequestsController extends PullRequestsControllerBase
|
||||||
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
|
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
|
||||||
with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
with CommitsService with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||||
|
|
||||||
trait PullRequestsControllerBase extends ControllerBase {
|
trait PullRequestsControllerBase extends ControllerBase {
|
||||||
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
|
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
|
||||||
with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
with CommitsService with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
|
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
|
||||||
|
|
||||||
@@ -59,7 +60,12 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
case class MergeForm(message: String)
|
case class MergeForm(message: String)
|
||||||
|
|
||||||
get("/:owner/:repository/pulls")(referrersOnly { repository =>
|
get("/:owner/:repository/pulls")(referrersOnly { repository =>
|
||||||
|
val q = request.getParameter("q")
|
||||||
|
if(Option(q).exists(_.contains("is:issue"))){
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/issues?q=" + StringUtil.urlEncode(q))
|
||||||
|
} else {
|
||||||
searchPullRequests(None, repository)
|
searchPullRequests(None, repository)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
|
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
|
||||||
@@ -73,7 +79,8 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
pulls.html.pullreq(
|
pulls.html.pullreq(
|
||||||
issue, pullreq,
|
issue, pullreq,
|
||||||
getComments(owner, name, issueId),
|
(commits.flatten.map(commit => getCommitComments(owner, name, commit.id, true)).flatten.toList ::: getComments(owner, name, issueId))
|
||||||
|
.sortWith((a, b) => a.registeredDate before b.registeredDate),
|
||||||
getIssueLabels(owner, name, issueId),
|
getIssueLabels(owner, name, issueId),
|
||||||
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
|
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
|
||||||
getMilestonesWithIssueCount(owner, name),
|
getMilestonesWithIssueCount(owner, name),
|
||||||
@@ -273,6 +280,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
|
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
|
||||||
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
|
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
|
||||||
},
|
},
|
||||||
|
commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList,
|
||||||
originBranch,
|
originBranch,
|
||||||
forkedBranch,
|
forkedBranch,
|
||||||
oldId.getName,
|
oldId.getName,
|
||||||
@@ -460,13 +468,13 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
issues.html.list(
|
issues.html.list(
|
||||||
"pulls",
|
"pulls",
|
||||||
searchIssue(condition, Map.empty, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
|
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
|
||||||
page,
|
page,
|
||||||
(getCollaborators(owner, repoName) :+ owner).sorted,
|
(getCollaborators(owner, repoName) :+ owner).sorted,
|
||||||
getMilestones(owner, repoName),
|
getMilestones(owner, repoName),
|
||||||
getLabels(owner, repoName),
|
getLabels(owner, repoName),
|
||||||
countIssue(condition.copy(state = "open" ), Map.empty, true, owner -> repoName),
|
countIssue(condition.copy(state = "open" ), true, owner -> repoName),
|
||||||
countIssue(condition.copy(state = "closed"), Map.empty, true, owner -> repoName),
|
countIssue(condition.copy(state = "closed"), true, owner -> repoName),
|
||||||
condition,
|
condition,
|
||||||
repository,
|
repository,
|
||||||
hasWritePermission(owner, repoName, context.loginAccount))
|
hasWritePermission(owner, repoName, context.loginAccount))
|
||||||
|
|||||||
@@ -20,16 +20,16 @@ import org.eclipse.jgit.revwalk.RevCommit
|
|||||||
import service.WebHookService.WebHookPayload
|
import service.WebHookService.WebHookPayload
|
||||||
|
|
||||||
class RepositoryViewerController extends RepositoryViewerControllerBase
|
class RepositoryViewerController extends RepositoryViewerControllerBase
|
||||||
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService
|
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
||||||
with ReferrerAuthenticator with CollaboratorsAuthenticator
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The repository viewer.
|
* The repository viewer.
|
||||||
*/
|
*/
|
||||||
trait RepositoryViewerControllerBase extends ControllerBase {
|
trait RepositoryViewerControllerBase extends ControllerBase {
|
||||||
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService
|
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
||||||
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService =>
|
||||||
|
|
||||||
ArchiveCommand.registerFormat("zip", new ZipFormat)
|
ArchiveCommand.registerFormat("zip", new ZipFormat)
|
||||||
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
|
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
|
||||||
@@ -52,6 +52,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
fileName: String
|
fileName: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
case class CommentForm(
|
||||||
|
fileName: Option[String],
|
||||||
|
oldLineNumber: Option[Int],
|
||||||
|
newLineNumber: Option[Int],
|
||||||
|
content: String,
|
||||||
|
issueId: Option[Int]
|
||||||
|
)
|
||||||
|
|
||||||
val editorForm = mapping(
|
val editorForm = mapping(
|
||||||
"branch" -> trim(label("Branch", text(required))),
|
"branch" -> trim(label("Branch", text(required))),
|
||||||
"path" -> trim(label("Path", text())),
|
"path" -> trim(label("Path", text())),
|
||||||
@@ -70,6 +78,14 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
"fileName" -> trim(label("Filename", text(required)))
|
"fileName" -> trim(label("Filename", text(required)))
|
||||||
)(DeleteForm.apply)
|
)(DeleteForm.apply)
|
||||||
|
|
||||||
|
val commentForm = mapping(
|
||||||
|
"fileName" -> trim(label("Filename", optional(text()))),
|
||||||
|
"oldLineNumber" -> trim(label("Old line number", optional(number()))),
|
||||||
|
"newLineNumber" -> trim(label("New line number", optional(number()))),
|
||||||
|
"content" -> trim(label("Content", text(required))),
|
||||||
|
"issueId" -> trim(label("Issue Id", optional(number())))
|
||||||
|
)(CommentForm.apply)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns converted HTML from Markdown for preview.
|
* Returns converted HTML from Markdown for preview.
|
||||||
*/
|
*/
|
||||||
@@ -198,8 +214,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
if(raw){
|
if(raw){
|
||||||
// Download
|
// Download
|
||||||
defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes =>
|
defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes =>
|
||||||
contentType = FileUtil.getContentType(path, bytes)
|
RawData(FileUtil.getContentType(path, bytes), bytes)
|
||||||
bytes
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId),
|
repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId),
|
||||||
@@ -221,12 +236,88 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
|
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
|
||||||
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
|
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
|
||||||
JGitUtil.getTagsOfCommit(git, revCommit.getName),
|
JGitUtil.getTagsOfCommit(git, revCommit.getName),
|
||||||
repository, diffs, oldCommitId)
|
getCommitComments(repository.owner, repository.name, id, false),
|
||||||
|
repository, diffs, oldCommitId, hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
post("/:owner/:repository/commit/:id/comment/new", commentForm)(readableUsersOnly { (form, repository) =>
|
||||||
|
val id = params("id")
|
||||||
|
createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName, form.content,
|
||||||
|
form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined)
|
||||||
|
form.issueId match {
|
||||||
|
case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
|
||||||
|
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
|
||||||
|
}
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/commit/${id}")
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxGet("/:owner/:repository/commit/:id/comment/_form")(readableUsersOnly { repository =>
|
||||||
|
val id = params("id")
|
||||||
|
val fileName = params.get("fileName")
|
||||||
|
val oldLineNumber = params.get("oldLineNumber") map (_.toInt)
|
||||||
|
val newLineNumber = params.get("newLineNumber") map (_.toInt)
|
||||||
|
val issueId = params.get("issueId") map (_.toInt)
|
||||||
|
repo.html.commentform(
|
||||||
|
commitId = id,
|
||||||
|
fileName, oldLineNumber, newLineNumber, issueId,
|
||||||
|
hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount),
|
||||||
|
repository = repository
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/commit/:id/comment/_data/new", commentForm)(readableUsersOnly { (form, repository) =>
|
||||||
|
val id = params("id")
|
||||||
|
val commentId = createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName,
|
||||||
|
form.content, form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined)
|
||||||
|
form.issueId match {
|
||||||
|
case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
|
||||||
|
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
|
||||||
|
}
|
||||||
|
helper.html.commitcomment(getCommitComment(repository.owner, repository.name, commentId.toString).get,
|
||||||
|
hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxGet("/:owner/:repository/commit_comments/_data/:id")(readableUsersOnly { repository =>
|
||||||
|
getCommitComment(repository.owner, repository.name, params("id")) map { x =>
|
||||||
|
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
|
||||||
|
params.get("dataType") collect {
|
||||||
|
case t if t == "html" => repo.html.editcomment(
|
||||||
|
x.content, x.commentId, x.userName, x.repositoryName)
|
||||||
|
} getOrElse {
|
||||||
|
contentType = formats("json")
|
||||||
|
org.json4s.jackson.Serialization.write(
|
||||||
|
Map("content" -> view.Markdown.toHtml(x.content,
|
||||||
|
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else Unauthorized
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/commit_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
|
||||||
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
|
getCommitComment(owner, name, params("id")).map { comment =>
|
||||||
|
if(isEditable(owner, name, comment.commentedUserName)){
|
||||||
|
updateCommitComment(comment.commentId, form.content)
|
||||||
|
redirect(s"/${owner}/${name}/commit_comments/_data/${comment.commentId}")
|
||||||
|
} else Unauthorized
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/commit_comments/delete/:id")(readableUsersOnly { repository =>
|
||||||
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
|
getCommitComment(owner, name, params("id")).map { comment =>
|
||||||
|
if(isEditable(owner, name, comment.commentedUserName)){
|
||||||
|
Ok(deleteCommitComment(comment.commentId))
|
||||||
|
} else Unauthorized
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays branches.
|
* Displays branches.
|
||||||
*/
|
*/
|
||||||
@@ -350,6 +441,10 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
repo.html.files(revision, repository,
|
repo.html.files(revision, repository,
|
||||||
if(path == ".") Nil else path.split("/").toList, // current path
|
if(path == ".") Nil else path.split("/").toList, // current path
|
||||||
|
context.loginAccount match {
|
||||||
|
case None => List()
|
||||||
|
case account: Option[model.Account] => getGroupsByUserName(account.get.userName)
|
||||||
|
}, // groups of current user
|
||||||
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
|
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
|
||||||
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
|
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
|
||||||
flash.get("info"), flash.get("error"))
|
flash.get("info"), flash.get("error"))
|
||||||
@@ -400,6 +495,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
//refUpdate.setRefLogMessage("merged", true)
|
//refUpdate.setRefLogMessage("merged", true)
|
||||||
refUpdate.update()
|
refUpdate.update()
|
||||||
|
|
||||||
|
// update pull request
|
||||||
|
updatePullRequests(repository.owner, repository.name, branch)
|
||||||
|
|
||||||
// record activity
|
// record activity
|
||||||
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch,
|
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch,
|
||||||
List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))))
|
List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))))
|
||||||
@@ -436,7 +534,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): File = {
|
private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): Unit = {
|
||||||
val revision = name.stripSuffix(suffix)
|
val revision = name.stripSuffix(suffix)
|
||||||
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
|
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
|
||||||
if(workDir.exists) {
|
if(workDir.exists) {
|
||||||
@@ -444,21 +542,26 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
workDir.mkdirs
|
workDir.mkdirs
|
||||||
|
|
||||||
val file = new File(workDir, repository.name + "-" +
|
val filename = repository.name + "-" +
|
||||||
(if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + suffix)
|
(if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + suffix
|
||||||
|
|
||||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
|
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
|
||||||
using(new java.io.FileOutputStream(file)) { out =>
|
|
||||||
|
contentType = "application/octet-stream"
|
||||||
|
response.setHeader("Content-Disposition", s"attachment; filename=${filename}")
|
||||||
|
response.setBufferSize(1024 * 1024);
|
||||||
|
|
||||||
git.archive
|
git.archive
|
||||||
.setFormat(suffix.tail)
|
.setFormat(suffix.tail)
|
||||||
.setTree(revCommit.getTree)
|
.setTree(revCommit.getTree)
|
||||||
.setOutputStream(out)
|
.setOutputStream(response.getOutputStream)
|
||||||
.call()
|
.call()
|
||||||
}
|
|
||||||
contentType = "application/octet-stream"
|
Unit
|
||||||
response.setHeader("Content-Disposition", s"attachment; filename=${file.getName}")
|
|
||||||
file
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
|
||||||
|
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,8 @@ package app
|
|||||||
import service.{AccountService, SystemSettingsService}
|
import service.{AccountService, SystemSettingsService}
|
||||||
import SystemSettingsService._
|
import SystemSettingsService._
|
||||||
import util.AdminAuthenticator
|
import util.AdminAuthenticator
|
||||||
import util.Directory._
|
|
||||||
import util.ControlUtil._
|
|
||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
import ssh.SshServer
|
import ssh.SshServer
|
||||||
import org.apache.commons.io.FileUtils
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import plugin.{Plugin, PluginSystem}
|
|
||||||
import org.scalatra.Ok
|
|
||||||
import util.Implicits._
|
|
||||||
|
|
||||||
class SystemSettingsController extends SystemSettingsControllerBase
|
class SystemSettingsController extends SystemSettingsControllerBase
|
||||||
with AccountService with AdminAuthenticator
|
with AccountService with AdminAuthenticator
|
||||||
@@ -21,7 +14,10 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
private val form = mapping(
|
private val form = mapping(
|
||||||
"baseUrl" -> trim(label("Base URL", optional(text()))),
|
"baseUrl" -> trim(label("Base URL", optional(text()))),
|
||||||
|
"information" -> trim(label("Information", optional(text()))),
|
||||||
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
|
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
|
||||||
|
"allowAnonymousAccess" -> trim(label("Anonymous access", boolean())),
|
||||||
|
"isCreateRepoOptionPublic" -> trim(label("Default option to create a new repository", boolean())),
|
||||||
"gravatar" -> trim(label("Gravatar", boolean())),
|
"gravatar" -> trim(label("Gravatar", boolean())),
|
||||||
"notification" -> trim(label("Notification", boolean())),
|
"notification" -> trim(label("Notification", boolean())),
|
||||||
"ssh" -> trim(label("SSH access", boolean())),
|
"ssh" -> trim(label("SSH access", boolean())),
|
||||||
@@ -47,6 +43,7 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
|||||||
"fullNameAttribute" -> trim(label("Full name attribute", optional(text()))),
|
"fullNameAttribute" -> trim(label("Full name attribute", optional(text()))),
|
||||||
"mailAttribute" -> trim(label("Mail address attribute", optional(text()))),
|
"mailAttribute" -> trim(label("Mail address attribute", optional(text()))),
|
||||||
"tls" -> trim(label("Enable TLS", optional(boolean()))),
|
"tls" -> trim(label("Enable TLS", optional(boolean()))),
|
||||||
|
"ssl" -> trim(label("Enable SSL", optional(boolean()))),
|
||||||
"keystore" -> trim(label("Keystore", optional(text())))
|
"keystore" -> trim(label("Keystore", optional(text())))
|
||||||
)(Ldap.apply))
|
)(Ldap.apply))
|
||||||
)(SystemSettings.apply).verifying { settings =>
|
)(SystemSettings.apply).verifying { settings =>
|
||||||
@@ -84,118 +81,4 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
|||||||
redirect("/admin/system")
|
redirect("/admin/system")
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/admin/plugins")(adminOnly {
|
|
||||||
if(enablePluginSystem){
|
|
||||||
val installedPlugins = plugin.PluginSystem.plugins
|
|
||||||
val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable")
|
|
||||||
admin.plugins.html.installed(installedPlugins, updatablePlugins)
|
|
||||||
} else NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/admin/plugins/_update", pluginForm)(adminOnly { form =>
|
|
||||||
if(enablePluginSystem){
|
|
||||||
deletePlugins(form.pluginIds)
|
|
||||||
installPlugins(form.pluginIds)
|
|
||||||
redirect("/admin/plugins")
|
|
||||||
} else NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/admin/plugins/_delete", pluginForm)(adminOnly { form =>
|
|
||||||
if(enablePluginSystem){
|
|
||||||
deletePlugins(form.pluginIds)
|
|
||||||
redirect("/admin/plugins")
|
|
||||||
} else NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/admin/plugins/available")(adminOnly {
|
|
||||||
if(enablePluginSystem){
|
|
||||||
val installedPlugins = plugin.PluginSystem.plugins
|
|
||||||
val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available")
|
|
||||||
admin.plugins.html.available(availablePlugins)
|
|
||||||
} else NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/admin/plugins/_install", pluginForm)(adminOnly { form =>
|
|
||||||
if(enablePluginSystem){
|
|
||||||
installPlugins(form.pluginIds)
|
|
||||||
redirect("/admin/plugins")
|
|
||||||
} else NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/admin/plugins/console")(adminOnly {
|
|
||||||
if(enablePluginSystem){
|
|
||||||
admin.plugins.html.console()
|
|
||||||
} else NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/admin/plugins/console")(adminOnly {
|
|
||||||
if(enablePluginSystem){
|
|
||||||
val script = request.getParameter("script")
|
|
||||||
val result = plugin.ScalaPlugin.eval(script)
|
|
||||||
Ok()
|
|
||||||
} else NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO Move these methods to PluginSystem or Service?
|
|
||||||
private def deletePlugins(pluginIds: List[String]): Unit = {
|
|
||||||
pluginIds.foreach { pluginId =>
|
|
||||||
plugin.PluginSystem.uninstall(pluginId)
|
|
||||||
val dir = new java.io.File(PluginHome, pluginId)
|
|
||||||
if(dir.exists && dir.isDirectory){
|
|
||||||
FileUtils.deleteQuietly(dir)
|
|
||||||
PluginSystem.uninstall(pluginId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def installPlugins(pluginIds: List[String]): Unit = {
|
|
||||||
val dir = getPluginCacheDir()
|
|
||||||
val installedPlugins = plugin.PluginSystem.plugins
|
|
||||||
getAvailablePlugins(installedPlugins).filter(x => pluginIds.contains(x.id)).foreach { plugin =>
|
|
||||||
val pluginDir = new java.io.File(PluginHome, plugin.id)
|
|
||||||
if(pluginDir.exists){
|
|
||||||
FileUtils.deleteDirectory(pluginDir)
|
|
||||||
}
|
|
||||||
FileUtils.copyDirectory(new java.io.File(dir, plugin.repository + "/" + plugin.id), pluginDir)
|
|
||||||
PluginSystem.installPlugin(plugin.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def getAvailablePlugins(installedPlugins: List[Plugin]): List[SystemSettingsControllerBase.AvailablePlugin] = {
|
|
||||||
val repositoryRoot = getPluginCacheDir()
|
|
||||||
|
|
||||||
if(repositoryRoot.exists && repositoryRoot.isDirectory){
|
|
||||||
PluginSystem.repositories.flatMap { repo =>
|
|
||||||
val repoDir = new java.io.File(repositoryRoot, repo.id)
|
|
||||||
if(repoDir.exists && repoDir.isDirectory){
|
|
||||||
repoDir.listFiles.filter(d => d.isDirectory && !d.getName.startsWith(".")).map { plugin =>
|
|
||||||
val propertyFile = new java.io.File(plugin, "plugin.properties")
|
|
||||||
val properties = new java.util.Properties()
|
|
||||||
if(propertyFile.exists && propertyFile.isFile){
|
|
||||||
using(new FileInputStream(propertyFile)){ in =>
|
|
||||||
properties.load(in)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SystemSettingsControllerBase.AvailablePlugin(
|
|
||||||
repository = repo.id,
|
|
||||||
id = properties.getProperty("id"),
|
|
||||||
version = properties.getProperty("version"),
|
|
||||||
author = properties.getProperty("author"),
|
|
||||||
url = properties.getProperty("url"),
|
|
||||||
description = properties.getProperty("description"),
|
|
||||||
status = installedPlugins.find(_.id == properties.getProperty("id")) match {
|
|
||||||
case Some(x) if(PluginSystem.isUpdatable(x.version, properties.getProperty("version")))=> "updatable"
|
|
||||||
case Some(x) => "installed"
|
|
||||||
case None => "available"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else Nil
|
|
||||||
}
|
|
||||||
} else Nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object SystemSettingsControllerBase {
|
|
||||||
case class AvailablePlugin(repository: String, id: String, version: String,
|
|
||||||
author: String, url: String, description: String, status: String)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,8 +164,7 @@ trait WikiControllerBase extends ControllerBase {
|
|||||||
val path = multiParams("splat").head
|
val path = multiParams("splat").head
|
||||||
|
|
||||||
getFileContent(repository.owner, repository.name, path).map { bytes =>
|
getFileContent(repository.owner, repository.name, path).map { bytes =>
|
||||||
contentType = FileUtil.getContentType(path, bytes)
|
RawData(FileUtil.getContentType(path, bytes), bytes)
|
||||||
bytes
|
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -44,4 +44,11 @@ protected[model] trait TemplateComponent { self: Profile =>
|
|||||||
byRepository(userName, repositoryName) && (this.milestoneId === milestoneId)
|
byRepository(userName, repositoryName) && (this.milestoneId === milestoneId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trait CommitTemplate extends BasicTemplate { self: Table[_] =>
|
||||||
|
val commitId = column[String]("COMMIT_ID")
|
||||||
|
|
||||||
|
def byCommit(owner: String, repository: String, commitId: String) =
|
||||||
|
byRepository(owner, repository) && (this.commitId === commitId)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
78
src/main/scala/model/Comment.scala
Normal file
78
src/main/scala/model/Comment.scala
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
trait Comment {
|
||||||
|
val commentedUserName: String
|
||||||
|
val registeredDate: java.util.Date
|
||||||
|
}
|
||||||
|
|
||||||
|
trait IssueCommentComponent extends TemplateComponent { self: Profile =>
|
||||||
|
import profile.simple._
|
||||||
|
import self._
|
||||||
|
|
||||||
|
lazy val IssueComments = new TableQuery(tag => new IssueComments(tag)){
|
||||||
|
def autoInc = this returning this.map(_.commentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
class IssueComments(tag: Tag) extends Table[IssueComment](tag, "ISSUE_COMMENT") with IssueTemplate {
|
||||||
|
val commentId = column[Int]("COMMENT_ID", O AutoInc)
|
||||||
|
val action = column[String]("ACTION")
|
||||||
|
val commentedUserName = column[String]("COMMENTED_USER_NAME")
|
||||||
|
val content = column[String]("CONTENT")
|
||||||
|
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
||||||
|
val updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||||
|
def * = (userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate) <> (IssueComment.tupled, IssueComment.unapply)
|
||||||
|
|
||||||
|
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class IssueComment (
|
||||||
|
userName: String,
|
||||||
|
repositoryName: String,
|
||||||
|
issueId: Int,
|
||||||
|
commentId: Int = 0,
|
||||||
|
action: String,
|
||||||
|
commentedUserName: String,
|
||||||
|
content: String,
|
||||||
|
registeredDate: java.util.Date,
|
||||||
|
updatedDate: java.util.Date
|
||||||
|
) extends Comment
|
||||||
|
|
||||||
|
trait CommitCommentComponent extends TemplateComponent { self: Profile =>
|
||||||
|
import profile.simple._
|
||||||
|
import self._
|
||||||
|
|
||||||
|
lazy val CommitComments = new TableQuery(tag => new CommitComments(tag)){
|
||||||
|
def autoInc = this returning this.map(_.commentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
class CommitComments(tag: Tag) extends Table[CommitComment](tag, "COMMIT_COMMENT") with CommitTemplate {
|
||||||
|
val commentId = column[Int]("COMMENT_ID", O AutoInc)
|
||||||
|
val commentedUserName = column[String]("COMMENTED_USER_NAME")
|
||||||
|
val content = column[String]("CONTENT")
|
||||||
|
val fileName = column[Option[String]]("FILE_NAME")
|
||||||
|
val oldLine = column[Option[Int]]("OLD_LINE_NUMBER")
|
||||||
|
val newLine = column[Option[Int]]("NEW_LINE_NUMBER")
|
||||||
|
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
||||||
|
val updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||||
|
val pullRequest = column[Boolean]("PULL_REQUEST")
|
||||||
|
def * = (userName, repositoryName, commitId, commentId, commentedUserName, content, fileName, oldLine, newLine, registeredDate, updatedDate, pullRequest) <> (CommitComment.tupled, CommitComment.unapply)
|
||||||
|
|
||||||
|
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class CommitComment(
|
||||||
|
userName: String,
|
||||||
|
repositoryName: String,
|
||||||
|
commitId: String,
|
||||||
|
commentId: Int = 0,
|
||||||
|
commentedUserName: String,
|
||||||
|
content: String,
|
||||||
|
fileName: Option[String],
|
||||||
|
oldLine: Option[Int],
|
||||||
|
newLine: Option[Int],
|
||||||
|
registeredDate: java.util.Date,
|
||||||
|
updatedDate: java.util.Date,
|
||||||
|
pullRequest: Boolean
|
||||||
|
) extends Comment
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
trait IssueCommentComponent extends TemplateComponent { self: Profile =>
|
|
||||||
import profile.simple._
|
|
||||||
import self._
|
|
||||||
|
|
||||||
lazy val IssueComments = new TableQuery(tag => new IssueComments(tag)){
|
|
||||||
def autoInc = this returning this.map(_.commentId)
|
|
||||||
}
|
|
||||||
|
|
||||||
class IssueComments(tag: Tag) extends Table[IssueComment](tag, "ISSUE_COMMENT") with IssueTemplate {
|
|
||||||
val commentId = column[Int]("COMMENT_ID", O AutoInc)
|
|
||||||
val action = column[String]("ACTION")
|
|
||||||
val commentedUserName = column[String]("COMMENTED_USER_NAME")
|
|
||||||
val content = column[String]("CONTENT")
|
|
||||||
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
|
||||||
val updatedDate = column[java.util.Date]("UPDATED_DATE")
|
|
||||||
def * = (userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate) <> (IssueComment.tupled, IssueComment.unapply)
|
|
||||||
|
|
||||||
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case class IssueComment(
|
|
||||||
userName: String,
|
|
||||||
repositoryName: String,
|
|
||||||
issueId: Int,
|
|
||||||
commentId: Int = 0,
|
|
||||||
action: String,
|
|
||||||
commentedUserName: String,
|
|
||||||
content: String,
|
|
||||||
registeredDate: java.util.Date,
|
|
||||||
updatedDate: java.util.Date
|
|
||||||
)
|
|
||||||
@@ -22,6 +22,7 @@ object Profile extends {
|
|||||||
} with AccountComponent
|
} with AccountComponent
|
||||||
with ActivityComponent
|
with ActivityComponent
|
||||||
with CollaboratorComponent
|
with CollaboratorComponent
|
||||||
|
with CommitCommentComponent
|
||||||
with GroupMemberComponent
|
with GroupMemberComponent
|
||||||
with IssueComponent
|
with IssueComponent
|
||||||
with IssueCommentComponent
|
with IssueCommentComponent
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package plugin
|
|
||||||
|
|
||||||
import plugin.PluginSystem._
|
|
||||||
import java.sql.Connection
|
|
||||||
|
|
||||||
trait Plugin {
|
|
||||||
val id: String
|
|
||||||
val version: String
|
|
||||||
val author: String
|
|
||||||
val url: String
|
|
||||||
val description: String
|
|
||||||
|
|
||||||
def repositoryMenus : List[RepositoryMenu]
|
|
||||||
def globalMenus : List[GlobalMenu]
|
|
||||||
def repositoryActions : List[RepositoryAction]
|
|
||||||
def globalActions : List[Action]
|
|
||||||
def javaScripts : List[JavaScript]
|
|
||||||
}
|
|
||||||
|
|
||||||
object PluginConnectionHolder {
|
|
||||||
val threadLocal = new ThreadLocal[Connection]
|
|
||||||
}
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
package plugin
|
|
||||||
|
|
||||||
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
import util.Directory._
|
|
||||||
import util.ControlUtil._
|
|
||||||
import org.apache.commons.io.{IOUtils, FileUtils}
|
|
||||||
import Security._
|
|
||||||
import service.PluginService
|
|
||||||
import model.Profile._
|
|
||||||
import profile.simple._
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.sql.Connection
|
|
||||||
import app.Context
|
|
||||||
import service.RepositoryService.RepositoryInfo
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides extension points to plug-ins.
|
|
||||||
*/
|
|
||||||
object PluginSystem extends PluginService {
|
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(PluginSystem.getClass)
|
|
||||||
|
|
||||||
private val initialized = new AtomicBoolean(false)
|
|
||||||
private val pluginsMap = scala.collection.mutable.Map[String, Plugin]()
|
|
||||||
private val repositoriesList = scala.collection.mutable.ListBuffer[PluginRepository]()
|
|
||||||
|
|
||||||
def install(plugin: Plugin): Unit = {
|
|
||||||
pluginsMap.put(plugin.id, plugin)
|
|
||||||
}
|
|
||||||
|
|
||||||
def plugins: List[Plugin] = pluginsMap.values.toList
|
|
||||||
|
|
||||||
def uninstall(id: String)(implicit session: Session): Unit = {
|
|
||||||
pluginsMap.remove(id)
|
|
||||||
|
|
||||||
// Delete from PLUGIN table
|
|
||||||
deletePlugin(id)
|
|
||||||
|
|
||||||
// Drop tables
|
|
||||||
val pluginDir = new java.io.File(PluginHome)
|
|
||||||
val sqlFile = new java.io.File(pluginDir, s"${id}/sql/drop.sql")
|
|
||||||
if(sqlFile.exists){
|
|
||||||
val sql = IOUtils.toString(new FileInputStream(sqlFile), "UTF-8")
|
|
||||||
using(session.conn.createStatement()){ stmt =>
|
|
||||||
stmt.executeUpdate(sql)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def repositories: List[PluginRepository] = repositoriesList.toList
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the plugin system. Load scripts from GITBUCKET_HOME/plugins.
|
|
||||||
*/
|
|
||||||
def init()(implicit session: Session): Unit = {
|
|
||||||
if(initialized.compareAndSet(false, true)){
|
|
||||||
// Load installed plugins
|
|
||||||
val pluginDir = new java.io.File(PluginHome)
|
|
||||||
if(pluginDir.exists && pluginDir.isDirectory){
|
|
||||||
pluginDir.listFiles.filter(f => f.isDirectory && !f.getName.startsWith(".")).foreach { dir =>
|
|
||||||
installPlugin(dir.getName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Add default plugin repositories
|
|
||||||
repositoriesList += PluginRepository("central", "https://github.com/takezoe/gitbucket_plugins.git")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Method name seems to not so good.
|
|
||||||
def installPlugin(id: String)(implicit session: Session): Unit = {
|
|
||||||
val pluginHome = new java.io.File(PluginHome)
|
|
||||||
val pluginDir = new java.io.File(pluginHome, id)
|
|
||||||
|
|
||||||
val scalaFile = new java.io.File(pluginDir, "plugin.scala")
|
|
||||||
if(scalaFile.exists && scalaFile.isFile){
|
|
||||||
val properties = new java.util.Properties()
|
|
||||||
using(new java.io.FileInputStream(new java.io.File(pluginDir, "plugin.properties"))){ in =>
|
|
||||||
properties.load(in)
|
|
||||||
}
|
|
||||||
|
|
||||||
val pluginId = properties.getProperty("id")
|
|
||||||
val version = properties.getProperty("version")
|
|
||||||
val author = properties.getProperty("author")
|
|
||||||
val url = properties.getProperty("url")
|
|
||||||
val description = properties.getProperty("description")
|
|
||||||
|
|
||||||
val source = s"""
|
|
||||||
|val id = "${pluginId}"
|
|
||||||
|val version = "${version}"
|
|
||||||
|val author = "${author}"
|
|
||||||
|val url = "${url}"
|
|
||||||
|val description = "${description}"
|
|
||||||
""".stripMargin + FileUtils.readFileToString(scalaFile, "UTF-8")
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Compile and eval Scala source code
|
|
||||||
ScalaPlugin.eval(pluginDir.listFiles.filter(_.getName.endsWith(".scala.html")).map { file =>
|
|
||||||
ScalaPlugin.compileTemplate(
|
|
||||||
id.replaceAll("-", ""),
|
|
||||||
file.getName.replaceAll("\\.scala\\.html$", ""),
|
|
||||||
IOUtils.toString(new FileInputStream(file)))
|
|
||||||
}.mkString("\n") + source)
|
|
||||||
|
|
||||||
// Migrate database
|
|
||||||
val plugin = getPlugin(pluginId)
|
|
||||||
if(plugin.isEmpty){
|
|
||||||
registerPlugin(model.Plugin(pluginId, version))
|
|
||||||
migrate(session.conn, pluginId, "0.0")
|
|
||||||
} else {
|
|
||||||
updatePlugin(model.Plugin(pluginId, version))
|
|
||||||
migrate(session.conn, pluginId, plugin.get.version)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
case e: Throwable => logger.warn(s"Error in plugin loading for ${scalaFile.getAbsolutePath}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Should PluginSystem provide a way to migrate resources other than H2?
|
|
||||||
private def migrate(conn: Connection, pluginId: String, current: String): Unit = {
|
|
||||||
val pluginDir = new java.io.File(PluginHome)
|
|
||||||
|
|
||||||
// TODO Is ot possible to use this migration system in GitBucket migration?
|
|
||||||
val dim = current.split("\\.")
|
|
||||||
val currentVersion = Version(dim(0).toInt, dim(1).toInt)
|
|
||||||
|
|
||||||
val sqlDir = new java.io.File(pluginDir, s"${pluginId}/sql")
|
|
||||||
if(sqlDir.exists && sqlDir.isDirectory){
|
|
||||||
sqlDir.listFiles.filter(_.getName.endsWith(".sql")).map { file =>
|
|
||||||
val array = file.getName.replaceFirst("\\.sql", "").split("_")
|
|
||||||
Version(array(0).toInt, array(1).toInt)
|
|
||||||
}
|
|
||||||
.sorted.reverse.takeWhile(_ > currentVersion)
|
|
||||||
.reverse.foreach { version =>
|
|
||||||
val sqlFile = new java.io.File(pluginDir, s"${pluginId}/sql/${version.major}_${version.minor}.sql")
|
|
||||||
val sql = IOUtils.toString(new FileInputStream(sqlFile), "UTF-8")
|
|
||||||
using(conn.createStatement()){ stmt =>
|
|
||||||
stmt.executeUpdate(sql)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case class Version(major: Int, minor: Int) extends Ordered[Version] {
|
|
||||||
|
|
||||||
override def compare(that: Version): Int = {
|
|
||||||
if(major != that.major){
|
|
||||||
major.compare(that.major)
|
|
||||||
} else{
|
|
||||||
minor.compare(that.minor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def displayString: String = major + "." + minor
|
|
||||||
}
|
|
||||||
|
|
||||||
def repositoryMenus : List[RepositoryMenu] = pluginsMap.values.flatMap(_.repositoryMenus).toList
|
|
||||||
def globalMenus : List[GlobalMenu] = pluginsMap.values.flatMap(_.globalMenus).toList
|
|
||||||
def repositoryActions : List[RepositoryAction] = pluginsMap.values.flatMap(_.repositoryActions).toList
|
|
||||||
def globalActions : List[Action] = pluginsMap.values.flatMap(_.globalActions).toList
|
|
||||||
def javaScripts : List[JavaScript] = pluginsMap.values.flatMap(_.javaScripts).toList
|
|
||||||
|
|
||||||
// Case classes to hold plug-ins information internally in GitBucket
|
|
||||||
case class PluginRepository(id: String, url: String)
|
|
||||||
case class GlobalMenu(label: String, url: String, icon: String, condition: Context => Boolean)
|
|
||||||
case class RepositoryMenu(label: String, name: String, url: String, icon: String, condition: Context => Boolean)
|
|
||||||
case class Action(method: String, path: String, security: Security, function: (HttpServletRequest, HttpServletResponse, Context) => Any)
|
|
||||||
case class RepositoryAction(method: String, path: String, security: Security, function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any)
|
|
||||||
case class Button(label: String, href: String)
|
|
||||||
case class JavaScript(filter: String => Boolean, script: String)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the plugin is updatable.
|
|
||||||
*/
|
|
||||||
def isUpdatable(oldVersion: String, newVersion: String): Boolean = {
|
|
||||||
if(oldVersion == newVersion){
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
val dim1 = oldVersion.split("\\.").map(_.toInt)
|
|
||||||
val dim2 = newVersion.split("\\.").map(_.toInt)
|
|
||||||
dim1.zip(dim2).foreach { case (a, b) =>
|
|
||||||
if(a < b){
|
|
||||||
return true
|
|
||||||
} else if(a > b){
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package plugin
|
|
||||||
|
|
||||||
import util.Directory._
|
|
||||||
import org.eclipse.jgit.api.Git
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.quartz.{Scheduler, JobExecutionContext, Job}
|
|
||||||
import org.quartz.JobBuilder._
|
|
||||||
import org.quartz.TriggerBuilder._
|
|
||||||
import org.quartz.SimpleScheduleBuilder._
|
|
||||||
|
|
||||||
class PluginUpdateJob extends Job {
|
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[PluginUpdateJob])
|
|
||||||
private var failedCount = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clone or pull all plugin repositories
|
|
||||||
*
|
|
||||||
* TODO Support plugin repository access through the proxy server
|
|
||||||
*/
|
|
||||||
override def execute(context: JobExecutionContext): Unit = {
|
|
||||||
try {
|
|
||||||
if(failedCount > 3){
|
|
||||||
logger.error("Skip plugin information updating because failed count is over limit")
|
|
||||||
} else {
|
|
||||||
logger.info("Start plugin information updating")
|
|
||||||
PluginSystem.repositories.foreach { repository =>
|
|
||||||
logger.info(s"Updating ${repository.id}: ${repository.url}...")
|
|
||||||
val dir = getPluginCacheDir()
|
|
||||||
val repo = new java.io.File(dir, repository.id)
|
|
||||||
if(repo.exists){
|
|
||||||
// pull if the repository is already cloned
|
|
||||||
Git.open(repo).pull().call()
|
|
||||||
} else {
|
|
||||||
// clone if the repository is not exist
|
|
||||||
Git.cloneRepository().setURI(repository.url).setDirectory(repo).call()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.info("End plugin information updating")
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
case e: Exception => {
|
|
||||||
failedCount = failedCount + 1
|
|
||||||
logger.error("Failed to update plugin information", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object PluginUpdateJob {
|
|
||||||
|
|
||||||
def schedule(scheduler: Scheduler): Unit = {
|
|
||||||
val job = newJob(classOf[PluginUpdateJob])
|
|
||||||
.withIdentity("pluginUpdateJob")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val trigger = newTrigger()
|
|
||||||
.withIdentity("pluginUpdateTrigger")
|
|
||||||
.startNow()
|
|
||||||
.withSchedule(simpleSchedule().withIntervalInHours(24).repeatForever())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
scheduler.scheduleJob(job, trigger)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package plugin
|
|
||||||
|
|
||||||
import scala.collection.mutable.ListBuffer
|
|
||||||
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
|
||||||
import app.Context
|
|
||||||
import plugin.PluginSystem._
|
|
||||||
import plugin.PluginSystem.RepositoryMenu
|
|
||||||
import plugin.Security._
|
|
||||||
import service.RepositoryService.RepositoryInfo
|
|
||||||
import scala.reflect.runtime.currentMirror
|
|
||||||
import scala.tools.reflect.ToolBox
|
|
||||||
import play.twirl.compiler.TwirlCompiler
|
|
||||||
import scala.io.Codec
|
|
||||||
|
|
||||||
// TODO This is a sample implementation for Scala based plug-ins.
|
|
||||||
class ScalaPlugin(val id: String, val version: String,
|
|
||||||
val author: String, val url: String, val description: String) extends Plugin {
|
|
||||||
|
|
||||||
private val repositoryMenuList = ListBuffer[RepositoryMenu]()
|
|
||||||
private val globalMenuList = ListBuffer[GlobalMenu]()
|
|
||||||
private val repositoryActionList = ListBuffer[RepositoryAction]()
|
|
||||||
private val globalActionList = ListBuffer[Action]()
|
|
||||||
private val javaScriptList = ListBuffer[JavaScript]()
|
|
||||||
|
|
||||||
def repositoryMenus : List[RepositoryMenu] = repositoryMenuList.toList
|
|
||||||
def globalMenus : List[GlobalMenu] = globalMenuList.toList
|
|
||||||
def repositoryActions : List[RepositoryAction] = repositoryActionList.toList
|
|
||||||
def globalActions : List[Action] = globalActionList.toList
|
|
||||||
def javaScripts : List[JavaScript] = javaScriptList.toList
|
|
||||||
|
|
||||||
def addRepositoryMenu(label: String, name: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
|
|
||||||
repositoryMenuList += RepositoryMenu(label, name, url, icon, condition)
|
|
||||||
}
|
|
||||||
|
|
||||||
def addGlobalMenu(label: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
|
|
||||||
globalMenuList += GlobalMenu(label, url, icon, condition)
|
|
||||||
}
|
|
||||||
|
|
||||||
def addGlobalAction(method: String, path: String, security: Security = All())(function: (HttpServletRequest, HttpServletResponse, Context) => Any): Unit = {
|
|
||||||
globalActionList += Action(method, path, security, function)
|
|
||||||
}
|
|
||||||
|
|
||||||
def addRepositoryAction(method: String, path: String, security: Security = All())(function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any): Unit = {
|
|
||||||
repositoryActionList += RepositoryAction(method, path, security, function)
|
|
||||||
}
|
|
||||||
|
|
||||||
def addJavaScript(filter: String => Boolean, script: String): Unit = {
|
|
||||||
javaScriptList += JavaScript(filter, script)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
object ScalaPlugin {
|
|
||||||
|
|
||||||
def define(id: String, version: String, author: String, url: String, description: String)
|
|
||||||
= new ScalaPlugin(id, version, author, url, description)
|
|
||||||
|
|
||||||
def eval(source: String): Any = {
|
|
||||||
val toolbox = currentMirror.mkToolBox()
|
|
||||||
val tree = toolbox.parse(source)
|
|
||||||
toolbox.eval(tree)
|
|
||||||
}
|
|
||||||
|
|
||||||
def compileTemplate(packageName: String, name: String, source: String): String = {
|
|
||||||
val result = TwirlCompiler.parseAndGenerateCodeNewParser(
|
|
||||||
Array(packageName, name),
|
|
||||||
source.getBytes("UTF-8"),
|
|
||||||
Codec(scala.util.Properties.sourceEncoding),
|
|
||||||
"",
|
|
||||||
"play.twirl.api.HtmlFormat.Appendable",
|
|
||||||
"play.twirl.api.HtmlFormat",
|
|
||||||
"",
|
|
||||||
false)
|
|
||||||
|
|
||||||
result.replaceFirst("package .*", "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package plugin
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines enum case classes to specify permission for actions which is provided by plugin.
|
|
||||||
*/
|
|
||||||
object Security {
|
|
||||||
|
|
||||||
sealed trait Security
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All users and guests
|
|
||||||
*/
|
|
||||||
case class All() extends Security
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Only signed-in users
|
|
||||||
*/
|
|
||||||
case class Login() extends Security
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Only repository owner and collaborators
|
|
||||||
*/
|
|
||||||
case class Member() extends Security
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Only repository owner and managers of group repository
|
|
||||||
*/
|
|
||||||
case class Owner() extends Security
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Only administrators
|
|
||||||
*/
|
|
||||||
case class Admin() extends Security
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import java.sql.PreparedStatement
|
|
||||||
import play.twirl.api.Html
|
|
||||||
import util.ControlUtil._
|
|
||||||
import scala.collection.mutable.ListBuffer
|
|
||||||
|
|
||||||
package object plugin {
|
|
||||||
|
|
||||||
case class Redirect(path: String)
|
|
||||||
case class Fragment(html: Html)
|
|
||||||
case class RawData(contentType: String, content: Array[Byte])
|
|
||||||
|
|
||||||
object db {
|
|
||||||
// TODO labelled place holder support
|
|
||||||
def select(sql: String, params: Any*): Seq[Map[String, String]] = {
|
|
||||||
defining(PluginConnectionHolder.threadLocal.get){ conn =>
|
|
||||||
using(conn.prepareStatement(sql)){ stmt =>
|
|
||||||
setParams(stmt, params: _*)
|
|
||||||
using(stmt.executeQuery()){ rs =>
|
|
||||||
val list = new ListBuffer[Map[String, String]]()
|
|
||||||
while(rs.next){
|
|
||||||
defining(rs.getMetaData){ meta =>
|
|
||||||
val map = Range(1, meta.getColumnCount + 1).map { i =>
|
|
||||||
val name = meta.getColumnName(i)
|
|
||||||
(name, rs.getString(name))
|
|
||||||
}.toMap
|
|
||||||
list += map
|
|
||||||
}
|
|
||||||
}
|
|
||||||
list
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO labelled place holder support
|
|
||||||
def update(sql: String, params: Any*): Int = {
|
|
||||||
defining(PluginConnectionHolder.threadLocal.get){ conn =>
|
|
||||||
using(conn.prepareStatement(sql)){ stmt =>
|
|
||||||
setParams(stmt, params: _*)
|
|
||||||
stmt.executeUpdate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def setParams(stmt: PreparedStatement, params: Any*): Unit = {
|
|
||||||
params.zipWithIndex.foreach { case (p, i) =>
|
|
||||||
p match {
|
|
||||||
case x: String => stmt.setString(i + 1, x)
|
|
||||||
case x: Int => stmt.setInt(i + 1, x)
|
|
||||||
case x: Boolean => stmt.setBoolean(i + 1, x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -95,6 +95,15 @@ trait ActivityService {
|
|||||||
Some(cut(comment, 200)),
|
Some(cut(comment, 200)),
|
||||||
currentDate)
|
currentDate)
|
||||||
|
|
||||||
|
def recordCommentCommitActivity(userName: String, repositoryName: String, activityUserName: String, commitId: String, comment: String)
|
||||||
|
(implicit s: Session): Unit =
|
||||||
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
|
"comment_commit",
|
||||||
|
s"[user:${activityUserName}] commented on commit [commit:${userName}/${repositoryName}@${commitId}]",
|
||||||
|
Some(cut(comment, 200)),
|
||||||
|
currentDate
|
||||||
|
)
|
||||||
|
|
||||||
def recordCreateWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String)
|
def recordCreateWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String)
|
||||||
(implicit s: Session): Unit =
|
(implicit s: Session): Unit =
|
||||||
Activities insert Activity(userName, repositoryName, activityUserName,
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
@@ -151,10 +160,10 @@ trait ActivityService {
|
|||||||
None,
|
None,
|
||||||
currentDate)
|
currentDate)
|
||||||
|
|
||||||
def recordForkActivity(userName: String, repositoryName: String, activityUserName: String)(implicit s: Session): Unit =
|
def recordForkActivity(userName: String, repositoryName: String, activityUserName: String, forkedUserName: String)(implicit s: Session): Unit =
|
||||||
Activities insert Activity(userName, repositoryName, activityUserName,
|
Activities insert Activity(userName, repositoryName, activityUserName,
|
||||||
"fork",
|
"fork",
|
||||||
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${activityUserName}/${repositoryName}]",
|
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${forkedUserName}/${repositoryName}]",
|
||||||
None,
|
None,
|
||||||
currentDate)
|
currentDate)
|
||||||
|
|
||||||
|
|||||||
52
src/main/scala/service/CommitsService.scala
Normal file
52
src/main/scala/service/CommitsService.scala
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import scala.slick.jdbc.{StaticQuery => Q}
|
||||||
|
import Q.interpolation
|
||||||
|
|
||||||
|
import model.Profile._
|
||||||
|
import profile.simple._
|
||||||
|
import model.CommitComment
|
||||||
|
import util.Implicits._
|
||||||
|
import util.StringUtil._
|
||||||
|
|
||||||
|
|
||||||
|
trait CommitsService {
|
||||||
|
|
||||||
|
def getCommitComments(owner: String, repository: String, commitId: String, pullRequest: Boolean)(implicit s: Session) =
|
||||||
|
CommitComments filter {
|
||||||
|
t => t.byCommit(owner, repository, commitId) && (t.pullRequest === pullRequest || pullRequest)
|
||||||
|
} list
|
||||||
|
|
||||||
|
def getCommitComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
|
||||||
|
if (commentId forall (_.isDigit))
|
||||||
|
CommitComments filter { t =>
|
||||||
|
t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository)
|
||||||
|
} firstOption
|
||||||
|
else
|
||||||
|
None
|
||||||
|
|
||||||
|
def createCommitComment(owner: String, repository: String, commitId: String, loginUser: String,
|
||||||
|
content: String, fileName: Option[String], oldLine: Option[Int], newLine: Option[Int], pullRequest: Boolean)(implicit s: Session): Int =
|
||||||
|
CommitComments.autoInc insert CommitComment(
|
||||||
|
userName = owner,
|
||||||
|
repositoryName = repository,
|
||||||
|
commitId = commitId,
|
||||||
|
commentedUserName = loginUser,
|
||||||
|
content = content,
|
||||||
|
fileName = fileName,
|
||||||
|
oldLine = oldLine,
|
||||||
|
newLine = newLine,
|
||||||
|
registeredDate = currentDate,
|
||||||
|
updatedDate = currentDate,
|
||||||
|
pullRequest = pullRequest)
|
||||||
|
|
||||||
|
def updateCommitComment(commentId: Int, content: String)(implicit s: Session) =
|
||||||
|
CommitComments
|
||||||
|
.filter (_.byPrimaryKey(commentId))
|
||||||
|
.map { t =>
|
||||||
|
t.content -> t.updatedDate
|
||||||
|
}.update (content, currentDate)
|
||||||
|
|
||||||
|
def deleteCommitComment(commentId: Int)(implicit s: Session) =
|
||||||
|
CommitComments filter (_.byPrimaryKey(commentId)) delete
|
||||||
|
}
|
||||||
@@ -47,9 +47,9 @@ trait IssuesService {
|
|||||||
* @param repos Tuple of the repository owner and the repository name
|
* @param repos Tuple of the repository owner and the repository name
|
||||||
* @return the count of the search result
|
* @return the count of the search result
|
||||||
*/
|
*/
|
||||||
def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
|
def countIssue(condition: IssueSearchCondition, onlyPullRequest: Boolean,
|
||||||
repos: (String, String)*)(implicit s: Session): Int =
|
repos: (String, String)*)(implicit s: Session): Int =
|
||||||
Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first
|
Query(searchIssueQuery(repos, condition, onlyPullRequest).length).first
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the Map which contains issue count for each labels.
|
* Returns the Map which contains issue count for each labels.
|
||||||
@@ -62,7 +62,7 @@ trait IssuesService {
|
|||||||
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
|
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
|
||||||
filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = {
|
filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = {
|
||||||
|
|
||||||
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false)
|
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), false)
|
||||||
.innerJoin(IssueLabels).on { (t1, t2) =>
|
.innerJoin(IssueLabels).on { (t1, t2) =>
|
||||||
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
||||||
}
|
}
|
||||||
@@ -78,46 +78,21 @@ trait IssuesService {
|
|||||||
.toMap
|
.toMap
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Returns list which contains issue count for each repository.
|
|
||||||
// * If the issue does not exist, its repository is not included in the result.
|
|
||||||
// *
|
|
||||||
// * @param condition the search condition
|
|
||||||
// * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
|
|
||||||
// * @param repos Tuple of the repository owner and the repository name
|
|
||||||
// * @return list which contains issue count for each repository
|
|
||||||
// */
|
|
||||||
// def countIssueGroupByRepository(
|
|
||||||
// condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
|
|
||||||
// repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = {
|
|
||||||
// searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest)
|
|
||||||
// .groupBy { t =>
|
|
||||||
// t.userName -> t.repositoryName
|
|
||||||
// }
|
|
||||||
// .map { case (repo, t) =>
|
|
||||||
// (repo._1, repo._2, t.length)
|
|
||||||
// }
|
|
||||||
// .sortBy(_._3 desc)
|
|
||||||
// .list
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the search result against issues.
|
* Returns the search result against issues.
|
||||||
*
|
*
|
||||||
* @param condition the search condition
|
* @param condition the search condition
|
||||||
* @param filterUser the filter user name (key is "all", "assigned", "created_by", "not_created_by" or "mentioned", value is the user name)
|
|
||||||
* @param pullRequest if true then returns only pull requests, false then returns only issues.
|
* @param pullRequest if true then returns only pull requests, false then returns only issues.
|
||||||
* @param offset the offset for pagination
|
* @param offset the offset for pagination
|
||||||
* @param limit the limit for pagination
|
* @param limit the limit for pagination
|
||||||
* @param repos Tuple of the repository owner and the repository name
|
* @param repos Tuple of the repository owner and the repository name
|
||||||
* @return the search result (list of tuples which contain issue, labels and comment count)
|
* @return the search result (list of tuples which contain issue, labels and comment count)
|
||||||
*/
|
*/
|
||||||
def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], pullRequest: Boolean,
|
def searchIssue(condition: IssueSearchCondition, pullRequest: Boolean, offset: Int, limit: Int, repos: (String, String)*)
|
||||||
offset: Int, limit: Int, repos: (String, String)*)
|
|
||||||
(implicit s: Session): List[IssueInfo] = {
|
(implicit s: Session): List[IssueInfo] = {
|
||||||
|
|
||||||
// get issues and comment count and labels
|
// get issues and comment count and labels
|
||||||
searchIssueQuery(repos, condition, filterUser, pullRequest)
|
searchIssueQuery(repos, condition, pullRequest)
|
||||||
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
|
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
|
||||||
.sortBy { case (t1, t2) =>
|
.sortBy { case (t1, t2) =>
|
||||||
(condition.sort match {
|
(condition.sort match {
|
||||||
@@ -158,20 +133,14 @@ trait IssuesService {
|
|||||||
/**
|
/**
|
||||||
* Assembles query for conditional issue searching.
|
* Assembles query for conditional issue searching.
|
||||||
*/
|
*/
|
||||||
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition,
|
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, pullRequest: Boolean)(implicit s: Session) =
|
||||||
filterUser: Map[String, String], pullRequest: Boolean)(implicit s: Session) =
|
|
||||||
Issues filter { t1 =>
|
Issues filter { t1 =>
|
||||||
condition.repo
|
repos
|
||||||
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
|
|
||||||
.getOrElse (repos)
|
|
||||||
.map { case (owner, repository) => t1.byRepository(owner, repository) }
|
.map { case (owner, repository) => t1.byRepository(owner, repository) }
|
||||||
.foldLeft[Column[Boolean]](false) ( _ || _ ) &&
|
.foldLeft[Column[Boolean]](false) ( _ || _ ) &&
|
||||||
(t1.closed === (condition.state == "closed").bind) &&
|
(t1.closed === (condition.state == "closed").bind) &&
|
||||||
(t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
|
(t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
|
||||||
(t1.milestoneId.? isEmpty, condition.milestoneId == Some(None)) &&
|
(t1.milestoneId.? isEmpty, condition.milestoneId == Some(None)) &&
|
||||||
(t1.assignedUserName === filterUser("assigned").bind, filterUser.get("assigned").isDefined) &&
|
|
||||||
(t1.openedUserName === filterUser("created_by").bind, filterUser.get("created_by").isDefined) &&
|
|
||||||
(t1.openedUserName =!= filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) &&
|
|
||||||
(t1.assignedUserName === condition.assigned.get.bind, condition.assigned.isDefined) &&
|
(t1.assignedUserName === condition.assigned.get.bind, condition.assigned.isDefined) &&
|
||||||
(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
|
(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
|
||||||
(t1.pullRequest === pullRequest.bind) &&
|
(t1.pullRequest === pullRequest.bind) &&
|
||||||
@@ -192,10 +161,10 @@ trait IssuesService {
|
|||||||
// Organization (group) filter
|
// Organization (group) filter
|
||||||
(t1.userName inSetBind condition.groups, condition.groups.nonEmpty) &&
|
(t1.userName inSetBind condition.groups, condition.groups.nonEmpty) &&
|
||||||
// Mentioned filter
|
// Mentioned filter
|
||||||
((t1.openedUserName === filterUser("mentioned").bind) || t1.assignedUserName === filterUser("mentioned").bind ||
|
((t1.openedUserName === condition.mentioned.get.bind) || t1.assignedUserName === condition.mentioned.get.bind ||
|
||||||
(IssueComments filter { t2 =>
|
(IssueComments filter { t2 =>
|
||||||
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === filterUser("mentioned").bind)
|
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === condition.mentioned.get.bind)
|
||||||
} exists), filterUser.get("mentioned").isDefined)
|
} exists), condition.mentioned.isDefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
|
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
|
||||||
@@ -294,6 +263,7 @@ trait IssuesService {
|
|||||||
|
|
||||||
// Search Issue
|
// Search Issue
|
||||||
val issues = Issues
|
val issues = Issues
|
||||||
|
.filter(_.byRepository(owner, repository))
|
||||||
.innerJoin(IssueOutline).on { case (t1, t2) =>
|
.innerJoin(IssueOutline).on { case (t1, t2) =>
|
||||||
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
||||||
}
|
}
|
||||||
@@ -309,6 +279,7 @@ trait IssuesService {
|
|||||||
|
|
||||||
// Search IssueComment
|
// Search IssueComment
|
||||||
val comments = IssueComments
|
val comments = IssueComments
|
||||||
|
.filter(_.byRepository(owner, repository))
|
||||||
.innerJoin(Issues).on { case (t1, t2) =>
|
.innerJoin(Issues).on { case (t1, t2) =>
|
||||||
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
||||||
}
|
}
|
||||||
@@ -354,7 +325,7 @@ object IssuesService {
|
|||||||
milestoneId: Option[Option[Int]] = None,
|
milestoneId: Option[Option[Int]] = None,
|
||||||
author: Option[String] = None,
|
author: Option[String] = None,
|
||||||
assigned: Option[String] = None,
|
assigned: Option[String] = None,
|
||||||
repo: Option[String] = None,
|
mentioned: Option[String] = None,
|
||||||
state: String = "open",
|
state: String = "open",
|
||||||
sort: String = "created",
|
sort: String = "created",
|
||||||
direction: String = "desc",
|
direction: String = "desc",
|
||||||
@@ -368,16 +339,42 @@ object IssuesService {
|
|||||||
|
|
||||||
def nonEmpty: Boolean = !isEmpty
|
def nonEmpty: Boolean = !isEmpty
|
||||||
|
|
||||||
|
def toFilterString: String = (
|
||||||
|
List(
|
||||||
|
Some(s"is:${state}"),
|
||||||
|
author.map(author => s"author:${author}"),
|
||||||
|
assigned.map(assignee => s"assignee:${assignee}"),
|
||||||
|
mentioned.map(mentioned => s"mentions:${mentioned}")
|
||||||
|
).flatten ++
|
||||||
|
labels.map(label => s"label:${label}") ++
|
||||||
|
List(
|
||||||
|
milestoneId.map { _ match {
|
||||||
|
case Some(x) => s"milestone:${milestoneId}"
|
||||||
|
case None => "no:milestone"
|
||||||
|
}},
|
||||||
|
(sort, direction) match {
|
||||||
|
case ("created" , "desc") => None
|
||||||
|
case ("created" , "asc" ) => Some("sort:created-asc")
|
||||||
|
case ("comments", "desc") => Some("sort:comments-desc")
|
||||||
|
case ("comments", "asc" ) => Some("sort:comments-asc")
|
||||||
|
case ("updated" , "desc") => Some("sort:updated-desc")
|
||||||
|
case ("updated" , "asc" ) => Some("sort:updated-asc")
|
||||||
|
},
|
||||||
|
visibility.map(visibility => s"visibility:${visibility}")
|
||||||
|
).flatten ++
|
||||||
|
groups.map(group => s"group:${group}")
|
||||||
|
).mkString(" ")
|
||||||
|
|
||||||
def toURL: String =
|
def toURL: String =
|
||||||
"?" + List(
|
"?" + List(
|
||||||
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
|
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
|
||||||
milestoneId.map { id => "milestone=" + (id match {
|
milestoneId.map { _ match {
|
||||||
case Some(x) => x.toString
|
case Some(x) => "milestone=" + x
|
||||||
case None => "none"
|
case None => "milestone=none"
|
||||||
})},
|
}},
|
||||||
author .map(x => "author=" + urlEncode(x)),
|
author .map(x => "author=" + urlEncode(x)),
|
||||||
assigned .map(x => "assigned=" + urlEncode(x)),
|
assigned .map(x => "assigned=" + urlEncode(x)),
|
||||||
repo.map("for=" + urlEncode(_)),
|
mentioned.map(x => "mentioned=" + urlEncode(x)),
|
||||||
Some("state=" + urlEncode(state)),
|
Some("state=" + urlEncode(state)),
|
||||||
Some("sort=" + urlEncode(sort)),
|
Some("sort=" + urlEncode(sort)),
|
||||||
Some("direction=" + urlEncode(direction)),
|
Some("direction=" + urlEncode(direction)),
|
||||||
@@ -394,6 +391,47 @@ object IssuesService {
|
|||||||
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
|
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores IssueSearchCondition instance from filter query.
|
||||||
|
*/
|
||||||
|
def apply(filter: String, milestones: Map[String, Int]): IssueSearchCondition = {
|
||||||
|
val conditions = filter.split("[ \t]+").map { x =>
|
||||||
|
val dim = x.split(":")
|
||||||
|
dim(0) -> dim(1)
|
||||||
|
}.groupBy(_._1).map { case (key, values) =>
|
||||||
|
key -> values.map(_._2).toSeq
|
||||||
|
}
|
||||||
|
|
||||||
|
val (sort, direction) = conditions.get("sort").flatMap(_.headOption).getOrElse("created-desc") match {
|
||||||
|
case "created-asc" => ("created" , "asc" )
|
||||||
|
case "comments-desc" => ("comments", "desc")
|
||||||
|
case "comments-asc" => ("comments", "asc" )
|
||||||
|
case "updated-desc" => ("comments", "desc")
|
||||||
|
case "updated-asc" => ("comments", "asc" )
|
||||||
|
case _ => ("created" , "desc")
|
||||||
|
}
|
||||||
|
|
||||||
|
IssueSearchCondition(
|
||||||
|
conditions.get("label").map(_.toSet).getOrElse(Set.empty),
|
||||||
|
conditions.get("milestone").flatMap(_.headOption) match {
|
||||||
|
case None => None
|
||||||
|
case Some("none") => Some(None)
|
||||||
|
case Some(x) => milestones.get(x).map(x => Some(x))
|
||||||
|
},
|
||||||
|
conditions.get("author").flatMap(_.headOption),
|
||||||
|
conditions.get("assignee").flatMap(_.headOption),
|
||||||
|
conditions.get("mentions").flatMap(_.headOption),
|
||||||
|
conditions.get("is").getOrElse(Seq.empty).find(x => x == "open" || x == "closed").getOrElse("open"),
|
||||||
|
sort,
|
||||||
|
direction,
|
||||||
|
conditions.get("visibility").flatMap(_.headOption),
|
||||||
|
conditions.get("group").map(_.toSet).getOrElse(Set.empty)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores IssueSearchCondition instance from request parameters.
|
||||||
|
*/
|
||||||
def apply(request: HttpServletRequest): IssueSearchCondition =
|
def apply(request: HttpServletRequest): IssueSearchCondition =
|
||||||
IssueSearchCondition(
|
IssueSearchCondition(
|
||||||
param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
|
param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
|
||||||
@@ -403,7 +441,7 @@ object IssuesService {
|
|||||||
},
|
},
|
||||||
param(request, "author"),
|
param(request, "author"),
|
||||||
param(request, "assigned"),
|
param(request, "assigned"),
|
||||||
param(request, "for"),
|
param(request, "mentioned"),
|
||||||
param(request, "state", Seq("open", "closed")).getOrElse("open"),
|
param(request, "state", Seq("open", "closed")).getOrElse("open"),
|
||||||
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
|
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
|
||||||
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"),
|
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package service
|
|||||||
import model.Profile._
|
import model.Profile._
|
||||||
import profile.simple._
|
import profile.simple._
|
||||||
import model.{PullRequest, Issue}
|
import model.{PullRequest, Issue}
|
||||||
|
import util.JGitUtil
|
||||||
|
|
||||||
trait PullRequestService { self: IssuesService =>
|
trait PullRequestService { self: IssuesService =>
|
||||||
import PullRequestService._
|
import PullRequestService._
|
||||||
@@ -81,6 +82,18 @@ trait PullRequestService { self: IssuesService =>
|
|||||||
.map { case (t1, t2) => t1 }
|
.map { case (t1, t2) => t1 }
|
||||||
.list
|
.list
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch pull request contents into refs/pull/${issueId}/head and update pull request table.
|
||||||
|
*/
|
||||||
|
def updatePullRequests(owner: String, repository: String, branch: String)(implicit s: Session): Unit =
|
||||||
|
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
|
||||||
|
if(Repositories.filter(_.byRepository(pullreq.userName, pullreq.repositoryName)).exists.run){
|
||||||
|
val (commitIdTo, commitIdFrom) = JGitUtil.updatePullRequest(
|
||||||
|
pullreq.userName, pullreq.repositoryName, pullreq.branch, pullreq.issueId,
|
||||||
|
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch)
|
||||||
|
updateCommitId(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo, commitIdFrom)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object PullRequestService {
|
object PullRequestService {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ trait RepositorySearchService { self: IssuesService =>
|
|||||||
searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
|
searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) =>
|
||||||
IssueSearchResult(
|
IssueSearchResult(
|
||||||
issue.issueId,
|
issue.issueId,
|
||||||
|
issue.isPullRequest,
|
||||||
issue.title,
|
issue.title,
|
||||||
issue.openedUserName,
|
issue.openedUserName,
|
||||||
issue.registeredDate,
|
issue.registeredDate,
|
||||||
@@ -111,6 +112,7 @@ object RepositorySearchService {
|
|||||||
|
|
||||||
case class IssueSearchResult(
|
case class IssueSearchResult(
|
||||||
issueId: Int,
|
issueId: Int,
|
||||||
|
isPullRequest: Boolean,
|
||||||
title: String,
|
title: String,
|
||||||
openedUserName: String,
|
openedUserName: String,
|
||||||
registeredDate: java.util.Date,
|
registeredDate: java.util.Date,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package service
|
|||||||
|
|
||||||
import model.Profile._
|
import model.Profile._
|
||||||
import profile.simple._
|
import profile.simple._
|
||||||
import model.{Repository, Account, Collaborator}
|
import model.{Repository, Account, Collaborator, Label}
|
||||||
import util.JGitUtil
|
import util.JGitUtil
|
||||||
|
|
||||||
trait RepositoryService { self: AccountService =>
|
trait RepositoryService { self: AccountService =>
|
||||||
@@ -54,7 +54,7 @@ trait RepositoryService { self: AccountService =>
|
|||||||
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val activities = Activities .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val commitComments = CommitComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val collaborators = Collaborators .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val collaborators = Collaborators .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
|
|
||||||
Repositories.filter { t =>
|
Repositories.filter { t =>
|
||||||
@@ -69,6 +69,13 @@ trait RepositoryService { self: AccountService =>
|
|||||||
t.requestRepositoryName === oldRepositoryName.bind
|
t.requestRepositoryName === oldRepositoryName.bind
|
||||||
}.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName)
|
}.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName)
|
||||||
|
|
||||||
|
// Updates activity fk before deleting repository because activity is sorted by activityId
|
||||||
|
// and it can't be changed by deleting-and-inserting record.
|
||||||
|
Activities.filter(_.byRepository(oldUserName, oldRepositoryName)).list.foreach { activity =>
|
||||||
|
Activities.filter(_.activityId === activity.activityId.bind)
|
||||||
|
.map(x => (x.userName, x.repositoryName)).update(newUserName, newRepositoryName)
|
||||||
|
}
|
||||||
|
|
||||||
deleteRepository(oldUserName, oldRepositoryName)
|
deleteRepository(oldUserName, oldRepositoryName)
|
||||||
|
|
||||||
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
@@ -87,8 +94,17 @@ trait RepositoryService { self: AccountService =>
|
|||||||
PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
Activities .insertAll(activities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
|
||||||
|
// Convert labelId
|
||||||
|
val oldLabelMap = labels.map(x => (x.labelId, x.labelName)).toMap
|
||||||
|
val newLabelMap = Labels.filter(_.byRepository(newUserName, newRepositoryName)).map(x => (x.labelName, x.labelId)).list.toMap
|
||||||
|
IssueLabels.insertAll(issueLabels.map(x => x.copy(
|
||||||
|
labelId = newLabelMap(oldLabelMap(x.labelId)),
|
||||||
|
userName = newUserName,
|
||||||
|
repositoryName = newRepositoryName
|
||||||
|
)) :_*)
|
||||||
|
|
||||||
if(account.isGroupAccount){
|
if(account.isGroupAccount){
|
||||||
Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
|
Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
|
||||||
} else {
|
} else {
|
||||||
@@ -96,12 +112,11 @@ trait RepositoryService { self: AccountService =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update activity messages
|
// Update activity messages
|
||||||
val updateActivities = Activities.filter { t =>
|
Activities.filter { t =>
|
||||||
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
|
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
|
||||||
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%")
|
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%") ||
|
||||||
}.map { t => t.activityId -> t.message }.list
|
(t.message like s"%:${oldUserName}/${oldRepositoryName}@%")
|
||||||
|
}.map { t => t.activityId -> t.message }.list.foreach { case (activityId, message) =>
|
||||||
updateActivities.foreach { case (activityId, message) =>
|
|
||||||
Activities.filter(_.activityId === activityId.bind).map(_.message).update(
|
Activities.filter(_.activityId === activityId.bind).map(_.message).update(
|
||||||
message
|
message
|
||||||
.replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]")
|
.replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]")
|
||||||
@@ -109,6 +124,7 @@ trait RepositoryService { self: AccountService =>
|
|||||||
.replace(s"[tag:${oldUserName}/${oldRepositoryName}#" ,s"[tag:${newUserName}/${newRepositoryName}#")
|
.replace(s"[tag:${oldUserName}/${oldRepositoryName}#" ,s"[tag:${newUserName}/${newRepositoryName}#")
|
||||||
.replace(s"[pullreq:${oldUserName}/${oldRepositoryName}#",s"[pullreq:${newUserName}/${newRepositoryName}#")
|
.replace(s"[pullreq:${oldUserName}/${oldRepositoryName}#",s"[pullreq:${newUserName}/${newRepositoryName}#")
|
||||||
.replace(s"[issue:${oldUserName}/${oldRepositoryName}#" ,s"[issue:${newUserName}/${newRepositoryName}#")
|
.replace(s"[issue:${oldUserName}/${oldRepositoryName}#" ,s"[issue:${newUserName}/${newRepositoryName}#")
|
||||||
|
.replace(s"[commit:${oldUserName}/${oldRepositoryName}@" ,s"[commit:${newUserName}/${newRepositoryName}@")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,6 +134,7 @@ trait RepositoryService { self: AccountService =>
|
|||||||
def deleteRepository(userName: String, repositoryName: String)(implicit s: Session): Unit = {
|
def deleteRepository(userName: String, repositoryName: String)(implicit s: Session): Unit = {
|
||||||
Activities .filter(_.byRepository(userName, repositoryName)).delete
|
Activities .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
Collaborators .filter(_.byRepository(userName, repositoryName)).delete
|
Collaborators .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
|
CommitComments.filter(_.byRepository(userName, repositoryName)).delete
|
||||||
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
|
IssueLabels .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
Labels .filter(_.byRepository(userName, repositoryName)).delete
|
Labels .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
IssueComments .filter(_.byRepository(userName, repositoryName)).delete
|
IssueComments .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
@@ -127,6 +144,30 @@ trait RepositoryService { self: AccountService =>
|
|||||||
Milestones .filter(_.byRepository(userName, repositoryName)).delete
|
Milestones .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
WebHooks .filter(_.byRepository(userName, repositoryName)).delete
|
WebHooks .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
Repositories .filter(_.byRepository(userName, repositoryName)).delete
|
Repositories .filter(_.byRepository(userName, repositoryName)).delete
|
||||||
|
|
||||||
|
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME
|
||||||
|
Repositories
|
||||||
|
.filter { x => (x.originUserName === userName.bind) && (x.originRepositoryName === repositoryName.bind) }
|
||||||
|
.map { x => (x.userName, x.repositoryName) }
|
||||||
|
.list
|
||||||
|
.foreach { case (userName, repositoryName) =>
|
||||||
|
Repositories
|
||||||
|
.filter(_.byRepository(userName, repositoryName))
|
||||||
|
.map(x => (x.originUserName?, x.originRepositoryName?))
|
||||||
|
.update(None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME
|
||||||
|
Repositories
|
||||||
|
.filter { x => (x.parentUserName === userName.bind) && (x.parentRepositoryName === repositoryName.bind) }
|
||||||
|
.map { x => (x.userName, x.repositoryName) }
|
||||||
|
.list
|
||||||
|
.foreach { case (userName, repositoryName) =>
|
||||||
|
Repositories
|
||||||
|
.filter(_.byRepository(userName, repositoryName))
|
||||||
|
.map(x => (x.parentUserName?, x.parentRepositoryName?))
|
||||||
|
.update(None, None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -156,8 +197,8 @@ trait RepositoryService { self: AccountService =>
|
|||||||
new RepositoryInfo(
|
new RepositoryInfo(
|
||||||
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
|
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
|
||||||
repository,
|
repository,
|
||||||
issues.size,
|
issues.count(_ == false),
|
||||||
issues.filter(_ == true).size,
|
issues.count(_ == true),
|
||||||
getForkedCount(
|
getForkedCount(
|
||||||
repository.originUserName.getOrElse(repository.userName),
|
repository.originUserName.getOrElse(repository.userName),
|
||||||
repository.originRepositoryName.getOrElse(repository.repositoryName)
|
repository.originRepositoryName.getOrElse(repository.repositoryName)
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ trait SystemSettingsService {
|
|||||||
def saveSystemSettings(settings: SystemSettings): Unit = {
|
def saveSystemSettings(settings: SystemSettings): Unit = {
|
||||||
defining(new java.util.Properties()){ props =>
|
defining(new java.util.Properties()){ props =>
|
||||||
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
|
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
|
||||||
|
settings.information.foreach(x => props.setProperty(Information, x))
|
||||||
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
|
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
|
||||||
|
props.setProperty(AllowAnonymousAccess, settings.allowAnonymousAccess.toString)
|
||||||
|
props.setProperty(IsCreateRepoOptionPublic, settings.isCreateRepoOptionPublic.toString)
|
||||||
props.setProperty(Gravatar, settings.gravatar.toString)
|
props.setProperty(Gravatar, settings.gravatar.toString)
|
||||||
props.setProperty(Notification, settings.notification.toString)
|
props.setProperty(Notification, settings.notification.toString)
|
||||||
props.setProperty(Ssh, settings.ssh.toString)
|
props.setProperty(Ssh, settings.ssh.toString)
|
||||||
@@ -39,8 +42,9 @@ trait SystemSettingsService {
|
|||||||
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
|
props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
|
||||||
ldap.additionalFilterCondition.foreach(x => props.setProperty(LdapAdditionalFilterCondition, x))
|
ldap.additionalFilterCondition.foreach(x => props.setProperty(LdapAdditionalFilterCondition, x))
|
||||||
ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
|
ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
|
||||||
ldap.mailAttribute.foreach(x => props.setProperty(LdapMailAddressAttribute, x.toString))
|
ldap.mailAttribute.foreach(x => props.setProperty(LdapMailAddressAttribute, x))
|
||||||
ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
|
ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
|
||||||
|
ldap.ssl.foreach(x => props.setProperty(LdapSsl, x.toString))
|
||||||
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
|
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,7 +64,10 @@ trait SystemSettingsService {
|
|||||||
}
|
}
|
||||||
SystemSettings(
|
SystemSettings(
|
||||||
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
|
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
|
||||||
|
getOptionValue[String](props, Information, None),
|
||||||
getValue(props, AllowAccountRegistration, false),
|
getValue(props, AllowAccountRegistration, false),
|
||||||
|
getValue(props, AllowAnonymousAccess, true),
|
||||||
|
getValue(props, IsCreateRepoOptionPublic, true),
|
||||||
getValue(props, Gravatar, true),
|
getValue(props, Gravatar, true),
|
||||||
getValue(props, Notification, false),
|
getValue(props, Notification, false),
|
||||||
getValue(props, Ssh, false),
|
getValue(props, Ssh, false),
|
||||||
@@ -90,6 +97,7 @@ trait SystemSettingsService {
|
|||||||
getOptionValue(props, LdapFullNameAttribute, None),
|
getOptionValue(props, LdapFullNameAttribute, None),
|
||||||
getOptionValue(props, LdapMailAddressAttribute, None),
|
getOptionValue(props, LdapMailAddressAttribute, None),
|
||||||
getOptionValue[Boolean](props, LdapTls, None),
|
getOptionValue[Boolean](props, LdapTls, None),
|
||||||
|
getOptionValue[Boolean](props, LdapSsl, None),
|
||||||
getOptionValue(props, LdapKeystore, None)))
|
getOptionValue(props, LdapKeystore, None)))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -105,7 +113,10 @@ object SystemSettingsService {
|
|||||||
|
|
||||||
case class SystemSettings(
|
case class SystemSettings(
|
||||||
baseUrl: Option[String],
|
baseUrl: Option[String],
|
||||||
|
information: Option[String],
|
||||||
allowAccountRegistration: Boolean,
|
allowAccountRegistration: Boolean,
|
||||||
|
allowAnonymousAccess: Boolean,
|
||||||
|
isCreateRepoOptionPublic: Boolean,
|
||||||
gravatar: Boolean,
|
gravatar: Boolean,
|
||||||
notification: Boolean,
|
notification: Boolean,
|
||||||
ssh: Boolean,
|
ssh: Boolean,
|
||||||
@@ -131,6 +142,7 @@ object SystemSettingsService {
|
|||||||
fullNameAttribute: Option[String],
|
fullNameAttribute: Option[String],
|
||||||
mailAttribute: Option[String],
|
mailAttribute: Option[String],
|
||||||
tls: Option[Boolean],
|
tls: Option[Boolean],
|
||||||
|
ssl: Option[Boolean],
|
||||||
keystore: Option[String])
|
keystore: Option[String])
|
||||||
|
|
||||||
case class Smtp(
|
case class Smtp(
|
||||||
@@ -147,7 +159,10 @@ object SystemSettingsService {
|
|||||||
val DefaultLdapPort = 389
|
val DefaultLdapPort = 389
|
||||||
|
|
||||||
private val BaseURL = "base_url"
|
private val BaseURL = "base_url"
|
||||||
|
private val Information = "information"
|
||||||
private val AllowAccountRegistration = "allow_account_registration"
|
private val AllowAccountRegistration = "allow_account_registration"
|
||||||
|
private val AllowAnonymousAccess = "allow_anonymous_access"
|
||||||
|
private val IsCreateRepoOptionPublic = "is_create_repository_option_public"
|
||||||
private val Gravatar = "gravatar"
|
private val Gravatar = "gravatar"
|
||||||
private val Notification = "notification"
|
private val Notification = "notification"
|
||||||
private val Ssh = "ssh"
|
private val Ssh = "ssh"
|
||||||
@@ -170,6 +185,7 @@ object SystemSettingsService {
|
|||||||
private val LdapFullNameAttribute = "ldap.fullname_attribute"
|
private val LdapFullNameAttribute = "ldap.fullname_attribute"
|
||||||
private val LdapMailAddressAttribute = "ldap.mail_attribute"
|
private val LdapMailAddressAttribute = "ldap.mail_attribute"
|
||||||
private val LdapTls = "ldap.tls"
|
private val LdapTls = "ldap.tls"
|
||||||
|
private val LdapSsl = "ldap.ssl"
|
||||||
private val LdapKeystore = "ldap.keystore"
|
private val LdapKeystore = "ldap.keystore"
|
||||||
|
|
||||||
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
|
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A =
|
||||||
@@ -191,7 +207,7 @@ object SystemSettingsService {
|
|||||||
else value
|
else value
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO temporary flag
|
// // TODO temporary flag
|
||||||
val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
|
// val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,9 @@ import org.apache.commons.io.IOUtils
|
|||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import util.Directory._
|
import util.Directory._
|
||||||
import util.ControlUtil._
|
import util.ControlUtil._
|
||||||
|
import util.JDBCUtil._
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import util.Directory
|
import util.Directory
|
||||||
import plugin.PluginUpdateJob
|
|
||||||
import service.SystemSettingsService
|
|
||||||
|
|
||||||
object AutoUpdate {
|
object AutoUpdate {
|
||||||
|
|
||||||
@@ -53,23 +52,54 @@ object AutoUpdate {
|
|||||||
* The history of versions. A head of this sequence is the current BitBucket version.
|
* The history of versions. A head of this sequence is the current BitBucket version.
|
||||||
*/
|
*/
|
||||||
val versions = Seq(
|
val versions = Seq(
|
||||||
|
new Version(2, 8),
|
||||||
|
new Version(2, 7) {
|
||||||
|
override def update(conn: Connection): Unit = {
|
||||||
|
super.update(conn)
|
||||||
|
conn.select("SELECT * FROM REPOSITORY"){ rs =>
|
||||||
|
// Rename attached files directory from /issues to /comments
|
||||||
|
val userName = rs.getString("USER_NAME")
|
||||||
|
val repoName = rs.getString("REPOSITORY_NAME")
|
||||||
|
defining(Directory.getAttachedDir(userName, repoName)){ newDir =>
|
||||||
|
val oldDir = new File(newDir.getParentFile, "issues")
|
||||||
|
if(oldDir.exists && oldDir.isDirectory){
|
||||||
|
oldDir.renameTo(newDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME if it does not exist
|
||||||
|
val originalUserName = rs.getString("ORIGIN_USER_NAME")
|
||||||
|
val originalRepoName = rs.getString("ORIGIN_REPOSITORY_NAME")
|
||||||
|
if(originalUserName != null && originalRepoName != null){
|
||||||
|
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
|
||||||
|
originalUserName, originalRepoName) == 0){
|
||||||
|
conn.update("UPDATE REPOSITORY SET ORIGIN_USER_NAME = NULL, ORIGIN_REPOSITORY_NAME = NULL " +
|
||||||
|
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME if it does not exist
|
||||||
|
val parentUserName = rs.getString("PARENT_USER_NAME")
|
||||||
|
val parentRepoName = rs.getString("PARENT_REPOSITORY_NAME")
|
||||||
|
if(parentUserName != null && parentRepoName != null){
|
||||||
|
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
|
||||||
|
parentUserName, parentRepoName) == 0){
|
||||||
|
conn.update("UPDATE REPOSITORY SET PARENT_USER_NAME = NULL, PARENT_REPOSITORY_NAME = NULL " +
|
||||||
|
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Version(2, 6),
|
||||||
new Version(2, 5),
|
new Version(2, 5),
|
||||||
new Version(2, 4),
|
new Version(2, 4),
|
||||||
new Version(2, 3) {
|
new Version(2, 3) {
|
||||||
override def update(conn: Connection): Unit = {
|
override def update(conn: Connection): Unit = {
|
||||||
super.update(conn)
|
super.update(conn)
|
||||||
using(conn.createStatement.executeQuery("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'")){ rs =>
|
conn.select("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'"){ rs =>
|
||||||
while(rs.next) {
|
val curInfo = rs.getString("ADDITIONAL_INFO")
|
||||||
val info = rs.getString("ADDITIONAL_INFO")
|
val newInfo = curInfo.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n")
|
||||||
val newInfo = info.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n")
|
if (curInfo != newInfo) {
|
||||||
if (info != newInfo) {
|
conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID"))
|
||||||
val id = rs.getString("ACTIVITY_ID")
|
|
||||||
using(conn.prepareStatement("UPDATE ACTIVITY SET ADDITIONAL_INFO=? WHERE ACTIVITY_ID=?")) { sql =>
|
|
||||||
sql.setString(1, newInfo)
|
|
||||||
sql.setLong(2, id.toLong)
|
|
||||||
sql.executeUpdate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FileUtils.deleteDirectory(Directory.getPluginCacheDir())
|
FileUtils.deleteDirectory(Directory.getPluginCacheDir())
|
||||||
@@ -86,8 +116,7 @@ object AutoUpdate {
|
|||||||
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
|
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
|
||||||
|
|
||||||
super.update(conn)
|
super.update(conn)
|
||||||
using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
|
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
|
||||||
while(rs.next){
|
|
||||||
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
|
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
|
||||||
if(dir.exists && dir.isDirectory){
|
if(dir.exists && dir.isDirectory){
|
||||||
dir.listFiles.foreach { file =>
|
dir.listFiles.foreach { file =>
|
||||||
@@ -102,7 +131,6 @@ object AutoUpdate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Version(1, 13),
|
Version(1, 13),
|
||||||
Version(1, 12),
|
Version(1, 12),
|
||||||
@@ -118,8 +146,7 @@ object AutoUpdate {
|
|||||||
override def update(conn: Connection): Unit = {
|
override def update(conn: Connection): Unit = {
|
||||||
super.update(conn)
|
super.update(conn)
|
||||||
// Fix wiki repository configuration
|
// Fix wiki repository configuration
|
||||||
using(conn.createStatement.executeQuery("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY")){ rs =>
|
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
|
||||||
while(rs.next){
|
|
||||||
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
|
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
|
||||||
defining(git.getRepository.getConfig){ config =>
|
defining(git.getRepository.getConfig){ config =>
|
||||||
if(!config.getBoolean("http", "receivepack", false)){
|
if(!config.getBoolean("http", "receivepack", false)){
|
||||||
@@ -130,7 +157,6 @@ object AutoUpdate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Version(1, 2),
|
Version(1, 2),
|
||||||
Version(1, 1),
|
Version(1, 1),
|
||||||
@@ -170,11 +196,10 @@ object AutoUpdate {
|
|||||||
* Update database schema automatically in the context initializing.
|
* Update database schema automatically in the context initializing.
|
||||||
*/
|
*/
|
||||||
class AutoUpdateListener extends ServletContextListener {
|
class AutoUpdateListener extends ServletContextListener {
|
||||||
import org.quartz.impl.StdSchedulerFactory
|
|
||||||
import AutoUpdate._
|
import AutoUpdate._
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
|
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
|
||||||
private val scheduler = StdSchedulerFactory.getDefaultScheduler
|
// private val scheduler = StdSchedulerFactory.getDefaultScheduler
|
||||||
|
|
||||||
override def contextInitialized(event: ServletContextEvent): Unit = {
|
override def contextInitialized(event: ServletContextEvent): Unit = {
|
||||||
val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
|
val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
|
||||||
@@ -209,31 +234,9 @@ class AutoUpdateListener extends ServletContextListener {
|
|||||||
}
|
}
|
||||||
logger.debug("End schema update")
|
logger.debug("End schema update")
|
||||||
}
|
}
|
||||||
|
|
||||||
if(SystemSettingsService.enablePluginSystem){
|
|
||||||
getDatabase(context).withSession { implicit session =>
|
|
||||||
logger.debug("Starting plugin system...")
|
|
||||||
try {
|
|
||||||
plugin.PluginSystem.init()
|
|
||||||
|
|
||||||
scheduler.start()
|
|
||||||
PluginUpdateJob.schedule(scheduler)
|
|
||||||
logger.debug("PluginUpdateJob is started.")
|
|
||||||
|
|
||||||
logger.debug("Plugin system is initialized.")
|
|
||||||
} catch {
|
|
||||||
case ex: Throwable => {
|
|
||||||
logger.error("Failed to initialize plugin system", ex)
|
|
||||||
ex.printStackTrace()
|
|
||||||
throw ex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def contextDestroyed(sce: ServletContextEvent): Unit = {
|
def contextDestroyed(sce: ServletContextEvent): Unit = {
|
||||||
scheduler.shutdown()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private def getConnection(servletContext: ServletContext): Connection =
|
private def getConnection(servletContext: ServletContext): Connection =
|
||||||
|
|||||||
@@ -28,24 +28,32 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
|
|||||||
override def setCharacterEncoding(encoding: String) = {}
|
override def setCharacterEncoding(encoding: String) = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isUpdating = request.getRequestURI.endsWith("/git-receive-pack") || "service=git-receive-pack".equals(request.getQueryString)
|
||||||
|
|
||||||
|
val settings = loadSystemSettings()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
defining(request.paths){ case Array(_, repositoryOwner, repositoryName, _*) =>
|
defining(request.paths){
|
||||||
|
case Array(_, repositoryOwner, repositoryName, _*) =>
|
||||||
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
|
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
|
||||||
case Some(repository) => {
|
case Some(repository) => {
|
||||||
if(!request.getRequestURI.endsWith("/git-receive-pack") &&
|
if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){
|
||||||
!"service=git-receive-pack".equals(request.getQueryString) && !repository.repository.isPrivate){
|
|
||||||
chain.doFilter(req, wrappedResponse)
|
chain.doFilter(req, wrappedResponse)
|
||||||
} else {
|
} else {
|
||||||
request.getHeader("Authorization") match {
|
request.getHeader("Authorization") match {
|
||||||
case null => requireAuth(response)
|
case null => requireAuth(response)
|
||||||
case auth => decodeAuthHeader(auth).split(":") match {
|
case auth => decodeAuthHeader(auth).split(":") match {
|
||||||
case Array(username, password) => getWritableUser(username, password, repository) match {
|
case Array(username, password) => {
|
||||||
|
authenticate(settings, username, password) match {
|
||||||
case Some(account) => {
|
case Some(account) => {
|
||||||
|
if(isUpdating && hasWritePermission(repository.owner, repository.name, Some(account))){
|
||||||
request.setAttribute(Keys.Request.UserName, account.userName)
|
request.setAttribute(Keys.Request.UserName, account.userName)
|
||||||
|
}
|
||||||
chain.doFilter(req, wrappedResponse)
|
chain.doFilter(req, wrappedResponse)
|
||||||
}
|
}
|
||||||
case None => requireAuth(response)
|
case None => requireAuth(response)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case _ => requireAuth(response)
|
case _ => requireAuth(response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,6 +64,10 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
|
|||||||
response.sendError(HttpServletResponse.SC_NOT_FOUND)
|
response.sendError(HttpServletResponse.SC_NOT_FOUND)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case _ => {
|
||||||
|
logger.debug(s"Not enough path arguments: ${request.paths}")
|
||||||
|
response.sendError(HttpServletResponse.SC_NOT_FOUND)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
case ex: Exception => {
|
case ex: Exception => {
|
||||||
@@ -65,13 +77,6 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def getWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo)
|
|
||||||
(implicit session: Session): Option[Account] =
|
|
||||||
authenticate(loadSystemSettings(), username, password) match {
|
|
||||||
case x @ Some(account) if(hasWritePermission(repository.owner, repository.name, x)) => x
|
|
||||||
case _ => None
|
|
||||||
}
|
|
||||||
|
|
||||||
private def requireAuth(response: HttpServletResponse): Unit = {
|
private def requireAuth(response: HttpServletResponse): Unit = {
|
||||||
response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"")
|
response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"")
|
||||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
|
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
|
||||||
|
|||||||
@@ -134,8 +134,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
|||||||
|
|
||||||
// Retrieve all issue count in the repository
|
// Retrieve all issue count in the repository
|
||||||
val issueCount =
|
val issueCount =
|
||||||
countIssue(IssueSearchCondition(state = "open"), Map.empty, false, owner -> repository) +
|
countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) +
|
||||||
countIssue(IssueSearchCondition(state = "closed"), Map.empty, false, owner -> repository)
|
countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository)
|
||||||
|
|
||||||
// Extract new commit and apply issue comment
|
// Extract new commit and apply issue comment
|
||||||
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
|
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
|
||||||
@@ -174,7 +174,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
|||||||
case ReceiveCommand.Type.CREATE |
|
case ReceiveCommand.Type.CREATE |
|
||||||
ReceiveCommand.Type.UPDATE |
|
ReceiveCommand.Type.UPDATE |
|
||||||
ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
|
ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
|
||||||
updatePullRequests(branchName)
|
updatePullRequests(owner, repository, branchName)
|
||||||
case _ =>
|
case _ =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,26 +211,4 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch pull request contents into refs/pull/${issueId}/head and update pull request table.
|
|
||||||
*/
|
|
||||||
private def updatePullRequests(branch: String) =
|
|
||||||
getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq =>
|
|
||||||
if(getRepository(pullreq.userName, pullreq.repositoryName, baseUrl).isDefined){
|
|
||||||
using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName)),
|
|
||||||
Git.open(Directory.getRepositoryDir(pullreq.requestUserName, pullreq.requestRepositoryName))){ (oldGit, newGit) =>
|
|
||||||
oldGit.fetch
|
|
||||||
.setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString)
|
|
||||||
.setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/pull/${pullreq.issueId}/head").setForceUpdate(true))
|
|
||||||
.call
|
|
||||||
|
|
||||||
val commitIdTo = oldGit.getRepository.resolve(s"refs/pull/${pullreq.issueId}/head").getName
|
|
||||||
val commitIdFrom = JGitUtil.getForkedCommitId(oldGit, newGit,
|
|
||||||
pullreq.userName, pullreq.repositoryName, pullreq.branch,
|
|
||||||
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch)
|
|
||||||
updateCommitId(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo, commitIdFrom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,192 +0,0 @@
|
|||||||
package servlet
|
|
||||||
|
|
||||||
import javax.servlet._
|
|
||||||
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
|
||||||
import org.apache.commons.io.IOUtils
|
|
||||||
import play.twirl.api.Html
|
|
||||||
import service.{AccountService, RepositoryService, SystemSettingsService}
|
|
||||||
import model.{Account, Session}
|
|
||||||
import util.{JGitUtil, Keys}
|
|
||||||
import plugin.{RawData, Fragment, PluginConnectionHolder, Redirect}
|
|
||||||
import service.RepositoryService.RepositoryInfo
|
|
||||||
import plugin.Security._
|
|
||||||
|
|
||||||
class PluginActionInvokeFilter extends Filter with SystemSettingsService with RepositoryService with AccountService {
|
|
||||||
|
|
||||||
def init(config: FilterConfig) = {}
|
|
||||||
|
|
||||||
def destroy(): Unit = {}
|
|
||||||
|
|
||||||
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
|
|
||||||
(req, res) match {
|
|
||||||
case (request: HttpServletRequest, response: HttpServletResponse) => {
|
|
||||||
Database(req.getServletContext) withTransaction { implicit session =>
|
|
||||||
val path = request.getRequestURI.substring(request.getServletContext.getContextPath.length)
|
|
||||||
if(!processGlobalAction(path, request, response) && !processRepositoryAction(path, request, response)){
|
|
||||||
chain.doFilter(req, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def processGlobalAction(path: String, request: HttpServletRequest, response: HttpServletResponse)
|
|
||||||
(implicit session: Session): Boolean = {
|
|
||||||
plugin.PluginSystem.globalActions.find(x =>
|
|
||||||
x.method.toLowerCase == request.getMethod.toLowerCase && path.matches(x.path)
|
|
||||||
).map { action =>
|
|
||||||
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
|
|
||||||
val systemSettings = loadSystemSettings()
|
|
||||||
implicit val context = app.Context(systemSettings, Option(loginAccount), request)
|
|
||||||
|
|
||||||
if(authenticate(action.security, context)){
|
|
||||||
val result = try {
|
|
||||||
PluginConnectionHolder.threadLocal.set(session.conn)
|
|
||||||
action.function(request, response, context)
|
|
||||||
} finally {
|
|
||||||
PluginConnectionHolder.threadLocal.remove()
|
|
||||||
}
|
|
||||||
processActionResult(result, request, response, context)
|
|
||||||
} else {
|
|
||||||
// TODO NotFound or Error?
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} getOrElse false
|
|
||||||
}
|
|
||||||
|
|
||||||
private def processRepositoryAction(path: String, request: HttpServletRequest, response: HttpServletResponse)
|
|
||||||
(implicit session: Session): Boolean = {
|
|
||||||
val elements = path.split("/")
|
|
||||||
if(elements.length > 3){
|
|
||||||
val owner = elements(1)
|
|
||||||
val name = elements(2)
|
|
||||||
val remain = elements.drop(3).mkString("/", "/", "")
|
|
||||||
|
|
||||||
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
|
|
||||||
val systemSettings = loadSystemSettings()
|
|
||||||
implicit val context = app.Context(systemSettings, Option(loginAccount), request)
|
|
||||||
|
|
||||||
getRepository(owner, name, systemSettings.baseUrl(request)).flatMap { repository =>
|
|
||||||
plugin.PluginSystem.repositoryActions.find(x => remain.matches(x.path)).map { action =>
|
|
||||||
if(authenticate(action.security, context, repository)){
|
|
||||||
val result = try {
|
|
||||||
PluginConnectionHolder.threadLocal.set(session.conn)
|
|
||||||
action.function(request, response, context, repository)
|
|
||||||
} finally {
|
|
||||||
PluginConnectionHolder.threadLocal.remove()
|
|
||||||
}
|
|
||||||
processActionResult(result, request, response, context)
|
|
||||||
} else {
|
|
||||||
// TODO NotFound or Error?
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
} getOrElse false
|
|
||||||
} else false
|
|
||||||
}
|
|
||||||
|
|
||||||
private def processActionResult(result: Any, request: HttpServletRequest, response: HttpServletResponse,
|
|
||||||
context: app.Context): Unit = {
|
|
||||||
result match {
|
|
||||||
case null|None => renderError(request, response, context, 404)
|
|
||||||
case x: String => renderGlobalHtml(request, response, context, x)
|
|
||||||
case Some(x: String) => renderGlobalHtml(request, response, context, x)
|
|
||||||
case x: Html => renderGlobalHtml(request, response, context, x.toString)
|
|
||||||
case Some(x: Html) => renderGlobalHtml(request, response, context, x.toString)
|
|
||||||
case x: Fragment => renderFragmentHtml(request, response, context, x.html.toString)
|
|
||||||
case Some(x: Fragment) => renderFragmentHtml(request, response, context, x.html.toString)
|
|
||||||
case x: RawData => renderRawData(request, response, context, x)
|
|
||||||
case Some(x: RawData) => renderRawData(request, response, context, x)
|
|
||||||
case x: Redirect => response.sendRedirect(x.path)
|
|
||||||
case Some(x: Redirect) => response.sendRedirect(x.path)
|
|
||||||
case x: AnyRef => renderJson(request, response, x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Authentication for global action
|
|
||||||
*/
|
|
||||||
private def authenticate(security: Security, context: app.Context)(implicit session: Session): Boolean = {
|
|
||||||
// Global Action
|
|
||||||
security match {
|
|
||||||
case All() => true
|
|
||||||
case Login() => context.loginAccount.isDefined
|
|
||||||
case Admin() => context.loginAccount.exists(_.isAdmin)
|
|
||||||
case _ => false // TODO throw Exception?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Authenticate for repository action
|
|
||||||
*/
|
|
||||||
private def authenticate(security: Security, context: app.Context, repository: RepositoryInfo)(implicit session: Session): Boolean = {
|
|
||||||
if(repository.repository.isPrivate){
|
|
||||||
// Private Repository
|
|
||||||
security match {
|
|
||||||
case Admin() => context.loginAccount.exists(_.isAdmin)
|
|
||||||
case Owner() => context.loginAccount.exists { account =>
|
|
||||||
account.userName == repository.owner ||
|
|
||||||
getGroupMembers(repository.owner).exists(m => m.userName == account.userName && m.isManager)
|
|
||||||
}
|
|
||||||
case _ => context.loginAccount.exists { account =>
|
|
||||||
account.isAdmin || account.userName == repository.owner ||
|
|
||||||
getCollaborators(repository.owner, repository.name).contains(account.userName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Public Repository
|
|
||||||
security match {
|
|
||||||
case All() => true
|
|
||||||
case Login() => context.loginAccount.isDefined
|
|
||||||
case Owner() => context.loginAccount.exists { account =>
|
|
||||||
account.userName == repository.owner ||
|
|
||||||
getGroupMembers(repository.owner).exists(m => m.userName == account.userName && m.isManager)
|
|
||||||
}
|
|
||||||
case Member() => context.loginAccount.exists { account =>
|
|
||||||
account.userName == repository.owner ||
|
|
||||||
getCollaborators(repository.owner, repository.name).contains(account.userName)
|
|
||||||
}
|
|
||||||
case Admin() => context.loginAccount.exists(_.isAdmin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def renderError(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, error: Int): Unit = {
|
|
||||||
response.sendError(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
private def renderGlobalHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, body: String): Unit = {
|
|
||||||
response.setContentType("text/html; charset=UTF-8")
|
|
||||||
val html = _root_.html.main("GitBucket", None)(Html(body))(context)
|
|
||||||
IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
|
|
||||||
}
|
|
||||||
|
|
||||||
private def renderRepositoryHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, repository: RepositoryInfo, body: String): Unit = {
|
|
||||||
response.setContentType("text/html; charset=UTF-8")
|
|
||||||
val html = _root_.html.main("GitBucket", None)(_root_.html.menu("", repository)(Html(body))(context))(context) // TODO specify active side menu
|
|
||||||
IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
|
|
||||||
}
|
|
||||||
|
|
||||||
private def renderFragmentHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, body: String): Unit = {
|
|
||||||
response.setContentType("text/html; charset=UTF-8")
|
|
||||||
IOUtils.write(body.getBytes("UTF-8"), response.getOutputStream)
|
|
||||||
}
|
|
||||||
|
|
||||||
private def renderRawData(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, rawData: RawData): Unit = {
|
|
||||||
response.setContentType(rawData.contentType)
|
|
||||||
IOUtils.write(rawData.content, response.getOutputStream)
|
|
||||||
}
|
|
||||||
|
|
||||||
private def renderJson(request: HttpServletRequest, response: HttpServletResponse, obj: AnyRef): Unit = {
|
|
||||||
import org.json4s._
|
|
||||||
import org.json4s.jackson.Serialization
|
|
||||||
import org.json4s.jackson.Serialization.write
|
|
||||||
implicit val formats = Serialization.formats(NoTypeHints)
|
|
||||||
|
|
||||||
val json = write(obj)
|
|
||||||
|
|
||||||
response.setContentType("application/json; charset=UTF-8")
|
|
||||||
IOUtils.write(json.getBytes("UTF-8"), response.getOutputStream)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -48,7 +48,7 @@ object Directory {
|
|||||||
* Directory for files which are attached to issue.
|
* Directory for files which are attached to issue.
|
||||||
*/
|
*/
|
||||||
def getAttachedDir(owner: String, repository: String): File =
|
def getAttachedDir(owner: String, repository: String): File =
|
||||||
new File(s"${RepositoryHome}/${owner}/${repository}/issues")
|
new File(s"${RepositoryHome}/${owner}/${repository}/comments")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Directory for uploaded files by the specified user.
|
* Directory for uploaded files by the specified user.
|
||||||
|
|||||||
55
src/main/scala/util/JDBCUtil.scala
Normal file
55
src/main/scala/util/JDBCUtil.scala
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import java.sql._
|
||||||
|
import util.ControlUtil._
|
||||||
|
import scala.collection.mutable.ListBuffer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides implicit class which extends java.sql.Connection.
|
||||||
|
* This is used in automatic migration in [[servlet.AutoUpdateListener]].
|
||||||
|
*/
|
||||||
|
object JDBCUtil {
|
||||||
|
|
||||||
|
implicit class RichConnection(conn: Connection){
|
||||||
|
|
||||||
|
def update(sql: String, params: Any*): Int = {
|
||||||
|
execute(sql, params: _*){ stmt =>
|
||||||
|
stmt.executeUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def select[T](sql: String, params: Any*)(f: ResultSet => T): Seq[T] = {
|
||||||
|
execute(sql, params: _*){ stmt =>
|
||||||
|
using(stmt.executeQuery()){ rs =>
|
||||||
|
val list = new ListBuffer[T]
|
||||||
|
while(rs.next){
|
||||||
|
list += f(rs)
|
||||||
|
}
|
||||||
|
list.toSeq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def selectInt(sql: String, params: Any*): Int = {
|
||||||
|
execute(sql, params: _*){ stmt =>
|
||||||
|
using(stmt.executeQuery()){ rs =>
|
||||||
|
if(rs.next) rs.getInt(1) else 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def execute[T](sql: String, params: Any*)(f: (PreparedStatement) => T): T = {
|
||||||
|
using(conn.prepareStatement(sql)){ stmt =>
|
||||||
|
params.zipWithIndex.foreach { case (p, i) =>
|
||||||
|
p match {
|
||||||
|
case x: Int => stmt.setInt(i + 1, x)
|
||||||
|
case x: String => stmt.setString(i + 1, x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f(stmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import org.eclipse.jgit.treewalk._
|
|||||||
import org.eclipse.jgit.treewalk.filter._
|
import org.eclipse.jgit.treewalk.filter._
|
||||||
import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
||||||
import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException}
|
import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException}
|
||||||
|
import org.eclipse.jgit.transport.RefSpec
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import org.eclipse.jgit.api.errors.{JGitInternalException, InvalidRefNameException, RefAlreadyExistsException, NoHeadException}
|
import org.eclipse.jgit.api.errors.{JGitInternalException, InvalidRefNameException, RefAlreadyExistsException, NoHeadException}
|
||||||
import service.RepositoryService
|
import service.RepositoryService
|
||||||
@@ -217,7 +218,7 @@ object JGitUtil {
|
|||||||
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl))
|
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
list = list.map(tuple =>
|
list.transform(tuple =>
|
||||||
if (tuple._2 != FileMode.TREE)
|
if (tuple._2 != FileMode.TREE)
|
||||||
tuple
|
tuple
|
||||||
else
|
else
|
||||||
@@ -674,6 +675,25 @@ object JGitUtil {
|
|||||||
}.head.id
|
}.head.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch pull request contents into refs/pull/${issueId}/head and return (commitIdTo, commitIdFrom)
|
||||||
|
*/
|
||||||
|
def updatePullRequest(userName: String, repositoryName:String, branch: String, issueId: Int,
|
||||||
|
requestUserName: String, requestRepositoryName: String, requestBranch: String):(String, String) =
|
||||||
|
using(Git.open(Directory.getRepositoryDir(userName, repositoryName)),
|
||||||
|
Git.open(Directory.getRepositoryDir(requestUserName, requestRepositoryName))){ (oldGit, newGit) =>
|
||||||
|
oldGit.fetch
|
||||||
|
.setRemote(Directory.getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString)
|
||||||
|
.setRefSpecs(new RefSpec(s"refs/heads/${requestBranch}:refs/pull/${issueId}/head").setForceUpdate(true))
|
||||||
|
.call
|
||||||
|
|
||||||
|
val commitIdTo = oldGit.getRepository.resolve(s"refs/pull/${issueId}/head").getName
|
||||||
|
val commitIdFrom = getForkedCommitId(oldGit, newGit,
|
||||||
|
userName, repositoryName, branch,
|
||||||
|
requestUserName, requestRepositoryName, requestBranch)
|
||||||
|
(commitIdTo, commitIdFrom)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the last modified commit of specified path
|
* Returns the last modified commit of specified path
|
||||||
* @param git the Git object
|
* @param git the Git object
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ object LDAPUtil {
|
|||||||
dn = ldapSettings.bindDN.getOrElse(""),
|
dn = ldapSettings.bindDN.getOrElse(""),
|
||||||
password = ldapSettings.bindPassword.getOrElse(""),
|
password = ldapSettings.bindPassword.getOrElse(""),
|
||||||
tls = ldapSettings.tls.getOrElse(false),
|
tls = ldapSettings.tls.getOrElse(false),
|
||||||
|
ssl = ldapSettings.ssl.getOrElse(false),
|
||||||
keystore = ldapSettings.keystore.getOrElse(""),
|
keystore = ldapSettings.keystore.getOrElse(""),
|
||||||
error = "System LDAP authentication failed."
|
error = "System LDAP authentication failed."
|
||||||
){ conn =>
|
){ conn =>
|
||||||
@@ -65,6 +66,7 @@ object LDAPUtil {
|
|||||||
dn = userDN,
|
dn = userDN,
|
||||||
password = password,
|
password = password,
|
||||||
tls = ldapSettings.tls.getOrElse(false),
|
tls = ldapSettings.tls.getOrElse(false),
|
||||||
|
ssl = ldapSettings.ssl.getOrElse(false),
|
||||||
keystore = ldapSettings.keystore.getOrElse(""),
|
keystore = ldapSettings.keystore.getOrElse(""),
|
||||||
error = "User LDAP Authentication Failed."
|
error = "User LDAP Authentication Failed."
|
||||||
){ conn =>
|
){ conn =>
|
||||||
@@ -96,7 +98,7 @@ object LDAPUtil {
|
|||||||
}).replaceAll("[^a-zA-Z0-9\\-_.]", "").replaceAll("^[_\\-]", "")
|
}).replaceAll("[^a-zA-Z0-9\\-_.]", "").replaceAll("^[_\\-]", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String, error: String)
|
private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, ssl: Boolean, keystore: String, error: String)
|
||||||
(f: LDAPConnection => Either[String, A]): Either[String, A] = {
|
(f: LDAPConnection => Either[String, A]): Either[String, A] = {
|
||||||
if (tls) {
|
if (tls) {
|
||||||
// Dynamically set Sun as the security provider
|
// Dynamically set Sun as the security provider
|
||||||
@@ -109,7 +111,13 @@ object LDAPUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val conn: LDAPConnection = new LDAPConnection(new LDAPJSSEStartTLSFactory())
|
val conn: LDAPConnection =
|
||||||
|
if(ssl) {
|
||||||
|
new LDAPConnection(new LDAPJSSESecureSocketFactory())
|
||||||
|
}else {
|
||||||
|
new LDAPConnection(new LDAPJSSEStartTLSFactory())
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Connect to the server
|
// Connect to the server
|
||||||
conn.connect(host, port)
|
conn.connect(host, port)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ trait LinkConverter { self: RequestCache =>
|
|||||||
issueIdPrefix: String = "#")(implicit context: app.Context): String = {
|
issueIdPrefix: String = "#")(implicit context: app.Context): String = {
|
||||||
value
|
value
|
||||||
// escape HTML tags
|
// escape HTML tags
|
||||||
.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """)
|
.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)
|
||||||
// convert issue id to link
|
// convert issue id to link
|
||||||
.replaceBy(("(?<=(^|\\W))" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m =>
|
.replaceBy(("(?<=(^|\\W))" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m =>
|
||||||
getIssue(repository.owner, repository.name, m.group(2)) match {
|
getIssue(repository.owner, repository.name, m.group(2)) match {
|
||||||
|
|||||||
@@ -18,9 +18,14 @@ object Markdown {
|
|||||||
/**
|
/**
|
||||||
* Converts Markdown of Wiki pages to HTML.
|
* Converts Markdown of Wiki pages to HTML.
|
||||||
*/
|
*/
|
||||||
def toHtml(markdown: String, repository: service.RepositoryService.RepositoryInfo,
|
def toHtml(markdown: String,
|
||||||
enableWikiLink: Boolean, enableRefsLink: Boolean,
|
repository: service.RepositoryService.RepositoryInfo,
|
||||||
enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): String = {
|
enableWikiLink: Boolean,
|
||||||
|
enableRefsLink: Boolean,
|
||||||
|
enableTaskList: Boolean = false,
|
||||||
|
hasWritePermission: Boolean = false,
|
||||||
|
pages: List[String] = Nil)(implicit context: app.Context): String = {
|
||||||
|
|
||||||
// escape issue id
|
// escape issue id
|
||||||
val s = if(enableRefsLink){
|
val s = if(enableRefsLink){
|
||||||
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
|
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
|
||||||
@@ -32,15 +37,19 @@ object Markdown {
|
|||||||
} else s
|
} else s
|
||||||
|
|
||||||
val rootNode = new PegDownProcessor(
|
val rootNode = new PegDownProcessor(
|
||||||
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS
|
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS | Extensions.SUPPRESS_ALL_HTML
|
||||||
).parseMarkdown(source.toCharArray)
|
).parseMarkdown(source.toCharArray)
|
||||||
|
|
||||||
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission).toHtml(rootNode)
|
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission, pages).toHtml(rootNode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GitBucketLinkRender(context: app.Context, repository: service.RepositoryService.RepositoryInfo,
|
class GitBucketLinkRender(
|
||||||
enableWikiLink: Boolean) extends LinkRenderer with WikiService {
|
context: app.Context,
|
||||||
|
repository: service.RepositoryService.RepositoryInfo,
|
||||||
|
enableWikiLink: Boolean,
|
||||||
|
pages: List[String]) extends LinkRenderer with WikiService {
|
||||||
|
|
||||||
override def render(node: WikiLinkNode): Rendering = {
|
override def render(node: WikiLinkNode): Rendering = {
|
||||||
if(enableWikiLink){
|
if(enableWikiLink){
|
||||||
try {
|
try {
|
||||||
@@ -54,7 +63,7 @@ class GitBucketLinkRender(context: app.Context, repository: service.RepositorySe
|
|||||||
|
|
||||||
val url = repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/" + StringUtil.urlEncode(page)
|
val url = repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/" + StringUtil.urlEncode(page)
|
||||||
|
|
||||||
if(getWikiPage(repository.owner, repository.name, page).isDefined){
|
if(pages.contains(page)){
|
||||||
new Rendering(url, label)
|
new Rendering(url, label)
|
||||||
} else {
|
} else {
|
||||||
new Rendering(url, label).withAttribute("class", "absent")
|
new Rendering(url, label).withAttribute("class", "absent")
|
||||||
@@ -91,9 +100,10 @@ class GitBucketHtmlSerializer(
|
|||||||
enableWikiLink: Boolean,
|
enableWikiLink: Boolean,
|
||||||
enableRefsLink: Boolean,
|
enableRefsLink: Boolean,
|
||||||
enableTaskList: Boolean,
|
enableTaskList: Boolean,
|
||||||
hasWritePermission: Boolean
|
hasWritePermission: Boolean,
|
||||||
|
pages: List[String]
|
||||||
)(implicit val context: app.Context) extends ToHtmlSerializer(
|
)(implicit val context: app.Context) extends ToHtmlSerializer(
|
||||||
new GitBucketLinkRender(context, repository, enableWikiLink),
|
new GitBucketLinkRender(context, repository, enableWikiLink, pages),
|
||||||
Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava
|
Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava
|
||||||
) with LinkConverter with RequestCache {
|
) with LinkConverter with RequestCache {
|
||||||
|
|
||||||
@@ -185,6 +195,32 @@ class GitBucketHtmlSerializer(
|
|||||||
printTag(node, "li")
|
printTag(node, "li")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override def visit(node: ExpLinkNode) {
|
||||||
|
printLink(linkRenderer.render(node, printLinkChildrenToString(node)))
|
||||||
|
}
|
||||||
|
|
||||||
|
def printLinkChildrenToString(node: SuperNode) = {
|
||||||
|
val priorPrinter = printer
|
||||||
|
printer = new Printer()
|
||||||
|
visitLinkChildren(node)
|
||||||
|
val result = printer.getString()
|
||||||
|
printer = priorPrinter
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def visitLinkChildren(node: SuperNode) {
|
||||||
|
import scala.collection.JavaConversions._
|
||||||
|
node.getChildren.foreach(child => child match {
|
||||||
|
case node: ExpImageNode => visitLinkChild(node)
|
||||||
|
case node: SuperNode => visitLinkChildren(node)
|
||||||
|
case _ => child.accept(this)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
def visitLinkChild(node: ExpImageNode) {
|
||||||
|
printer.print("<img src=\"").print(fixUrl(node.url, true)).print("\" alt=\"").printEncoded(printChildrenToString(node)).print("\"/>")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object GitBucketHtmlSerializer {
|
object GitBucketHtmlSerializer {
|
||||||
|
|||||||
@@ -38,10 +38,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* Format java.util.Date to "x {seconds/minutes/hours/days} ago"
|
* Format java.util.Date to "x {seconds/minutes/hours/days} ago"
|
||||||
* If duration over 1 month, format to "d MMM (yyyy)"
|
* If duration over 1 month, format to "d MMM (yyyy)"
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
def datetimeAgoRecentOnly(date: Date): String = {
|
def datetimeAgoRecentOnly(date: Date): String = {
|
||||||
val duration = new Date().getTime - date.getTime
|
val duration = new Date().getTime - date.getTime
|
||||||
@@ -88,9 +86,14 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
|||||||
/**
|
/**
|
||||||
* Converts Markdown of Wiki pages to HTML.
|
* Converts Markdown of Wiki pages to HTML.
|
||||||
*/
|
*/
|
||||||
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
|
def markdown(value: String,
|
||||||
enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): Html =
|
repository: service.RepositoryService.RepositoryInfo,
|
||||||
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission))
|
enableWikiLink: Boolean,
|
||||||
|
enableRefsLink: Boolean,
|
||||||
|
enableTaskList: Boolean = false,
|
||||||
|
hasWritePermission: Boolean = false,
|
||||||
|
pages: List[String] = Nil)(implicit context: app.Context): Html =
|
||||||
|
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission, pages))
|
||||||
|
|
||||||
def renderMarkup(filePath: List[String], fileContent: String, branch: String,
|
def renderMarkup(filePath: List[String], fileContent: String, branch: String,
|
||||||
repository: service.RepositoryService.RepositoryInfo,
|
repository: service.RepositoryService.RepositoryInfo,
|
||||||
@@ -152,6 +155,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
|||||||
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
|
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
|
||||||
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
|
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${m.group(3)}</a>""")
|
||||||
.replaceAll("\\[user:([^\\s]+?)\\]" , (m: Match) => user(m.group(1)).body)
|
.replaceAll("\\[user:([^\\s]+?)\\]" , (m: Match) => user(m.group(1)).body)
|
||||||
|
.replaceAll("\\[commit:([^\\s]+?)/([^\\s]+?)\\@([^\\s]+?)\\]", (m: Match) => s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/commit/${m.group(3)}">${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}</a>""")
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -246,6 +250,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def pre(value: Html): Html = Html(s"<pre>${value.body.trim.split("\n").map(_.trim).mkString("\n")}</pre>")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implicit conversion to add mkHtml() to Seq[Html].
|
* Implicit conversion to add mkHtml() to Seq[Html].
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@(groupNames: List[String])(implicit context: app.Context)
|
@(groupNames: List[String],
|
||||||
|
isCreateRepoOptionPublic: Boolean)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main("Create a New Repository"){
|
@html.main("Create a New Repository"){
|
||||||
@@ -29,7 +30,7 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="margin">
|
<fieldset class="margin">
|
||||||
<label class="radio">
|
<label class="radio">
|
||||||
<input type="radio" name="isPrivate" value="false" checked>
|
<input type="radio" name="isPrivate" value="false" @if(isCreateRepoOptionPublic){checked}>
|
||||||
<span class="strong"><img src="@assets/common/images/repo_public.png"/> </i> Public</span><br>
|
<span class="strong"><img src="@assets/common/images/repo_public.png"/> </i> Public</span><br>
|
||||||
<div>
|
<div>
|
||||||
<span>All users and guests can read this repository.</span>
|
<span>All users and guests can read this repository.</span>
|
||||||
@@ -38,7 +39,7 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label class="radio">
|
<label class="radio">
|
||||||
<input type="radio" name="isPrivate" value="true">
|
<input type="radio" name="isPrivate" value="true" @if(!isCreateRepoOptionPublic){checked}>
|
||||||
<span class="strong"><img src="@assets/common/images/repo_private.png"/> </i> Private</span><br>
|
<span class="strong"><img src="@assets/common/images/repo_private.png"/> </i> Private</span><br>
|
||||||
<div>
|
<div>
|
||||||
<span>Only collaborators can read this repository.</span>
|
<span>Only collaborators can read this repository.</span>
|
||||||
|
|||||||
@@ -11,11 +11,6 @@
|
|||||||
<li@if(active=="system"){ class="active"}>
|
<li@if(active=="system"){ class="active"}>
|
||||||
<a href="@path/admin/system">System Settings</a>
|
<a href="@path/admin/system">System Settings</a>
|
||||||
</li>
|
</li>
|
||||||
@if(service.SystemSettingsService.enablePluginSystem){
|
|
||||||
<li@if(active=="plugins"){ class="active"}>
|
|
||||||
<a href="@path/admin/plugins">Plugins</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
<li>
|
<li>
|
||||||
<a href="@path/console/login.jsp">H2 Console</a>
|
<a href="@path/console/login.jsp">H2 Console</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
@(plugins: List[app.SystemSettingsControllerBase.AvailablePlugin])(implicit context: app.Context)
|
|
||||||
@import context._
|
|
||||||
@import view.helpers._
|
|
||||||
@html.main("Plugins"){
|
|
||||||
@admin.html.menu("plugins"){
|
|
||||||
@tab("available")
|
|
||||||
<form action="@path/admin/plugins/_install" method="POST" validate="true">
|
|
||||||
<table class="table table-bordered">
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Version</th>
|
|
||||||
<th>Provider</th>
|
|
||||||
<th>Description</th>
|
|
||||||
</tr>
|
|
||||||
@plugins.zipWithIndex.map { case (plugin, i) =>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<input type="checkbox" name="pluginId[@i]" value="@plugin.id"/>
|
|
||||||
@plugin.id
|
|
||||||
</td>
|
|
||||||
<td>@plugin.version</td>
|
|
||||||
<td><a href="@plugin.url">@plugin.author</a></td>
|
|
||||||
<td>@plugin.description</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</table>
|
|
||||||
<input type="submit" id="install-plugins" class="btn btn-success" value="Install selected plugins"/>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
<script>
|
|
||||||
$(function(){
|
|
||||||
$('#install-plugins').click(function(){
|
|
||||||
return confirm('Selected plugin will be installed. Are you sure?');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
@()(implicit context: app.Context)
|
|
||||||
@import context._
|
|
||||||
@import view.helpers._
|
|
||||||
@html.main("JavaScript Console"){
|
|
||||||
@admin.html.menu("plugins"){
|
|
||||||
@tab("console")
|
|
||||||
<form method="POST">
|
|
||||||
<div class="box">
|
|
||||||
<div class="box-header">JavaScript Console</div>
|
|
||||||
<div class="box-content">
|
|
||||||
<div id="editor" style="width: 100%; height: 400px;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<fieldset>
|
|
||||||
<input type="button" id="evaluate" class="btn btn-success" value="Evaluate"/>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
<script src="@assets/vendors/ace/ace.js" type="text/javascript" charset="utf-8"></script>
|
|
||||||
<script>
|
|
||||||
$(function(){
|
|
||||||
var editor = ace.edit("editor");
|
|
||||||
editor.setTheme("ace/theme/monokai");
|
|
||||||
editor.getSession().setMode("ace/mode/javascript");
|
|
||||||
|
|
||||||
$('#evaluate').click(function(){
|
|
||||||
$.post('@path/admin/plugins/console', {
|
|
||||||
script: editor.getValue()
|
|
||||||
}, function(data){
|
|
||||||
alert('Success: ' + data);
|
|
||||||
}).fail(function(error){
|
|
||||||
alert(error.statusText);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
@(plugins: List[plugin.Plugin],
|
|
||||||
updatablePlugins: List[app.SystemSettingsControllerBase.AvailablePlugin])(implicit context: app.Context)
|
|
||||||
@import context._
|
|
||||||
@import view.helpers._
|
|
||||||
@html.main("Plugins"){
|
|
||||||
@admin.html.menu("plugins"){
|
|
||||||
@tab("installed")
|
|
||||||
<form method="POST" validate="true">
|
|
||||||
<table class="table table-bordered">
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Version</th>
|
|
||||||
<th>Provider</th>
|
|
||||||
<th>Description</th>
|
|
||||||
</tr>
|
|
||||||
@plugins.zipWithIndex.map { case (plugin, i) =>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<input type="checkbox" name="pluginId[@i]" value="@plugin.id"/>
|
|
||||||
@plugin.id
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
@plugin.version
|
|
||||||
@updatablePlugins.find(_.id == plugin.id).map { x =>
|
|
||||||
(@x.version is available)
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td><a href="@plugin.url">@plugin.author</a></td>
|
|
||||||
<td>@plugin.description</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</table>
|
|
||||||
<input type="submit" id="update-plugins" class="btn btn-success" value="Update selected plugins" formaction="@path/admin/plugins/_update"/>
|
|
||||||
<input type="submit" id="delete-plugins" class="btn btn-danger" value="Uninstall selected plugins" formaction="@path/admin/plugins/_delete"/>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
<script>
|
|
||||||
$(function(){
|
|
||||||
$('#update-plugins').click(function(){
|
|
||||||
return confirm('Selected plugin will be updated. Are you sure?');
|
|
||||||
});
|
|
||||||
$('#delete-plugins').click(function(){
|
|
||||||
return confirm('Selected plugin will be removed permanently. Are you sure?');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
@(active: String)(implicit context: app.Context)
|
|
||||||
@import context._
|
|
||||||
<ul class="nav nav-tabs">
|
|
||||||
<li@if(active == "installed"){ class="active"}><a href="@path/admin/plugins">Installed plugins</a></li>
|
|
||||||
<li@if(active == "available"){ class="active"}><a href="@path/admin/plugins/available">Available plugins</a></li>
|
|
||||||
@*
|
|
||||||
<li@if(active == "console" ){ class="active"}><a href="@path/admin/plugins/console">JavaScript console</a></li>
|
|
||||||
*@
|
|
||||||
</ul>
|
|
||||||
@@ -31,6 +31,14 @@
|
|||||||
You can use this property to adjust URL difference between the reverse proxy and GitBucket.
|
You can use this property to adjust URL difference between the reverse proxy and GitBucket.
|
||||||
</p>
|
</p>
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
|
<!-- Information -->
|
||||||
|
<!--====================================================================-->
|
||||||
|
<hr>
|
||||||
|
<label><span class="strong">Information</span> (HTML is available)</label>
|
||||||
|
<fieldset>
|
||||||
|
<textarea name="information" style="width: 600px; height: 100px;">@settings.information</textarea>
|
||||||
|
</fieldset>
|
||||||
|
<!--====================================================================-->
|
||||||
<!-- Account registration -->
|
<!-- Account registration -->
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
<hr>
|
<hr>
|
||||||
@@ -45,6 +53,33 @@
|
|||||||
<span class="strong">Deny</span> - Only administrators can create accounts.
|
<span class="strong">Deny</span> - Only administrators can create accounts.
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<hr>
|
||||||
|
<label class="strong">Default option to create a new repository</label>
|
||||||
|
<fieldset>
|
||||||
|
<label class="radio">
|
||||||
|
<input type="radio" name="isCreateRepoOptionPublic" value="true"@if(settings.isCreateRepoOptionPublic){ checked}>
|
||||||
|
<span class="strong">Public</span> - All users and guests can read that repository.
|
||||||
|
</label>
|
||||||
|
<label class="radio">
|
||||||
|
<input type="radio" name="isCreateRepoOptionPublic" value="false"@if(!settings.isCreateRepoOptionPublic){ checked}>
|
||||||
|
<span class="strong">Private</span> - Only collaborators can read that repository.
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<!--====================================================================-->
|
||||||
|
<!-- Anonymous access -->
|
||||||
|
<!--====================================================================-->
|
||||||
|
<hr>
|
||||||
|
<label class="strong">Anonymous access</label>
|
||||||
|
<fieldset>
|
||||||
|
<label class="radio">
|
||||||
|
<input type="radio" name="allowAnonymousAccess" value="true"@if(settings.allowAnonymousAccess){ checked}>
|
||||||
|
<span class="strong">Allow</span> - Anyone can view public repositories, user/group profiles.
|
||||||
|
</label>
|
||||||
|
<label class="radio">
|
||||||
|
<input type="radio" name="allowAnonymousAccess" value="false"@if(!settings.allowAnonymousAccess){ checked}>
|
||||||
|
<span class="strong">Deny</span> - Users must authenticate before viewing any information
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
<!-- Services -->
|
<!-- Services -->
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
@@ -161,6 +196,13 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<div class="controls">
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox" name="ldap.ssl"@if(settings.ldap.flatMap(_.ssl).getOrElse(false)){ checked}/> Enable SSL
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label class="control-label" for="ldapBindDN">Keystore</label>
|
<label class="control-label" for="ldapBindDN">Keystore</label>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
@html.main("Issues"){
|
@html.main("Issues"){
|
||||||
@dashboard.html.tab("issues")
|
@dashboard.html.tab("issues")
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
@issuesnavi(filter, "issues", condition)
|
||||||
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,6 @@
|
|||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@import service.IssuesService.IssueInfo
|
@import service.IssuesService.IssueInfo
|
||||||
<ul class="nav nav-pills-group pull-left fill-width">
|
|
||||||
<li class="@if(filter == "created_by"){active} first"><a href="@path/dashboard/issues/created_by@condition.toURL">Created</a></li>
|
|
||||||
<li class="@if(filter == "assigned"){active}"><a href="@path/dashboard/issues/assigned@condition.toURL">Assigned</a></li>
|
|
||||||
<li class="@if(filter == "mentioned"){active} last"><a href="@path/dashboard/issues/mentioned@condition.toURL">Mentioned</a></li>
|
|
||||||
</ul>
|
|
||||||
<table class="table table-bordered table-hover table-issues">
|
<table class="table table-bordered table-hover table-issues">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="background-color: #eee;">
|
<th style="background-color: #eee;">
|
||||||
@@ -27,7 +22,7 @@
|
|||||||
} else {
|
} else {
|
||||||
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png"/>
|
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png"/>
|
||||||
}
|
}
|
||||||
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a> ・
|
<a href="@path/@issue.userName/@issue.repositoryName">@issue.userName/@issue.repositoryName</a> ・
|
||||||
@if(issue.isPullRequest){
|
@if(issue.isPullRequest){
|
||||||
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
|
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
22
src/main/twirl/dashboard/issuesnavi.scala.html
Normal file
22
src/main/twirl/dashboard/issuesnavi.scala.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
@(filter: String,
|
||||||
|
active: String,
|
||||||
|
condition: service.IssuesService.IssueSearchCondition)(implicit context: app.Context)
|
||||||
|
@import context._
|
||||||
|
@import view.helpers._
|
||||||
|
<ul class="nav nav-pills-group pull-left fill-width">
|
||||||
|
<li class="@if(filter == "created_by"){active} first">
|
||||||
|
<a href="@path/dashboard/@active/created_by@condition.copy(author = None, assigned = None).toURL">Created</a>
|
||||||
|
</li>
|
||||||
|
<li class="@if(filter == "assigned"){active}">
|
||||||
|
<a href="@path/dashboard/@active/assigned@condition.copy(author = None, assigned = None).toURL">Assigned</a>
|
||||||
|
</li>
|
||||||
|
<li class="@if(filter == "mentioned"){active} last">
|
||||||
|
<a href="@path/dashboard/@active/mentioned@condition.copy(author = None, assigned = None).toURL">Mentioned</a>
|
||||||
|
</li>
|
||||||
|
<li class="pull-right">
|
||||||
|
<form method="GET" id="search-filter-form" action="@path/dashboard/@active" style="margin-bottom: 0px;">
|
||||||
|
<input type="text" id="search-filter-box" class="input-xlarge" name="q" style="height: 24px; width: 400px;"
|
||||||
|
value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
@html.main("Pull Requests"){
|
@html.main("Pull Requests"){
|
||||||
@dashboard.html.tab("pulls")
|
@dashboard.html.tab("pulls")
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
@issuesnavi(filter, "pulls", condition)
|
||||||
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
@(issues: List[service.IssuesService.IssueInfo],
|
|
||||||
page: Int,
|
|
||||||
openCount: Int,
|
|
||||||
closedCount: Int,
|
|
||||||
condition: service.IssuesService.IssueSearchCondition,
|
|
||||||
filter: String,
|
|
||||||
groups: List[String])(implicit context: app.Context)
|
|
||||||
@import context._
|
|
||||||
@import view.helpers._
|
|
||||||
@import service.IssuesService.IssueInfo
|
|
||||||
<ul class="nav nav-pills-group pull-left fill-width">
|
|
||||||
<li class="@if(filter == "created_by"){active} first"><a href="@path/dashboard/pulls/created_by@condition.toURL">Created</a></li>
|
|
||||||
<li class="@if(filter == "assigned"){active}"><a href="@path/dashboard/pulls/assigned@condition.toURL">Assigned</a></li>
|
|
||||||
<li class="@if(filter == "mentioned"){active} last"><a href="@path/dashboard/pulls/mentioned@condition.toURL">Mentioned</a></li>
|
|
||||||
</ul>
|
|
||||||
<table class="table table-bordered table-hover table-issues">
|
|
||||||
<tr>
|
|
||||||
<th style="background-color: #eee;">
|
|
||||||
@dashboard.html.header(openCount, closedCount, condition, groups)
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
@issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
|
|
||||||
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
|
|
||||||
<span class="pull-right muted">#@issue.issueId</span>
|
|
||||||
<div style="margin-left: 20px;">
|
|
||||||
@issue.content.map { content =>
|
|
||||||
@cut(content, 90)
|
|
||||||
}.getOrElse {
|
|
||||||
<span class="muted">No description available</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="small muted" style="margin-left: 20px;">
|
|
||||||
@avatarLink(issue.openedUserName, 20) by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
|
|
||||||
@if(commentCount > 0){
|
|
||||||
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</table>
|
|
||||||
<div class="pull-right">
|
|
||||||
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 10, condition.toURL)
|
|
||||||
</div>
|
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<img src="@assets/common/images/menu-pulls.png">
|
<img src="@assets/common/images/menu-pulls.png">
|
||||||
Pull Requests
|
Pull Requests
|
||||||
</a>
|
</a>
|
||||||
<a href="@path/dashboard/issues/repos" @if(active == "issues"){ class="active"}>
|
<a href="@path/dashboard/issues" @if(active == "issues"){ class="active"}>
|
||||||
<img src="@assets/common/images/menu-issues.png">
|
<img src="@assets/common/images/menu-issues.png">
|
||||||
Issues
|
Issues
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
@(activity.activityType match {
|
@(activity.activityType match {
|
||||||
case "open_issue" => detailActivity(activity, "activity-issue.png")
|
case "open_issue" => detailActivity(activity, "activity-issue.png")
|
||||||
case "comment_issue" => detailActivity(activity, "activity-comment.png")
|
case "comment_issue" => detailActivity(activity, "activity-comment.png")
|
||||||
|
case "comment_commit" => detailActivity(activity, "activity-comment.png")
|
||||||
case "close_issue" => detailActivity(activity, "activity-issue-close.png")
|
case "close_issue" => detailActivity(activity, "activity-issue-close.png")
|
||||||
case "reopen_issue" => detailActivity(activity, "activity-issue-reopen.png")
|
case "reopen_issue" => detailActivity(activity, "activity-issue-reopen.png")
|
||||||
case "open_pullreq" => detailActivity(activity, "activity-merge.png")
|
case "open_pullreq" => detailActivity(activity, "activity-merge.png")
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
@defining("(id=\")([\\w\\-]*)(\")".r.findFirstMatchIn(textarea.body).map(_.group(2))){ textareaId =>
|
@defining("(id=\")([\\w\\-]*)(\")".r.findFirstMatchIn(textarea.body).map(_.group(2))){ textareaId =>
|
||||||
<script>
|
<script>
|
||||||
$(function(){
|
$(function(){
|
||||||
|
try {
|
||||||
$([$('#@textareaId').closest('div')[0], $('#@textareaId').next('div')[0]]).dropzone({
|
$([$('#@textareaId').closest('div')[0], $('#@textareaId').next('div')[0]]).dropzone({
|
||||||
url: '@path/upload/image/@owner/@repository',
|
url: '@path/upload/image/@owner/@repository',
|
||||||
maxFilesize: 10,
|
maxFilesize: 10,
|
||||||
@@ -19,6 +20,11 @@ $(function(){
|
|||||||
$(file.previewElement).prevAll('div.dz-preview').addBack().remove();
|
$(file.previewElement).prevAll('div.dz-preview').addBack().remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} catch(e) {
|
||||||
|
if (e.message !== "Dropzone already attached.") {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Adjust clickable area width
|
// Adjust clickable area width
|
||||||
$('#@textareaId').next('div.clickable').css('width', ($('#@textareaId').width() + 8) + 'px');
|
$('#@textareaId').next('div.clickable').css('width', ($('#@textareaId').width() + 8) + 'px');
|
||||||
|
|||||||
35
src/main/twirl/helper/commitcomment.scala.html
Normal file
35
src/main/twirl/helper/commitcomment.scala.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
@(comment: model.CommitComment,
|
||||||
|
hasWritePermission: Boolean,
|
||||||
|
repository: service.RepositoryService.RepositoryInfo,
|
||||||
|
latestCommitId: Option[String] = None)(implicit context: app.Context)
|
||||||
|
@import context._
|
||||||
|
@import view.helpers._
|
||||||
|
<div class="@if(comment.fileName.isDefined && (!latestCommitId.isDefined || latestCommitId.get == comment.commitId)){inline-comment}" @if(comment.fileName.isDefined){filename=@comment.fileName.get} @if(comment.newLine.isDefined){newline=@comment.newLine.get} @if(comment.oldLine.isDefined){oldline=@comment.oldLine.get}>
|
||||||
|
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
|
||||||
|
<div class="box commit-comment-box commit-comment-@comment.commentId">
|
||||||
|
<div class="box-header-small">
|
||||||
|
@user(comment.commentedUserName, styleClass="username strong")
|
||||||
|
<span class="muted">
|
||||||
|
commented
|
||||||
|
@if(comment.pullRequest){
|
||||||
|
on this Pull Request
|
||||||
|
}else{
|
||||||
|
@if(comment.fileName.isDefined){
|
||||||
|
on @comment.fileName.get
|
||||||
|
}
|
||||||
|
in <a href="@path/@repository.owner/@repository.name/commit/@comment.commitId">@comment.commitId.substring(0, 7)</a>
|
||||||
|
}
|
||||||
|
@helper.html.datetimeago(comment.registeredDate)
|
||||||
|
</span>
|
||||||
|
<span class="pull-right">
|
||||||
|
@if(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false)){
|
||||||
|
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>
|
||||||
|
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle"></i></a>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="box-content commit-commentContent-@comment.commentId">
|
||||||
|
@markdown(comment.content, repository, false, true, true, hasWritePermission)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -2,7 +2,10 @@
|
|||||||
repository: service.RepositoryService.RepositoryInfo,
|
repository: service.RepositoryService.RepositoryInfo,
|
||||||
newCommitId: Option[String],
|
newCommitId: Option[String],
|
||||||
oldCommitId: Option[String],
|
oldCommitId: Option[String],
|
||||||
showIndex: Boolean)(implicit context: app.Context)
|
showIndex: Boolean,
|
||||||
|
issueId: Option[Int],
|
||||||
|
hasWritePermission: Boolean,
|
||||||
|
showLineNotes: Boolean)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
@import org.eclipse.jgit.diff.DiffEntry.ChangeType
|
||||||
@@ -39,29 +42,36 @@
|
|||||||
}
|
}
|
||||||
@diffs.zipWithIndex.map { case (diff, i) =>
|
@diffs.zipWithIndex.map { case (diff, i) =>
|
||||||
<a name="diff-@i"></a>
|
<a name="diff-@i"></a>
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered" commitId="@newCommitId" fileName="@diff.newPath">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="font-weight: normal; line-height: 27px;" class="box-header">
|
<th style="font-weight: normal; line-height: 27px;" class="box-header">
|
||||||
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
|
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
|
||||||
@diff.oldPath -> @diff.newPath
|
<img src="@assets/common/images/diff_move.png"/> @diff.oldPath -> @diff.newPath
|
||||||
@if(newCommitId.isDefined){
|
@if(newCommitId.isDefined){
|
||||||
<div class="pull-right align-right">
|
<div class="pull-right align-right">
|
||||||
|
<label class="checkbox" style="display: inline-block;"><input type="checkbox" class="toggle-notes" checked><span>Show notes</span></label>
|
||||||
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a>
|
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){
|
@if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){
|
||||||
@diff.newPath
|
@if(diff.changeType == ChangeType.ADD){
|
||||||
|
<img src="@assets/common/images/diff_add.png"/>
|
||||||
|
}else{
|
||||||
|
<img src="@assets/common/images/diff_edit.png"/>
|
||||||
|
} @diff.newPath
|
||||||
@if(newCommitId.isDefined){
|
@if(newCommitId.isDefined){
|
||||||
<div class="pull-right align-right">
|
<div class="pull-right align-right">
|
||||||
|
<label class="checkbox" style="display: inline-block;"><input type="checkbox" class="toggle-notes" checked><span>Show notes</span></label>
|
||||||
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a>
|
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-small">View file @@ @newCommitId.get.substring(0, 10)</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@if(diff.changeType == ChangeType.DELETE){
|
@if(diff.changeType == ChangeType.DELETE){
|
||||||
@diff.oldPath
|
<img src="@assets/common/images/diff_delete.png"/> @diff.oldPath
|
||||||
@if(oldCommitId.isDefined){
|
@if(oldCommitId.isDefined){
|
||||||
<div class="pull-right align-right">
|
<div class="pull-right align-right">
|
||||||
|
<label class="checkbox" style="display: inline-block;"><input type="checkbox" class="toggle-notes" checked><span>Show notes</span></label>
|
||||||
<a href="@url(repository)/blob/@oldCommitId.get/@diff.oldPath" class="btn btn-small">View file @@ @oldCommitId.get.substring(0, 10)</a>
|
<a href="@url(repository)/blob/@oldCommitId.get/@diff.oldPath" class="btn btn-small">View file @@ @oldCommitId.get.substring(0, 10)</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -110,7 +120,15 @@ $(function(){
|
|||||||
renderDiffs(0);
|
renderDiffs(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('.toggle-notes').change(function() {
|
||||||
|
if (!$(this).prop('checked')) {
|
||||||
|
$(this).closest('table').find('.not-diff.inline-comment-form').remove();
|
||||||
|
}
|
||||||
|
$(this).closest('table').find('.not-diff').toggle();
|
||||||
|
});
|
||||||
|
|
||||||
function renderDiffs(viewType){
|
function renderDiffs(viewType){
|
||||||
|
window.viewType = viewType;
|
||||||
@diffs.zipWithIndex.map { case (diff, i) =>
|
@diffs.zipWithIndex.map { case (diff, i) =>
|
||||||
@if(diff.newContent != None || diff.oldContent != None){
|
@if(diff.newContent != None || diff.oldContent != None){
|
||||||
if($('#oldText-@i').length > 0){
|
if($('#oldText-@i').length > 0){
|
||||||
@@ -118,6 +136,116 @@ $(function(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@if(showLineNotes){
|
||||||
|
function getInlineContainer(where) {
|
||||||
|
if (viewType == 0) {
|
||||||
|
if (where === 'new') {
|
||||||
|
return $('<tr class="not-diff"><td colspan="2"></td><td colspan="2" class="comment-box-container"></td></tr>');
|
||||||
|
} else if (where === 'old') {
|
||||||
|
return $('<tr class="not-diff"><td colspan="2" class="comment-box-container"></td><td colspan="2"></td></tr>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $('<tr class="not-diff"><td colspan="3" class="comment-box-container"></td></tr>');
|
||||||
|
}
|
||||||
|
$('.inline-comment').each(function(i, v) {
|
||||||
|
var $v = $(v), filename = $v.attr('filename'),
|
||||||
|
oldline = $v.attr('oldline'), newline = $v.attr('newline');
|
||||||
|
if (typeof $('#show-notes')[0] !== 'undefined' && !$('#show-notes')[0].checked) {
|
||||||
|
$(this).hide();
|
||||||
|
}
|
||||||
|
var tmp;
|
||||||
|
var diff;
|
||||||
|
if (typeof oldline !== 'undefined') {
|
||||||
|
if (typeof newline !== 'undefined') {
|
||||||
|
tmp = getInlineContainer();
|
||||||
|
} else {
|
||||||
|
tmp = getInlineContainer('old');
|
||||||
|
}
|
||||||
|
tmp.children('td:first').html($(this).clone().show());
|
||||||
|
diff = $('table[filename="' + filename + '"]');
|
||||||
|
diff.find('table.diff').find('.oldline[line-number=' + oldline + ']')
|
||||||
|
.parent().nextAll(':not(.not-diff):first').before(tmp);
|
||||||
|
} else {
|
||||||
|
tmp = getInlineContainer('new');
|
||||||
|
tmp.children('td:last').html($(this).clone().show());
|
||||||
|
diff = $('table[filename="' + filename + '"]');
|
||||||
|
diff.find('table.diff').find('.newline[line-number=' + newline + ']')
|
||||||
|
.parent().nextAll(':not(.not-diff):first').before(tmp);
|
||||||
|
}
|
||||||
|
if (!diff.find('.toggle-notes').prop('checked')) {
|
||||||
|
tmp.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@if(hasWritePermission) {
|
||||||
|
$('table.diff td').hover(
|
||||||
|
function() {
|
||||||
|
$(this).find('b').css('display', 'inline-block');
|
||||||
|
},
|
||||||
|
function() {
|
||||||
|
$(this).find('b').css('display', 'none');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
$('table.diff th').hover(
|
||||||
|
function() {
|
||||||
|
$(this).nextAll().find('b').first().css('display', 'inline-block');
|
||||||
|
},
|
||||||
|
function() {
|
||||||
|
$(this).nextAll().find('b').first().css('display', 'none');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
$('.add-comment').click(function() {
|
||||||
|
var $this = $(this),
|
||||||
|
$tr = $this.closest('tr'),
|
||||||
|
$check = $this.closest('table:not(.diff)').find('.toggle-notes');
|
||||||
|
if (!$check.prop('checked')) {
|
||||||
|
$check.prop('checked', true).trigger('change');
|
||||||
|
}
|
||||||
|
if (!$tr.nextAll(':not(.not-diff):first').prev().hasClass('inline-comment-form')) {
|
||||||
|
var commitId = $this.closest('.table-bordered').attr('commitId'),
|
||||||
|
fileName = $this.closest('.table-bordered').attr('fileName'),
|
||||||
|
oldLineNumber, newLineNumber,
|
||||||
|
url = '@url(repository)/commit/' + commitId + '/comment/_form?fileName=' + fileName@issueId.map { id => + '&issueId=@id' };
|
||||||
|
if (viewType == 0) {
|
||||||
|
oldLineNumber = $this.parent().prev('.oldline').attr('line-number');
|
||||||
|
newLineNumber = $this.parent().prev('.newline').attr('line-number');
|
||||||
|
} else {
|
||||||
|
oldLineNumber = $this.parent().prevAll('.oldline').attr('line-number');
|
||||||
|
newLineNumber = $this.parent().prevAll('.newline').attr('line-number');
|
||||||
|
}
|
||||||
|
if (!isNaN(oldLineNumber) && oldLineNumber) {
|
||||||
|
url += ('&oldLineNumber=' + oldLineNumber)
|
||||||
|
}
|
||||||
|
if (!isNaN(newLineNumber) && newLineNumber) {
|
||||||
|
url += ('&newLineNumber=' + newLineNumber)
|
||||||
|
}
|
||||||
|
$.get(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
dataType : 'html'
|
||||||
|
},
|
||||||
|
function(responseContent) {
|
||||||
|
$this.hide();
|
||||||
|
var tmp;
|
||||||
|
if (!isNaN(oldLineNumber) && oldLineNumber) {
|
||||||
|
if (!isNaN(newLineNumber) && newLineNumber) {
|
||||||
|
tmp = getInlineContainer();
|
||||||
|
} else {
|
||||||
|
tmp = getInlineContainer('old');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tmp = getInlineContainer('new');
|
||||||
|
}
|
||||||
|
tmp.addClass('inline-comment-form').children('.comment-box-container').html(responseContent);
|
||||||
|
$tr.nextAll(':not(.not-diff):first').before(tmp);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('table.diff').on('click', '.btn-default', function() {
|
||||||
|
$(this).closest('.inline-comment-form').remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
18
src/main/twirl/helper/forkrepository.scala.html
Normal file
18
src/main/twirl/helper/forkrepository.scala.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
@(repository: service.RepositoryService.RepositoryInfo,
|
||||||
|
groupAndPerm: Map[String, Boolean])(implicit context: app.Context)
|
||||||
|
@import context._
|
||||||
|
@import view.helpers._
|
||||||
|
<h2 class="facebox-header">Where should we fork this repository?</h2>
|
||||||
|
<form action="@url(repository)/fork" id="fork" method="post">
|
||||||
|
<div class="owner-select-grid">
|
||||||
|
<div class="owner-select-target js-fork-owner-select-target enabled">@avatar(loginAccount.get.userName, 100)<span class="owner css-truncate" title="@@@loginAccount.get.userName">@@@loginAccount.get.userName</span></div>
|
||||||
|
@for((groupName, isManager) <- groupAndPerm) {
|
||||||
|
@if(isManager) {
|
||||||
|
<div class="owner-select-target js-fork-owner-select-target enabled">@avatar(groupName, 100)<span class="owner css-truncate" title="@@@groupName">@@@groupName</span></div>
|
||||||
|
} else {
|
||||||
|
<div title="You don't have permission to fork here." class="owner-select-target js-fork-owner-select-target disabled">@avatar(groupName, 100)<span class="owner css-truncate" title="@@@groupName">@@@groupName</span></div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<input id="account" name="account" type="hidden" />
|
||||||
|
</form>
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean, hasWritePermission: Boolean,
|
@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean, hasWritePermission: Boolean,
|
||||||
style: String = "", placeholder: String = "Leave a comment", elastic: Boolean = false)(implicit context: app.Context)
|
style: String = "", placeholder: String = "Leave a comment", elastic: Boolean = false, uid: Long = new java.util.Date().getTime())(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
<div class="tabbable">
|
<div class="tabbable">
|
||||||
<ul class="nav nav-tabs" style="height: 37px;">
|
<ul class="nav nav-tabs" style="height: 37px;">
|
||||||
<li class="active"><a href="#tab1" data-toggle="tab">Write</a></li>
|
<li class="active"><a href="#tab@uid" data-toggle="tab">Write</a></li>
|
||||||
<li><a href="#tab2" data-toggle="tab" id="preview">Preview</a></li>
|
<li><a href="#tab@(uid+1)" data-toggle="tab" id="preview@uid">Preview</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div class="tab-pane active" id="tab1">
|
<div class="tab-pane active" id="tab@uid">
|
||||||
<span id="error-content" class="error"></span>
|
<span id="error-content" class="error"></span>
|
||||||
@textarea = {
|
@textarea = {
|
||||||
<textarea id="content" name="content"@if(style.nonEmpty){ style="@style"} placeholder="@placeholder">@content</textarea>
|
<textarea id="content@uid" name="content"@if(style.nonEmpty){ style="@style"} placeholder="@placeholder">@content</textarea>
|
||||||
}
|
}
|
||||||
@if(enableWikiLink){
|
@if(enableWikiLink){
|
||||||
@textarea
|
@textarea
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
@helper.html.attached(repository.owner, repository.name)(textarea)
|
@helper.html.attached(repository.owner, repository.name)(textarea)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="tab2">
|
<div class="tab-pane" id="tab@(uid+1)">
|
||||||
<div class="markdown-body" id="preview-area">
|
<div class="markdown-body" id="preview-area@uid">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,18 +30,18 @@
|
|||||||
<script>
|
<script>
|
||||||
$(function(){
|
$(function(){
|
||||||
@if(elastic){
|
@if(elastic){
|
||||||
$('#content').elastic();
|
$('#content@uid').elastic();
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#preview').click(function(){
|
$('#preview@uid').click(function(){
|
||||||
$('#preview-area').html('<img src="@assets/common/images/indicator.gif"> Previewing...');
|
$('#preview-area@uid').html('<img src="@assets/common/images/indicator.gif"> Previewing...');
|
||||||
$.post('@url(repository)/_preview', {
|
$.post('@url(repository)/_preview', {
|
||||||
content : $('#content').val(),
|
content : $('#content@uid').val(),
|
||||||
enableWikiLink : @enableWikiLink,
|
enableWikiLink : @enableWikiLink,
|
||||||
enableRefsLink : @enableRefsLink,
|
enableRefsLink : @enableRefsLink,
|
||||||
enableTaskList : @enableTaskList
|
enableTaskList : @enableTaskList
|
||||||
}, function(data){
|
}, function(data){
|
||||||
$('#preview-area').html(data);
|
$('#preview-area@uid').html(data);
|
||||||
prettyPrint();
|
prettyPrint();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,14 @@
|
|||||||
</div>
|
</div>
|
||||||
@helper.html.activities(activities)
|
@helper.html.activities(activities)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="span4">
|
<div class="span4">
|
||||||
|
@settings.information.map { information =>
|
||||||
|
<div class="alert alert-info" style="background-color: white; color: #555; border-color: #4183c4; font-size: small; line-height: 120%;">
|
||||||
|
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||||
|
@Html(information)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@if(loginAccount.isEmpty){
|
@if(loginAccount.isEmpty){
|
||||||
@signinform(settings)
|
@signinform(settings)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
@(issue: model.Issue,
|
@(issue: Option[model.Issue],
|
||||||
comments: List[model.IssueComment],
|
comments: List[model.Comment],
|
||||||
hasWritePermission: Boolean,
|
hasWritePermission: Boolean,
|
||||||
repository: service.RepositoryService.RepositoryInfo,
|
repository: service.RepositoryService.RepositoryInfo,
|
||||||
pullreq: Option[model.PullRequest] = None)(implicit context: app.Context)
|
pullreq: Option[model.PullRequest] = None)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
<div class="issue-avatar-image">@avatar(issue.openedUserName, 48)</div>
|
@if(issue.isDefined){
|
||||||
|
<div class="issue-avatar-image">@avatar(issue.get.openedUserName, 48)</div>
|
||||||
<div class="box issue-comment-box">
|
<div class="box issue-comment-box">
|
||||||
<div class="box-header-small">
|
<div class="box-header-small">
|
||||||
@user(issue.openedUserName, styleClass="username strong") <span class="muted">commented @helper.html.datetimeago(issue.registeredDate)</span>
|
@user(issue.get.openedUserName, styleClass="username strong") <span class="muted">commented @helper.html.datetimeago(issue.get.registeredDate)</span>
|
||||||
<span class="pull-right">
|
<span class="pull-right">
|
||||||
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
|
@if(hasWritePermission || loginAccount.map(_.userName == issue.get.openedUserName).getOrElse(false)){
|
||||||
<a href="#" data-issue-id="@issue.issueId"><i class="icon-pencil"></i></a>
|
<a href="#" data-issue-id="@issue.get.issueId"><i class="icon-pencil"></i></a>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-content issue-content" id="issueContent">
|
<div class="box-content issue-content" id="issueContent">
|
||||||
@markdown(issue.content getOrElse "No description provided.", repository, false, true, true, hasWritePermission)
|
@markdown(issue.get.content getOrElse "No description provided.", repository, false, true, true, hasWritePermission)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@comments.map { comment =>
|
@comments.map {
|
||||||
|
case comment: model.IssueComment => {
|
||||||
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){
|
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){
|
||||||
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
|
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
|
||||||
<div class="box issue-comment-box" id="comment-@comment.commentId">
|
<div class="box issue-comment-box" id="comment-@comment.commentId">
|
||||||
@@ -35,8 +38,8 @@
|
|||||||
@helper.html.datetimeago(comment.registeredDate)
|
@helper.html.datetimeago(comment.registeredDate)
|
||||||
</span>
|
</span>
|
||||||
<span class="pull-right">
|
<span class="pull-right">
|
||||||
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" &&
|
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer"
|
||||||
(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
|
&& (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
|
||||||
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>
|
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>
|
||||||
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle"></i></a>
|
<a href="#" data-comment-id="@comment.commentId"><i class="icon-remove-circle"></i></a>
|
||||||
}
|
}
|
||||||
@@ -68,7 +71,7 @@
|
|||||||
@if(pullreq.get.requestUserName == repository.owner){
|
@if(pullreq.get.requestUserName == repository.owner){
|
||||||
<span class="label label-info monospace">@pullreq.map(_.branch)</span> from <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span>
|
<span class="label label-info monospace">@pullreq.map(_.branch)</span> from <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span>
|
||||||
} else {
|
} else {
|
||||||
<span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span> to <span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span>
|
<span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span> from <span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span>
|
||||||
}
|
}
|
||||||
@helper.html.datetimeago(comment.registeredDate)
|
@helper.html.datetimeago(comment.registeredDate)
|
||||||
</div>
|
</div>
|
||||||
@@ -77,7 +80,7 @@
|
|||||||
<div class="small issue-comment-action">
|
<div class="small issue-comment-action">
|
||||||
<span class="label label-important">Closed</span>
|
<span class="label label-important">Closed</span>
|
||||||
@avatar(comment.commentedUserName, 20)
|
@avatar(comment.commentedUserName, 20)
|
||||||
@if(issue.isPullRequest){
|
@if(issue.isDefined && issue.get.isPullRequest){
|
||||||
@user(comment.commentedUserName, styleClass="username strong") closed the pull request @helper.html.datetimeago(comment.registeredDate)
|
@user(comment.commentedUserName, styleClass="username strong") closed the pull request @helper.html.datetimeago(comment.registeredDate)
|
||||||
} else {
|
} else {
|
||||||
@user(comment.commentedUserName, styleClass="username strong") closed the issue @helper.html.datetimeago(comment.registeredDate)
|
@user(comment.commentedUserName, styleClass="username strong") closed the issue @helper.html.datetimeago(comment.registeredDate)
|
||||||
@@ -99,9 +102,14 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case comment: model.CommitComment => {
|
||||||
|
@helper.html.commitcomment(comment, hasWritePermission, repository, pullreq.map(_.commitIdTo))
|
||||||
|
}
|
||||||
|
}
|
||||||
<script>
|
<script>
|
||||||
$(function(){
|
$(function(){
|
||||||
$('i.icon-pencil').click(function(){
|
@if(issue.isDefined){
|
||||||
|
$('.issue-comment-box i.icon-pencil').click(function(){
|
||||||
var id = $(this).closest('a').data('comment-id');
|
var id = $(this).closest('a').data('comment-id');
|
||||||
var url = '@url(repository)/issue_comments/_data/' + id;
|
var url = '@url(repository)/issue_comments/_data/' + id;
|
||||||
var $content = $('#commentContent-' + id);
|
var $content = $('#commentContent-' + id);
|
||||||
@@ -134,6 +142,34 @@ $(function(){
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
$(document).on('click', '.commit-comment-box i.icon-pencil', function(){
|
||||||
|
var id = $(this).closest('a').data('comment-id');
|
||||||
|
var url = '@url(repository)/commit_comments/_data/' + id;
|
||||||
|
var $content = $('.commit-commentContent-' + id, $(this).closest('.box'));
|
||||||
|
|
||||||
|
$.get(url,
|
||||||
|
{
|
||||||
|
dataType : 'html'
|
||||||
|
},
|
||||||
|
function(data){
|
||||||
|
$content.empty().html(data);
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
$(document).on('click', '.commit-comment-box i.icon-remove-circle', function(){
|
||||||
|
if(confirm('Are you sure you want to delete this?')) {
|
||||||
|
var id = $(this).closest('a').data('comment-id');
|
||||||
|
$.post('@url(repository)/commit_comments/delete/' + id,
|
||||||
|
function(data){
|
||||||
|
if(data > 0) {
|
||||||
|
$('.commit-comment-' + id).closest('.not-diff').remove();
|
||||||
|
$('.commit-comment-' + id).closest('.inline-comment').remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
var extractMarkdown = function(data){
|
var extractMarkdown = function(data){
|
||||||
$('body').append('<div id="tmp"></div>');
|
$('body').append('<div id="tmp"></div>');
|
||||||
@@ -156,15 +192,40 @@ $(function(){
|
|||||||
return ss.join('');
|
return ss.join('');
|
||||||
};
|
};
|
||||||
|
|
||||||
$('#issueContent').on('click', ':checkbox', function(ev){
|
$('div[class*=commit-commentContent-]').on('click', ':checkbox', function(ev){
|
||||||
var checkboxes = $('#issueContent :checkbox');
|
var $commentContent = $(ev.target).parents('div[class*=commit-commentContent-]'),
|
||||||
$.get('@url(repository)/issues/_data/@issue.issueId',
|
commentId = $commentContent.attr('class').match(/commit-commentContent-.+/)[0].replace(/commit-commentContent-/, ''),
|
||||||
|
checkboxes = $commentContent.find(':checkbox');
|
||||||
|
$.get('@url(repository)/commit_comments/_data/' + commentId,
|
||||||
{
|
{
|
||||||
dataType : 'html'
|
dataType : 'html'
|
||||||
},
|
},
|
||||||
function(responseContent){
|
function(responseContent){
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '@url(repository)/issues/edit/@issue.issueId',
|
url: '@url(repository)/commit_comments/edit/' + commentId,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
issueId : 0,
|
||||||
|
content : replaceTaskList(responseContent, checkboxes)
|
||||||
|
},
|
||||||
|
success: function(data) {
|
||||||
|
$('.commit-commentContent-' + commentId).html(data.content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
@if(issue.isDefined){
|
||||||
|
$('#issueContent').on('click', ':checkbox', function(ev){
|
||||||
|
var checkboxes = $('#issueContent :checkbox');
|
||||||
|
$.get('@url(repository)/issues/_data/@issue.get.issueId',
|
||||||
|
{
|
||||||
|
dataType : 'html'
|
||||||
|
},
|
||||||
|
function(responseContent){
|
||||||
|
$.ajax({
|
||||||
|
url: '@url(repository)/issues/edit/@issue.get.issueId',
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
data: {
|
data: {
|
||||||
title : $('#issueTitle').text(),
|
title : $('#issueTitle').text(),
|
||||||
@@ -196,5 +257,7 @@ $(function(){
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
|
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
|
||||||
@html.menu("issues", repository){
|
@html.menu("issues", repository){
|
||||||
@tab("issues", false, repository)
|
@navigation("issues", false, repository)
|
||||||
<br/><br/><hr style="margin-bottom: 10px;">
|
<br/><br/><hr style="margin-bottom: 10px;">
|
||||||
<form action="@url(repository)/issues/new" method="POST" validate="true">
|
<form action="@url(repository)/issues/new" method="POST" validate="true">
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
|
|||||||
@@ -10,8 +10,16 @@
|
|||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
|
@html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
|
||||||
@html.menu("issues", repository){
|
@html.menu("issues", repository){
|
||||||
<ul class="nav nav-tabs pull-left fill-width">
|
<div>
|
||||||
<li class="pull-left">
|
<div class="show-title pull-right">
|
||||||
|
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
|
||||||
|
<a class="btn btn-small" href="#" id="edit">Edit</a>
|
||||||
|
}
|
||||||
|
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New issue</a>
|
||||||
|
</div>
|
||||||
|
<div class="edit-title pull-right" style="display: none;">
|
||||||
|
<a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>
|
||||||
|
</div>
|
||||||
<h1>
|
<h1>
|
||||||
<span class="show-title">
|
<span class="show-title">
|
||||||
<span id="show-title">@issue.title</span>
|
<span id="show-title">@issue.title</span>
|
||||||
@@ -19,9 +27,10 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="edit-title" style="display: none;">
|
<span class="edit-title" style="display: none;">
|
||||||
<span id="error-edit-title" class="error"></span>
|
<span id="error-edit-title" class="error"></span>
|
||||||
<input type="text" class="span9" id="edit-title" value="@issue.title"/>
|
<input type="text" style="width: 700px;" id="edit-title" value="@issue.title"/>
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
</div>
|
||||||
@if(issue.closed) {
|
@if(issue.closed) {
|
||||||
<span class="label label-important issue-status">Closed</span>
|
<span class="label label-important issue-status">Closed</span>
|
||||||
} else {
|
} else {
|
||||||
@@ -29,28 +38,16 @@
|
|||||||
}
|
}
|
||||||
<span class="muted">
|
<span class="muted">
|
||||||
@user(issue.openedUserName, styleClass="username strong") opened this issue @helper.html.datetimeago(issue.registeredDate) - @defining(
|
@user(issue.openedUserName, styleClass="username strong") opened this issue @helper.html.datetimeago(issue.registeredDate) - @defining(
|
||||||
comments.filter( _.action.contains("comment") ).size
|
comments.count( _.action.contains("comment") )
|
||||||
){ count =>
|
){ count =>
|
||||||
@count @plural(count, "comment")
|
@count @plural(count, "comment")
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
<br/><br/>
|
<br/><br/>
|
||||||
</li>
|
<hr>
|
||||||
<li class="pull-right">
|
|
||||||
<div class="show-title">
|
|
||||||
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
|
|
||||||
<a class="btn btn-small" href="#" id="edit">Edit</a>
|
|
||||||
}
|
|
||||||
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New issue</a>
|
|
||||||
</div>
|
|
||||||
<div class="edit-title" style="display: none;">
|
|
||||||
<a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<div class="span10">
|
<div class="span10">
|
||||||
@commentlist(issue, comments, hasWritePermission, repository)
|
@commentlist(Some(issue), comments, hasWritePermission, repository)
|
||||||
@commentform(issue, true, hasWritePermission, repository)
|
@commentform(issue, true, hasWritePermission, repository)
|
||||||
</div>
|
</div>
|
||||||
<div class="span2">
|
<div class="span2">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@(issue: model.Issue,
|
@(issue: model.Issue,
|
||||||
comments: List[model.IssueComment],
|
comments: List[model.Comment],
|
||||||
issueLabels: List[model.Label],
|
issueLabels: List[model.Label],
|
||||||
collaborators: List[String],
|
collaborators: List[String],
|
||||||
milestones: List[(model.Milestone, Int, Int)],
|
milestones: List[(model.Milestone, Int, Int)],
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main(s"Labels - ${repository.owner}/${repository.name}"){
|
@html.main(s"Labels - ${repository.owner}/${repository.name}"){
|
||||||
@html.menu("issues", repository){
|
@html.menu("issues", repository){
|
||||||
@issues.html.tab("labels", hasWritePermission, repository)
|
@issues.html.navigation("labels", hasWritePermission, repository)
|
||||||
<br>
|
<br>
|
||||||
<table class="table table-bordered table-hover table-issues" id="new-label-table" style="display: none;">
|
<table class="table table-bordered table-hover table-issues" id="new-label-table" style="display: none;">
|
||||||
<tr><td></td></tr>
|
<tr><td></td></tr>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){
|
@html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){
|
||||||
@html.menu(target, repository){
|
@html.menu(target, repository){
|
||||||
@tab(target, true, repository)
|
@navigation(target, true, repository, Some(condition))
|
||||||
@listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
|
@listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
|
||||||
@if(hasWritePermission){
|
@if(hasWritePermission){
|
||||||
<form id="batcheditForm" method="POST">
|
<form id="batcheditForm" method="POST">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<h4>New milestone</h4>
|
<h4>New milestone</h4>
|
||||||
<div class="muted">Create a new milestone to help organize your issues and pull requests.</div>
|
<div class="muted">Create a new milestone to help organize your issues and pull requests.</div>
|
||||||
} else {
|
} else {
|
||||||
@issues.html.tab("milestones", false, repository)
|
@issues.html.navigation("milestones", false, repository)
|
||||||
<br><br>
|
<br><br>
|
||||||
}
|
}
|
||||||
<hr style="margin-top: 12px; margin-bottom: 18px;" class="fill-width"/>
|
<hr style="margin-top: 12px; margin-bottom: 18px;" class="fill-width"/>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
|
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
|
||||||
@html.menu("issues", repository){
|
@html.menu("issues", repository){
|
||||||
@issues.html.tab("milestones", hasWritePermission, repository)
|
@issues.html.navigation("milestones", hasWritePermission, repository)
|
||||||
<br>
|
<br>
|
||||||
<table class="table table-bordered table-hover table-issues">
|
<table class="table table-bordered table-hover table-issues">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -14,11 +14,11 @@
|
|||||||
<span class="small">
|
<span class="small">
|
||||||
<a class="button-link@if(state == "open"){ selected}" href="?state=open">
|
<a class="button-link@if(state == "open"){ selected}" href="?state=open">
|
||||||
<img src="@assets/common/images/milestone@(if(state == "open"){"-active"}).png"/>
|
<img src="@assets/common/images/milestone@(if(state == "open"){"-active"}).png"/>
|
||||||
@milestones.filter(_._1.closedDate.isEmpty).size Open
|
@milestones.count(_._1.closedDate.isEmpty) Open
|
||||||
</a>
|
</a>
|
||||||
<a class="button-link@if(state == "closed"){ selected}" href="?state=closed">
|
<a class="button-link@if(state == "closed"){ selected}" href="?state=closed">
|
||||||
<img src="@assets/common/images/milestone@(if(state == "closed"){"-active"}).png"/>
|
<img src="@assets/common/images/milestone@(if(state == "closed"){"-active"}).png"/>
|
||||||
@milestones.filter(_._1.closedDate.isDefined).size Closed
|
@milestones.count(_._1.closedDate.isDefined) Closed
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</th>
|
</th>
|
||||||
|
|||||||
58
src/main/twirl/issues/navigation.scala.html
Normal file
58
src/main/twirl/issues/navigation.scala.html
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
@(active: String,
|
||||||
|
newButton: Boolean,
|
||||||
|
repository: service.RepositoryService.RepositoryInfo,
|
||||||
|
condition: Option[service.IssuesService.IssueSearchCondition] = None)(implicit context: app.Context)
|
||||||
|
@import context._
|
||||||
|
@import view.helpers._
|
||||||
|
<ul class="nav nav-pills-group pull-left fill-width">
|
||||||
|
<li class="@if(active == "issues" ){active} first"><a href="@url(repository)/issues">Issues</a></li>
|
||||||
|
<li class="@if(active == "pulls" ){active}"><a href="@url(repository)/pulls">Pull requests</a></li>
|
||||||
|
<li class="@if(active == "labels" ){active}"><a href="@url(repository)/issues/labels">Labels</a></li>
|
||||||
|
<li class="@if(active == "milestones"){active} last"><a href="@url(repository)/issues/milestones">Milestones</a></li>
|
||||||
|
<li class="pull-right">
|
||||||
|
<form method="GET" id="search-filter-form" style="margin-bottom: 0px;">
|
||||||
|
@condition.map { condition =>
|
||||||
|
@if(loginAccount.isDefined){
|
||||||
|
<div class="input-prepend" style="margin-bottom: 0px;">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button type="button" class="btn dropdown-toggle" data-toggle="dropdown" style="height: 34px;">
|
||||||
|
Filter
|
||||||
|
<span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="?q=is:open">Open issues and pull requests</a></li>
|
||||||
|
<li><a href="?q=is:open+is:issue+author:@urlEncode(loginAccount.get.userName)">Your issues</a></li>
|
||||||
|
<li><a href="?q=is:open+is:pr+author:@urlEncode(loginAccount.get.userName)">Your pull requests</a></li>
|
||||||
|
<li><a href="?q=is:open+assignee:@urlEncode(loginAccount.get.userName)">Everything assigned to you</a></li>
|
||||||
|
@*
|
||||||
|
<li><a href="?q=is:open+mentions:@urlEncode(loginAccount.get.userName)">Everything mentioning you</a></li>
|
||||||
|
*@
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="search-filter-box" class="input-xlarge" name="q" style="height: 24px;" value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<input type="text" id="search-filter-box" class="input-xlarge" name="q" style="height: 24px;" value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@if(loginAccount.isDefined){
|
||||||
|
<div class="btn-group">
|
||||||
|
@if(newButton){
|
||||||
|
@if(active == "issues"){
|
||||||
|
<a class="btn btn-success" href="@url(repository)/issues/new" style="height: 24px;">New issue</a>
|
||||||
|
}
|
||||||
|
@if(active == "pulls"){
|
||||||
|
<a class="btn btn-success" href="@url(repository)/compare" style="height: 24px;">New pull request</a>
|
||||||
|
}
|
||||||
|
@if(active == "labels"){
|
||||||
|
<a class="btn btn-success" href="javascript:void(0);" id="new-label-button" style="height: 24px;">New label</a>
|
||||||
|
}
|
||||||
|
@if(active == "milestones"){
|
||||||
|
<a class="btn btn-success" href="@url(repository)/issues/milestones/new" style="height: 24px;">New milestone</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
@(active: String, newButton: Boolean,
|
|
||||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
|
||||||
@import context._
|
|
||||||
@import view.helpers._
|
|
||||||
<ul class="nav nav-pills-group pull-left fill-width">
|
|
||||||
<li class="@if(active == "issues" ){active} first"><a href="@url(repository)/issues">Issues</a></li>
|
|
||||||
<li class="@if(active == "pulls" ){active}"><a href="@url(repository)/pulls">Pull requests</a></li>
|
|
||||||
<li class="@if(active == "labels" ){active}"><a href="@url(repository)/issues/labels">Labels</a></li>
|
|
||||||
<li class="@if(active == "milestones"){active} last"><a href="@url(repository)/issues/milestones">Milestones</a></li>
|
|
||||||
@if(loginAccount.isDefined){
|
|
||||||
<li class="pull-right">
|
|
||||||
<div class="btn-group">
|
|
||||||
@if(newButton){
|
|
||||||
@if(active == "issues"){
|
|
||||||
<a class="btn btn-success" href="@url(repository)/issues/new">New issue</a>
|
|
||||||
}
|
|
||||||
@if(active == "pulls"){
|
|
||||||
<a class="btn btn-success" href="@url(repository)/compare">New pull request</a>
|
|
||||||
}
|
|
||||||
@if(active == "labels"){
|
|
||||||
<a class="btn btn-success" href="javascript:void(0);" id="new-label-button">New label</a>
|
|
||||||
}
|
|
||||||
@if(active == "milestones"){
|
|
||||||
<a class="btn btn-success" href="@url(repository)/issues/milestones/new">New milestone</a>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>@title</title>
|
<title>@title</title>
|
||||||
<link rel="icon" href="@assets/common/images/favicon.png" type="image/vnd.microsoft.icon" />
|
<link rel="icon" href="@assets/common/images/gitbucket.png" type="image/vnd.microsoft.icon" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<!-- Le styles -->
|
<!-- Le styles -->
|
||||||
<link href="@assets/vendors/bootstrap/css/bootstrap.css" rel="stylesheet">
|
<link href="@assets/vendors/bootstrap/css/bootstrap.css" rel="stylesheet">
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
<link href="@assets/vendors/datepicker/css/datepicker.css" rel="stylesheet">
|
<link href="@assets/vendors/datepicker/css/datepicker.css" rel="stylesheet">
|
||||||
<link href="@assets/vendors/colorpicker/css/bootstrap-colorpicker.css" rel="stylesheet">
|
<link href="@assets/vendors/colorpicker/css/bootstrap-colorpicker.css" rel="stylesheet">
|
||||||
<link href="@assets/vendors/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/>
|
<link href="@assets/vendors/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/>
|
||||||
|
<link href="@assets/vendors/facebox/facebox.css" rel="stylesheet"/>
|
||||||
<link href="@assets/common/css/gitbucket.css" rel="stylesheet">
|
<link href="@assets/common/css/gitbucket.css" rel="stylesheet">
|
||||||
<script src="@assets/vendors/jquery/jquery-1.9.1.js"></script>
|
<script src="@assets/vendors/jquery/jquery-1.9.1.js"></script>
|
||||||
<script src="@assets/vendors/dropzone/dropzone.js"></script>
|
<script src="@assets/vendors/dropzone/dropzone.js"></script>
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
<script src="@assets/vendors/google-code-prettify/prettify.js"></script>
|
<script src="@assets/vendors/google-code-prettify/prettify.js"></script>
|
||||||
<script src="@assets/vendors/zclip/ZeroClipboard.min.js"></script>
|
<script src="@assets/vendors/zclip/ZeroClipboard.min.js"></script>
|
||||||
<script src="@assets/vendors/elastic/jquery.elastic.source.js"></script>
|
<script src="@assets/vendors/elastic/jquery.elastic.source.js"></script>
|
||||||
|
<script src="@assets/vendors/facebox/facebox.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<form id="search" action="@path/search" method="POST">
|
<form id="search" action="@path/search" method="POST">
|
||||||
@@ -41,7 +43,7 @@
|
|||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
</button>
|
</button>
|
||||||
<a class="brand" href="@path/">
|
<a class="brand" href="@path/">
|
||||||
<img src="@assets/common/images/gitbucket.png"/>GitBucket
|
<img src="@assets/common/images/gitbucket.png" style="width: 24px; height: 24px;"/>GitBucket
|
||||||
@defining(servlet.AutoUpdate.getCurrentVersion){ version =>
|
@defining(servlet.AutoUpdate.getCurrentVersion){ version =>
|
||||||
<span class="header-version">@version.majorVersion.@version.minorVersion</span>
|
<span class="header-version">@version.majorVersion.@version.minorVersion</span>
|
||||||
}
|
}
|
||||||
@@ -60,21 +62,11 @@
|
|||||||
<li><a href="@path/groups/new">New group</a></li>
|
<li><a href="@path/groups/new">New group</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></a>
|
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></a>
|
||||||
@plugin.PluginSystem.globalMenus.map { menu =>
|
|
||||||
@if(menu.condition(context)){
|
|
||||||
<a href="@menu.url" class="menu" data-toggle="tooltip" data-placement="bottom" title="@menu.label">@if(menu.icon.nonEmpty){<img src="@menu.icon" class="plugin-global-menu"/>} else {@menu.label}</a>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@if(loginAccount.get.isAdmin){
|
@if(loginAccount.get.isAdmin){
|
||||||
<a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench"></i></a>
|
<a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench"></i></a>
|
||||||
}
|
}
|
||||||
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
|
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
|
||||||
} else {
|
} else {
|
||||||
@plugin.PluginSystem.globalMenus.map { menu =>
|
|
||||||
@if(menu.condition(context)){
|
|
||||||
<a href="@menu.url" class="menu" data-toggle="tooltip" data-placement="bottom" title="@menu.label">@if(menu.icon.nonEmpty){<img src="@menu.icon" class="plugin-global-menu"/>} else {@menu.label}</a>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
<a href="@path/signin?redirect=@urlEncode(currentPath)" class="btn btn-last" id="signin">Sign in</a>
|
<a href="@path/signin?redirect=@urlEncode(currentPath)" class="btn btn-last" id="signin">Sign in</a>
|
||||||
}
|
}
|
||||||
</div><!--/.nav-collapse -->
|
</div><!--/.nav-collapse -->
|
||||||
@@ -88,9 +80,6 @@
|
|||||||
$('#search').submit(function(){
|
$('#search').submit(function(){
|
||||||
return $.trim($(this).find('input[name=query]').val()) != '';
|
return $.trim($(this).find('input[name=query]').val()) != '';
|
||||||
});
|
});
|
||||||
@plugin.PluginSystem.javaScripts.filter(_.filter(context.currentPath)).map { js =>
|
|
||||||
@Html(js.script)
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
repository: service.RepositoryService.RepositoryInfo,
|
repository: service.RepositoryService.RepositoryInfo,
|
||||||
id: Option[String] = None,
|
id: Option[String] = None,
|
||||||
expand: Boolean = false,
|
expand: Boolean = false,
|
||||||
|
isNoGroup: Boolean = true,
|
||||||
info: Option[Any] = None,
|
info: Option[Any] = None,
|
||||||
error: Option[Any] = None)(body: Html)(implicit context: app.Context)
|
error: Option[Any] = None)(body: Html)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@@ -38,7 +39,15 @@
|
|||||||
@if(repository.commitCount > 0){
|
@if(repository.commitCount > 0){
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<div class="input-prepend">
|
<div class="input-prepend">
|
||||||
<a href="@path/@repository.owner/@repository.name/fork" class="btn btn-small" style="margin-bottom: 10px;">Fork</a>
|
@if(loginAccount.isEmpty){
|
||||||
|
<a title="You must be signed in to fork a repository" href="@path/signin" class="btn btn-small" style="margin-bottom: 10px;">Fork</a>
|
||||||
|
} else {
|
||||||
|
@if(isNoGroup) {
|
||||||
|
<a href="@path/@repository.owner/@repository.name/fork" class="btn btn-small" style="margin-bottom: 10px;" data-account="@loginAccount.get.userName">Fork</a>
|
||||||
|
} else {
|
||||||
|
<a href="@path/@repository.owner/@repository.name/fork" class="btn btn-small" rel="facebox" style="margin-bottom: 10px;">Fork</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
<span class="add-on count"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
|
<span class="add-on count"><a href="@url(repository)/network/members">@repository.forkedCount</a></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,11 +74,6 @@
|
|||||||
@sidemenu("/issues", "issues", "Issues", repository.issueCount)
|
@sidemenu("/issues", "issues", "Issues", repository.issueCount)
|
||||||
@sidemenu("/pulls" , "pulls" , "Pull Requests", repository.pullCount)
|
@sidemenu("/pulls" , "pulls" , "Pull Requests", repository.pullCount)
|
||||||
@sidemenu("/wiki" , "wiki" , "Wiki")
|
@sidemenu("/wiki" , "wiki" , "Wiki")
|
||||||
@plugin.PluginSystem.repositoryMenus.map { menu =>
|
|
||||||
@if(menu.condition(context)){
|
|
||||||
@sidemenuPlugin(menu.url, menu.label, menu.label, menu.icon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){
|
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){
|
||||||
@sidemenu("/settings", "settings", "Settings")
|
@sidemenu("/settings", "settings", "Settings")
|
||||||
}
|
}
|
||||||
@@ -175,6 +179,33 @@ $(function(){
|
|||||||
$(target).children('img.menu-icon' ).css('display', 'inline');
|
$(target).children('img.menu-icon' ).css('display', 'inline');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('a[rel*=facebox]').facebox();
|
||||||
|
|
||||||
|
$(document).on("click", ".js-fork-owner-select-target", function() {
|
||||||
|
if (!$(this).hasClass("disabled")) {
|
||||||
|
var account = $(this).text().replace("@@", "");
|
||||||
|
$("#account").val(account);
|
||||||
|
$("#fork").submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
@if(loginAccount.isDefined){
|
||||||
|
$(document).on("click", "a[data-account]", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var form = $('<form/>', {
|
||||||
|
action: $(this).attr('href'),
|
||||||
|
method: "post"
|
||||||
|
});
|
||||||
|
var account = $('<input/>', {
|
||||||
|
type: "hidden",
|
||||||
|
name: "account",
|
||||||
|
value: $(this).data('account')
|
||||||
|
});
|
||||||
|
form.append(account);
|
||||||
|
form.submit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@if(settings.ssh && loginAccount.isDefined){
|
@if(settings.ssh && loginAccount.isDefined){
|
||||||
$('#repository-url-http').click(function(){
|
$('#repository-url-http').click(function(){
|
||||||
$('#repository-url-proto').text('HTTP');
|
$('#repository-url-proto').text('HTTP');
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@(commits: Seq[Seq[util.JGitUtil.CommitInfo]],
|
@(commits: Seq[Seq[util.JGitUtil.CommitInfo]],
|
||||||
|
comments: Option[List[model.Comment]] = None,
|
||||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@@ -15,6 +16,14 @@
|
|||||||
@user(commit.authorName, commit.authorEmailAddress, "username")
|
@user(commit.authorName, commit.authorEmailAddress, "username")
|
||||||
</td>
|
</td>
|
||||||
<td>@commit.shortMessage</td>
|
<td>@commit.shortMessage</td>
|
||||||
|
<td style="width: 10%; text-align: right">
|
||||||
|
<span class="badge" style="display: inline">@if(comments.isDefined){
|
||||||
|
@comments.get.flatMap @{
|
||||||
|
case comment: model.CommitComment => Some(comment)
|
||||||
|
case other => None
|
||||||
|
}.count(t => t.commitId == commit.id && !t.pullRequest)
|
||||||
|
}</span>
|
||||||
|
</td>
|
||||||
<td style="width: 10%; text-align: right;">
|
<td style="width: 10%; text-align: right;">
|
||||||
<a href="@url(repository)/commit/@commit.id" class="monospace">@commit.id.substring(0, 7)</a>
|
<a href="@url(repository)/commit/@commit.id" class="monospace">@commit.id.substring(0, 7)</a>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@(commits: Seq[Seq[util.JGitUtil.CommitInfo]],
|
@(commits: Seq[Seq[util.JGitUtil.CommitInfo]],
|
||||||
diffs: Seq[util.JGitUtil.DiffInfo],
|
diffs: Seq[util.JGitUtil.DiffInfo],
|
||||||
members: List[(String, String)],
|
members: List[(String, String)],
|
||||||
|
comments: List[model.Comment],
|
||||||
originId: String,
|
originId: String,
|
||||||
forkedId: String,
|
forkedId: String,
|
||||||
sourceId: String,
|
sourceId: String,
|
||||||
@@ -45,7 +46,7 @@
|
|||||||
</div>
|
</div>
|
||||||
@if(commits.nonEmpty && hasWritePermission){
|
@if(commits.nonEmpty && hasWritePermission){
|
||||||
<div style="margin-bottom: 10px;" id="create-pull-request">
|
<div style="margin-bottom: 10px;" id="create-pull-request">
|
||||||
<a href="#" class="btn" id="show-form">Click to create a pull request for this comparison</a>
|
<a href="#" class="btn btn-success" id="show-form">Create pull request</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="pull-request-form" class="box" style="display: none;">
|
<div id="pull-request-form" class="box" style="display: none;">
|
||||||
<div class="box-content">
|
<div class="box-content">
|
||||||
@@ -81,8 +82,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
} else {
|
} else {
|
||||||
@pulls.html.commits(commits, repository)
|
@pulls.html.commits(commits, Some(comments), repository)
|
||||||
@helper.html.diff(diffs, repository, Some(commitId), Some(sourceId), true)
|
@helper.html.diff(diffs, repository, Some(commitId), Some(sourceId), true, None, hasWritePermission, false)
|
||||||
|
<p>Showing you all comments on commits in this comparison.</p>
|
||||||
|
@issues.html.commentlist(None, comments, hasWritePermission, repository, None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@(issue: model.Issue,
|
@(issue: model.Issue,
|
||||||
pullreq: model.PullRequest,
|
pullreq: model.PullRequest,
|
||||||
comments: List[model.IssueComment],
|
comments: List[model.Comment],
|
||||||
issueLabels: List[model.Label],
|
issueLabels: List[model.Label],
|
||||||
collaborators: List[String],
|
collaborators: List[String],
|
||||||
milestones: List[(model.Milestone, Int, Int)],
|
milestones: List[(model.Milestone, Int, Int)],
|
||||||
@@ -8,11 +8,17 @@
|
|||||||
hasWritePermission: Boolean,
|
hasWritePermission: Boolean,
|
||||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
|
@import model.IssueComment
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
<div class="row-fluid">
|
<div class="row-fluid">
|
||||||
<div class="span10">
|
<div class="span10">
|
||||||
@issues.html.commentlist(issue, comments, hasWritePermission, repository, Some(pullreq))
|
<div id="comment-list">
|
||||||
@defining(comments.exists(_.action == "merge")){ merged =>
|
@issues.html.commentlist(Some(issue), comments, hasWritePermission, repository, Some(pullreq))
|
||||||
|
</div>
|
||||||
|
@defining(comments.flatMap {
|
||||||
|
case comment: IssueComment => Some(comment)
|
||||||
|
case other => None
|
||||||
|
}.exists(_.action == "merge")){ merged =>
|
||||||
@if(hasWritePermission && !issue.closed){
|
@if(hasWritePermission && !issue.closed){
|
||||||
<div class="box issue-comment-box" style="background-color: #d8f5cd;">
|
<div class="box issue-comment-box" style="background-color: #d8f5cd;">
|
||||||
<div class="box-content"class="issue-content" style="border: 1px solid #95c97e; padding: 10px;">
|
<div class="box-content"class="issue-content" style="border: 1px solid #95c97e; padding: 10px;">
|
||||||
@@ -50,22 +56,9 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@issues.html.commentform(issue, !merged, hasWritePermission, repository)
|
@issues.html.commentform(issue, !merged, hasWritePermission, repository)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="span2">
|
<div class="span2">
|
||||||
@if(issue.closed) {
|
|
||||||
@if(merged){
|
|
||||||
<span class="label label-info issue-status">Merged</span>
|
|
||||||
} else {
|
|
||||||
<span class="label label-important issue-status">Closed</span>
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
<span class="label label-success issue-status">Open</span>
|
|
||||||
}
|
|
||||||
<div class="small" style="text-align: center;">
|
|
||||||
<span class="strong">@comments.size</span> @plural(comments.size, "comment")
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<hr/>
|
|
||||||
@issues.html.issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
|
@issues.html.issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,4 +6,4 @@
|
|||||||
<h4 style="color: #468847;">Able to merge</h4>
|
<h4 style="color: #468847;">Able to merge</h4>
|
||||||
<p>These branches can be automatically merged.</p>
|
<p>These branches can be automatically merged.</p>
|
||||||
}
|
}
|
||||||
<input type="submit" class="btn btn-success btn-block" value="Send pull request"/>
|
<input type="submit" class="btn btn-success btn-block" value="Create pull request"/>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
@helper.html.copy("repository-url-copy", requestRepositoryUrl){
|
@helper.html.copy("repository-url-copy", requestRepositoryUrl){
|
||||||
<input type="text" value="@requestRepositoryUrl" id="repository-url" readonly>
|
<input type="text" style="width: 500px;" value="@requestRepositoryUrl" id="repository-url" readonly>
|
||||||
}
|
}
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<p>
|
<p>
|
||||||
<span class="strong">Step 3:</span> Merge the changes and update the server
|
<span class="strong">Step 3:</span> Merge the changes and update the server
|
||||||
</p>
|
</p>
|
||||||
@defining(s"git checkout master\ngit merge ${pullreq.requestUserName}-${pullreq.requestBranch}\ngit push origin ${pullreq.branch}"){ command =>
|
@defining(s"git checkout ${pullreq.branch}\ngit merge ${pullreq.requestUserName}-${pullreq.requestBranch}\ngit push origin ${pullreq.branch}"){ command =>
|
||||||
@helper.html.copy("merge-command-copy-3", command){
|
@helper.html.copy("merge-command-copy-3", command){
|
||||||
<pre style="width: 500px; float: left;">@command</pre>
|
<pre style="width: 500px; float: left;">@command</pre>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@(issue: model.Issue,
|
@(issue: model.Issue,
|
||||||
pullreq: model.PullRequest,
|
pullreq: model.PullRequest,
|
||||||
comments: List[model.IssueComment],
|
comments: List[model.Comment],
|
||||||
issueLabels: List[model.Label],
|
issueLabels: List[model.Label],
|
||||||
collaborators: List[String],
|
collaborators: List[String],
|
||||||
milestones: List[(model.Milestone, Int, Int)],
|
milestones: List[(model.Milestone, Int, Int)],
|
||||||
@@ -14,26 +14,58 @@
|
|||||||
@html.main(s"${issue.title} - Pull Request #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
|
@html.main(s"${issue.title} - Pull Request #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
|
||||||
@html.menu("pulls", repository){
|
@html.menu("pulls", repository){
|
||||||
@defining(dayByDayCommits.flatten){ commits =>
|
@defining(dayByDayCommits.flatten){ commits =>
|
||||||
<div class="pullreq-info">
|
<div>
|
||||||
|
<div class="show-title pull-right">
|
||||||
|
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
|
||||||
|
<a class="btn btn-small" href="#" id="edit">Edit</a>
|
||||||
|
}
|
||||||
|
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New issue</a>
|
||||||
|
</div>
|
||||||
|
<div class="edit-title pull-right" style="display: none;">
|
||||||
|
<a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>
|
||||||
|
</div>
|
||||||
|
<h1>
|
||||||
|
<span class="show-title">
|
||||||
|
<span id="show-title">@issue.title</span>
|
||||||
|
<span class="muted">#@issue.issueId</span>
|
||||||
|
</span>
|
||||||
|
<span class="edit-title" style="display: none;">
|
||||||
|
<span id="error-edit-title" class="error"></span>
|
||||||
|
<input type="text" style="width: 700px;" id="edit-title" value="@issue.title"/>
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
@if(issue.closed) {
|
@if(issue.closed) {
|
||||||
@comments.find(_.action == "merge").map{ comment =>
|
@comments.flatMap @{
|
||||||
<span class="label label-info">Merged</span>
|
case comment: model.IssueComment => Some(comment)
|
||||||
|
case _ => None
|
||||||
|
}.find(_.action == "merge").map{ comment =>
|
||||||
|
<span class="label label-info issue-status">Merged</span>
|
||||||
|
<span class="muted">
|
||||||
@user(comment.commentedUserName, styleClass="username strong") merged @commits.size @plural(commits.size, "commit")
|
@user(comment.commentedUserName, styleClass="username strong") merged @commits.size @plural(commits.size, "commit")
|
||||||
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
|
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
|
||||||
@helper.html.datetimeago(comment.registeredDate)
|
@helper.html.datetimeago(comment.registeredDate)
|
||||||
|
</span>
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
<span class="label label-important">Closed</span>
|
<span class="label label-important issue-status">Closed</span>
|
||||||
|
<span class="muted">
|
||||||
@user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit")
|
@user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit")
|
||||||
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
|
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
<span class="label label-success">Open</span>
|
<span class="label label-success issue-status">Open</span>
|
||||||
|
<span class="muted">
|
||||||
@user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit")
|
@user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit")
|
||||||
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
|
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
</div>
|
<br/><br/>
|
||||||
<ul class="nav nav-tabs fill-width pull-left" id="pullreq-tab">
|
<ul class="nav nav-tabs fill-width pull-left" id="pullreq-tab">
|
||||||
<li class="active"><a href="#conversation">Conversation <span class="badge">@comments.size</span></a></li>
|
<li class="active"><a href="#conversation">Conversation <span class="badge">@comments.flatMap @{
|
||||||
|
case comment: model.IssueComment => Some(comment)
|
||||||
|
case _: model.CommitComment => None
|
||||||
|
}.size</span></a></li>
|
||||||
<li><a href="#commits">Commits <span class="badge">@commits.size</span></a></li>
|
<li><a href="#commits">Commits <span class="badge">@commits.size</span></a></li>
|
||||||
<li><a href="#files">Files Changed <span class="badge">@diffs.size</span></a></li>
|
<li><a href="#files">Files Changed <span class="badge">@diffs.size</span></a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -42,18 +74,51 @@
|
|||||||
@pulls.html.conversation(issue, pullreq, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
|
@pulls.html.conversation(issue, pullreq, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="commits">
|
<div class="tab-pane" id="commits">
|
||||||
@pulls.html.commits(dayByDayCommits, repository)
|
@pulls.html.commits(dayByDayCommits, Some(comments), repository)
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="files">
|
<div class="tab-pane" id="files">
|
||||||
@helper.html.diff(diffs, repository, Some(commits.head.id), Some(commits.last.id), true)
|
@helper.html.diff(diffs, repository, Some(commits.head.id), Some(commits.last.id), true, Some(pullreq.issueId), hasWritePermission, true)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
<script>
|
<script>
|
||||||
|
$(function(){
|
||||||
$('#pullreq-tab a').click(function (e) {
|
$('#pullreq-tab a').click(function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
$(this).tab('show');
|
$(this).tab('show');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#edit').click(function(){
|
||||||
|
$('.edit-title').show();
|
||||||
|
$('.show-title').hide();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#update').click(function(){
|
||||||
|
$(this).attr('disabled', 'disabled');
|
||||||
|
$.ajax({
|
||||||
|
url: '@url(repository)/issues/edit_title/@issue.issueId',
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
title : $('#edit-title').val()
|
||||||
|
}
|
||||||
|
}).done(function(data){
|
||||||
|
$('#show-title').empty().text(data.title);
|
||||||
|
$('#cancel').click();
|
||||||
|
$(this).removeAttr('disabled');
|
||||||
|
}).fail(function(req){
|
||||||
|
$(this).removeAttr('disabled');
|
||||||
|
$('#error-edit-title').text($.parseJSON(req.responseText).title);
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#cancel').click(function(){
|
||||||
|
$('.edit-title').hide();
|
||||||
|
$('.show-title').show();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
71
src/main/twirl/repo/commentform.scala.html
Normal file
71
src/main/twirl/repo/commentform.scala.html
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
@(commitId: String,
|
||||||
|
fileName: Option[String] = None,
|
||||||
|
oldLineNumber: Option[Int] = None,
|
||||||
|
newLineNumber: Option[Int] = None,
|
||||||
|
issueId: Option[Int] = None,
|
||||||
|
hasWritePermission: Boolean,
|
||||||
|
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||||
|
@import context._
|
||||||
|
@import view.helpers._
|
||||||
|
@if(loginAccount.isDefined){
|
||||||
|
@if(!fileName.isDefined){<hr/><br/>}
|
||||||
|
<form method="POST" validate="true" style="max-width: 874px;">
|
||||||
|
@if(!fileName.isDefined){
|
||||||
|
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
|
||||||
|
}
|
||||||
|
<div class="box issue-comment-box">
|
||||||
|
<div class="box-content">
|
||||||
|
@helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 635px; height: 100px; max-height: 150px;", elastic = true)
|
||||||
|
</div>
|
||||||
|
@if(fileName.isDefined){
|
||||||
|
<div class="pull-right" style="margin-top: 10px;">
|
||||||
|
<input type="button" class="btn btn-default" value="Cancel"/>
|
||||||
|
<input type="submit" class="btn btn-success btn-inline-comment" formaction="@url(repository)/commit/@commitId/comment/new" value="Comment"/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if(!fileName.isDefined){
|
||||||
|
<div class="pull-right">
|
||||||
|
<input type="submit" class="btn btn-success" formaction="@url(repository)/commit/@commitId/comment/new" value="Comment on this commit"/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@issueId.map { issueId => <input type="hidden" name="issueId" value="@issueId"> }
|
||||||
|
@fileName.map { fileName => <input type="hidden" name="fileName" value="@fileName"> }
|
||||||
|
@oldLineNumber.map { oldLineNumber => <input type="hidden" name="oldLineNumber" value="@oldLineNumber"> }
|
||||||
|
@newLineNumber.map { newLineNumber => <input type="hidden" name="newLineNumber" value="@newLineNumber"> }
|
||||||
|
</form>
|
||||||
|
<script>
|
||||||
|
$('.btn-inline-comment').click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$form = $(e.target).attr('disabled', 'disabled').closest('form');
|
||||||
|
var param = {};
|
||||||
|
$($form.serializeArray()).each(function(i, v) {
|
||||||
|
param[v.name] = v.value;
|
||||||
|
});
|
||||||
|
$.ajax({
|
||||||
|
url: '@url(repository)/commit/@commitId/comment/_data/new',
|
||||||
|
type: 'POST',
|
||||||
|
data: param
|
||||||
|
}).done(function(data) {
|
||||||
|
var tmp;
|
||||||
|
if (window.viewType == 0) {
|
||||||
|
tmp = '@(oldLineNumber, newLineNumber) match {
|
||||||
|
case (Some(_), None) => {<td colspan="2" class="comment-box-container"></td><td colspan="2"></td>}
|
||||||
|
case (None, Some(_)) => {<td colspan="2"></td><td colspan="2" class="comment-box-container"></td>}
|
||||||
|
case _ => {<td colspan="3" class="comment-box-container"></td>}
|
||||||
|
}'
|
||||||
|
} else {
|
||||||
|
tmp = '<td colspan="3" class="comment-box-container"></td>'
|
||||||
|
}
|
||||||
|
$form.closest('tr').removeClass('inline-comment-form').html(tmp).find('.comment-box-container').html(data);
|
||||||
|
$('#comment-list').append(data);
|
||||||
|
if (typeof $('#show-notes')[0] !== 'undefined' && !$('#show-notes')[0].checked) {
|
||||||
|
$('#comment-list').children('.inline-comment').hide();
|
||||||
|
}
|
||||||
|
}).fail(function(req) {
|
||||||
|
$('.btn-inline-comment').removeAttr('disabled');
|
||||||
|
$('#error-content', $form).html($.parseJSON(req.responseText).content);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
}
|
||||||
@@ -2,9 +2,11 @@
|
|||||||
commit: util.JGitUtil.CommitInfo,
|
commit: util.JGitUtil.CommitInfo,
|
||||||
branches: List[String],
|
branches: List[String],
|
||||||
tags: List[String],
|
tags: List[String],
|
||||||
|
comments: List[model.Comment],
|
||||||
repository: service.RepositoryService.RepositoryInfo,
|
repository: service.RepositoryService.RepositoryInfo,
|
||||||
diffs: Seq[util.JGitUtil.DiffInfo],
|
diffs: Seq[util.JGitUtil.DiffInfo],
|
||||||
oldCommitId: Option[String])(implicit context: app.Context)
|
oldCommitId: Option[String],
|
||||||
|
hasWritePermission: Boolean)(implicit context: app.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@import util.Implicits._
|
@import util.Implicits._
|
||||||
@@ -81,7 +83,14 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@helper.html.diff(diffs, repository, Some(commit.id), oldCommitId, true)
|
@helper.html.diff(diffs, repository, Some(commit.id), oldCommitId, true, None, hasWritePermission, true)
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox" id="show-notes"> Show line notes below
|
||||||
|
</label>
|
||||||
|
<div id="comment-list">
|
||||||
|
@issues.html.commentlist(None, comments, hasWritePermission, repository, None)
|
||||||
|
</div>
|
||||||
|
@commentform(commitId = commitId, hasWritePermission = hasWritePermission, repository = repository)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
<script>
|
<script>
|
||||||
@@ -122,6 +131,15 @@ $(function(){
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$('#show-notes').change(function() {
|
||||||
|
if (this.checked) {
|
||||||
|
$('.inline-comment').show();
|
||||||
|
} else {
|
||||||
|
$('.inline-comment').hide();
|
||||||
|
$('.diff .inline-comment').show();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
|
|||||||
45
src/main/twirl/repo/editcomment.scala.html
Normal file
45
src/main/twirl/repo/editcomment.scala.html
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
@(content: String, commentId: Int, owner: String, repository: String)(implicit context: app.Context)
|
||||||
|
@import context._
|
||||||
|
<span class="error-edit-content-@commentId error"></span>
|
||||||
|
@helper.html.attached(owner, repository){
|
||||||
|
<textarea style="width: 635px; height: 100px;" id="edit-content-@commentId">@content</textarea>
|
||||||
|
}
|
||||||
|
<div>
|
||||||
|
<input type="button" class="cancel-comment-@commentId btn btn-small btn-danger" value="Cancel"/>
|
||||||
|
<input type="button" class="update-comment-@commentId btn btn-small pull-right" value="Update comment"/>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
$(function(){
|
||||||
|
var curriedCallback = function($box) {
|
||||||
|
return function(data){
|
||||||
|
$('.update-comment-@commentId, .cancel-comment-@commentId', $box).removeAttr('disabled');
|
||||||
|
$('.commit-commentContent-@commentId').empty().html(data.content);
|
||||||
|
prettyPrint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).on('click', '.update-comment-@commentId', function(){
|
||||||
|
$box = $(this).closest('.box');
|
||||||
|
$('.update-comment-@commentId, .cancel-comment-@commentId', $box).attr('disabled', 'disabled');
|
||||||
|
$.ajax({
|
||||||
|
url: '@path/@owner/@repository/commit_comments/edit/@commentId',
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
content : $('#edit-content-@commentId', $box).val()
|
||||||
|
}
|
||||||
|
}).done(
|
||||||
|
curriedCallback($box)
|
||||||
|
).fail(function(req) {
|
||||||
|
$('.update-comment-@commentId, .cancel-comment-@commentId', $box).removeAttr('disabled');
|
||||||
|
$('.error-edit-content-@commentId', $box).text($.parseJSON(req.responseText).content);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.cancel-comment-@commentId', function(){
|
||||||
|
$box = $(this).closest('.box');
|
||||||
|
$('.update-comment-@commentId, .cancel-comment-@commentId', $box).attr('disabled', 'disabled');
|
||||||
|
$.get('@path/@owner/@repository/commit_comments/_data/@commentId', curriedCallback($box));
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -127,7 +127,8 @@ $(function(){
|
|||||||
$.post('@url(repository)/_preview', {
|
$.post('@url(repository)/_preview', {
|
||||||
content : editor.getValue(),
|
content : editor.getValue(),
|
||||||
enableWikiLink : false,
|
enableWikiLink : false,
|
||||||
enableRefsLink : false
|
enableRefsLink : false,
|
||||||
|
enableTaskList : false
|
||||||
}, function(data){
|
}, function(data){
|
||||||
$('#preview').empty().append(
|
$('#preview').empty().append(
|
||||||
$('<div class="markdown-body" style="padding-left: 16px; padding-right: 16px;">').html(data));
|
$('<div class="markdown-body" style="padding-left: 16px; padding-right: 16px;">').html(data));
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@(branch: String,
|
@(branch: String,
|
||||||
repository: service.RepositoryService.RepositoryInfo,
|
repository: service.RepositoryService.RepositoryInfo,
|
||||||
pathList: List[String],
|
pathList: List[String],
|
||||||
|
groupNames: List[String],
|
||||||
latestCommit: util.JGitUtil.CommitInfo,
|
latestCommit: util.JGitUtil.CommitInfo,
|
||||||
files: List[util.JGitUtil.FileInfo],
|
files: List[util.JGitUtil.FileInfo],
|
||||||
readme: Option[(List[String], String)],
|
readme: Option[(List[String], String)],
|
||||||
@@ -10,7 +11,7 @@
|
|||||||
@import context._
|
@import context._
|
||||||
@import view.helpers._
|
@import view.helpers._
|
||||||
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
|
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
|
||||||
@html.menu("code", repository, Some(branch), pathList.isEmpty, info, error){
|
@html.menu("code", repository, Some(branch), pathList.isEmpty, groupNames.isEmpty, info, error){
|
||||||
<div class="head">
|
<div class="head">
|
||||||
@helper.html.branchcontrol(
|
@helper.html.branchcontrol(
|
||||||
branch,
|
branch,
|
||||||
|
|||||||
@@ -7,21 +7,36 @@
|
|||||||
@if(!hasWritePermission){
|
@if(!hasWritePermission){
|
||||||
<h3>This is an empty repository</h3>
|
<h3>This is an empty repository</h3>
|
||||||
} else {
|
} else {
|
||||||
|
<h3><strong>Quick setup</strong> — if you've done this kind of thing before</h3>
|
||||||
|
<div class="empty-repo-options">
|
||||||
|
via <a href="@repository.httpUrl" class="git-protocol-selector">HTTP</a>
|
||||||
|
@if(settings.ssh && loginAccount.isDefined){
|
||||||
|
or
|
||||||
|
<a href="@repository.sshUrl(settings.sshPort.getOrElse(service.SystemSettingsService.DefaultSshPort), loginAccount.get.userName)" class="git-protocol-selector">SSH</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<h3 style="margin-top: 30px;">Create a new repository on the command line</h3>
|
<h3 style="margin-top: 30px;">Create a new repository on the command line</h3>
|
||||||
<pre>
|
@pre {
|
||||||
touch README.md
|
touch README.md
|
||||||
git init
|
git init
|
||||||
git add README.md
|
git add README.md
|
||||||
git commit -m "first commit"
|
git commit -m "first commit"
|
||||||
git remote add origin @repository.httpUrl
|
git remote add origin <span class="live-clone-url">@repository.httpUrl</span>
|
||||||
git push -u origin master
|
git push -u origin master
|
||||||
</pre>
|
}
|
||||||
|
|
||||||
<h3 style="margin-top: 30px;">Push an existing repository from the command line</h3>
|
<h3 style="margin-top: 30px;">Push an existing repository from the command line</h3>
|
||||||
<pre>
|
@pre {
|
||||||
git remote add origin @repository.httpUrl
|
git remote add origin <span class="live-clone-url">@repository.httpUrl</span>
|
||||||
git push -u origin master
|
git push -u origin master
|
||||||
</pre>
|
}
|
||||||
|
<script>
|
||||||
|
$(function(){
|
||||||
|
$('.git-protocol-selector').click(function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
$('.live-clone-url').text($(e.target).attr('href'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
@files.drop((page - 1) * CodeLimit).take(CodeLimit).map { file =>
|
@files.drop((page - 1) * CodeLimit).take(CodeLimit).map { file =>
|
||||||
<div>
|
<div>
|
||||||
<h5><a href="@url(repository)/blob/@repository.repository.defaultBranch/@file.path">@file.path</a></h5>
|
<h5><a href="@url(repository)/blob/@repository.repository.defaultBranch/@file.path">@file.path</a></h5>
|
||||||
<div class="small muted">Last commited @helper.html.datetimeago(file.lastModified)</div>
|
<div class="small muted">Last committed @helper.html.datetimeago(file.lastModified)</div>
|
||||||
<pre class="prettyprint linenums:@file.highlightLineNumber" style="padding-left: 25px;">@Html(file.highlightText)</pre>
|
<pre class="prettyprint linenums:@file.highlightLineNumber" style="padding-left: 25px;">@Html(file.highlightText)</pre>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
@issues.drop((page - 1) * IssueLimit).take(IssueLimit).map { issue =>
|
@issues.drop((page - 1) * IssueLimit).take(IssueLimit).map { issue =>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<div class="pull-right muted">#@issue.issueId</div>
|
<div class="pull-right muted">#@issue.issueId</div>
|
||||||
<h4 style="margin-top: 0px;"><a href="@url(repository)/issues/@issue.issueId">@issue.title</a></h4>
|
<h4 style="margin-top: 0px;"><a href="@url(repository)/@if(issue.isPullRequest){pull} else {issues}/@issue.issueId">@issue.title</a></h4>
|
||||||
@if(issue.highlightText.nonEmpty){
|
@if(issue.highlightText.nonEmpty){
|
||||||
<pre>@Html(issue.highlightText)</pre>
|
<pre>@Html(issue.highlightText)</pre>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
@import context._
|
@import context._
|
||||||
@main("Sign in"){
|
@main("Sign in"){
|
||||||
<div class="signin-form">
|
<div class="signin-form">
|
||||||
|
@settings.information.map { information =>
|
||||||
|
<div class="alert alert-info" style="background-color: white; color: #555; border-color: #4183c4; font-size: small; line-height: 120%;">
|
||||||
|
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||||
|
@Html(information)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@signinform(settings)
|
@signinform(settings)
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<h1 class="wiki-title"><span class="muted">Compare Revisions</span></h1>
|
<h1 class="wiki-title"><span class="muted">Compare Revisions</span></h1>
|
||||||
</li>
|
</li>
|
||||||
<li class="pull-right">
|
<li class="fill-width pull-right">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
@if(pageName.isDefined){
|
@if(pageName.isDefined){
|
||||||
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)">View Page</a>
|
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)">View Page</a>
|
||||||
@@ -26,7 +26,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@helper.html.diff(diffs, repository, None, None, false)
|
<div class="pull-left">
|
||||||
|
@helper.html.diff(diffs, repository, None, None, false, None, false, false)
|
||||||
|
</div>
|
||||||
@if(hasWritePermission){
|
@if(hasWritePermission){
|
||||||
<div>
|
<div>
|
||||||
@if(pageName.isDefined){
|
@if(pageName.isDefined){
|
||||||
|
|||||||
@@ -10,12 +10,12 @@
|
|||||||
<h1 class="wiki-title"><span class="muted">Editing</span> @if(pageName.isEmpty){New Page} else {@pageName}</h1>
|
<h1 class="wiki-title"><span class="muted">Editing</span> @if(pageName.isEmpty){New Page} else {@pageName}</h1>
|
||||||
</li>
|
</li>
|
||||||
<li class="pull-right">
|
<li class="pull-right">
|
||||||
<div class="btn-group">
|
<div>
|
||||||
@if(page.isDefined){
|
@if(page.isDefined){
|
||||||
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)">View Page</a>
|
|
||||||
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_delete" id="delete">Delete Page</a>
|
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_delete" id="delete">Delete Page</a>
|
||||||
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_history">Page History</a>
|
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_history">Page History</a>
|
||||||
}
|
}
|
||||||
|
<a class="btn btn-small btn-success" href="@url(repository)/wiki/_new">New Page</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -16,21 +16,29 @@
|
|||||||
</h1>
|
</h1>
|
||||||
</li>
|
</li>
|
||||||
<li class="pull-right">
|
<li class="pull-right">
|
||||||
<div class="btn-group">
|
<div>
|
||||||
@if(pageName.isEmpty){
|
@if(pageName.isEmpty){
|
||||||
@if(loginAccount.isDefined){
|
@if(loginAccount.isDefined){
|
||||||
<a class="btn btn-small" href="@url(repository)/wiki/_new">New Page</a>
|
<a class="btn btn-small" href="@url(repository)/wiki/_new">New Page</a>
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)">View Page</a>
|
|
||||||
@if(loginAccount.isDefined){
|
@if(loginAccount.isDefined){
|
||||||
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_edit">Edit Page</a>
|
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_edit">Edit Page</a>
|
||||||
|
<a class="btn btn-small btn-success" href="@url(repository)/wiki/_new">New Page</a>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<table class="table table-bordered fill-width pull-left">
|
<table class="table table-bordered fill-width pull-left">
|
||||||
|
<tr>
|
||||||
|
<th colspan="3">
|
||||||
|
<div class="pull-left" style="padding-top: 4px;">Revisions</div>
|
||||||
|
<div class="pull-right">
|
||||||
|
<input type="button" id="compare" value="Compare Revisions" class="btn btn-mini"/>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
@commits.map { commit =>
|
@commits.map { commit =>
|
||||||
<tr>
|
<tr>
|
||||||
<td width="0%"><input type="checkbox" name="commitId" value="@commit.id"></td>
|
<td width="0%"><input type="checkbox" name="commitId" value="@commit.id"></td>
|
||||||
@@ -41,8 +49,6 @@
|
|||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</table>
|
</table>
|
||||||
<input type="button" id="compare" value="Compare Revisions" class="btn"/>
|
|
||||||
<input type="button" id="top" value="Back to Top" class="btn"/>
|
|
||||||
<script>
|
<script>
|
||||||
$(function(){
|
$(function(){
|
||||||
$('input[name=commitId]').click(function(){
|
$('input[name=commitId]').click(function(){
|
||||||
|
|||||||
@@ -16,13 +16,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="pull-right">
|
<li class="pull-right">
|
||||||
<div class="btn-group">
|
|
||||||
@if(hasWritePermission){
|
@if(hasWritePermission){
|
||||||
<a class="btn btn-small" href="@url(repository)/wiki/_new">New Page</a>
|
<div>
|
||||||
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_edit">Edit Page</a>
|
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_edit">Edit Page</a>
|
||||||
}
|
<a class="btn btn-small btn-success" href="@url(repository)/wiki/_new">New Page</a>
|
||||||
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_history">Page History</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div style="width: 200px;" class="pull-right">
|
<div style="width: 200px;" class="pull-right">
|
||||||
@@ -54,7 +53,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="width: 650px;" class="pull-left">
|
<div style="width: 650px;" class="pull-left">
|
||||||
<div class="markdown-body">
|
<div class="markdown-body">
|
||||||
@markdown(page.content, repository, true, false)
|
@markdown(page.content, repository, true, false, false, false, pages)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,6 +226,10 @@ div.box-header-small {
|
|||||||
text-shadow: 0 1px 0 #fff
|
text-shadow: 0 1px 0 #fff
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inline-comment div.box-header-small {
|
||||||
|
background: #f2f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
div.box-content {
|
div.box-content {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border: 1px solid #d8d8d8;
|
border: 1px solid #d8d8d8;
|
||||||
@@ -860,9 +864,10 @@ div.issue-participants {
|
|||||||
margin-left: 50px;
|
margin-left: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.issue-comment-box {
|
div.issue-comment-box, div.commit-comment-box {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
margin-left: 50px;
|
margin-left: 50px;
|
||||||
|
max-width: 820px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.issue-comment-action {
|
div.issue-comment-action {
|
||||||
@@ -959,13 +964,60 @@ table.diff thead {
|
|||||||
}
|
}
|
||||||
|
|
||||||
table.inlinediff td.insert, table.inlinediff td.equal, table.inlinediff td.delete {
|
table.inlinediff td.insert, table.inlinediff td.equal, table.inlinediff td.delete {
|
||||||
width: 100%;
|
width: 900px;
|
||||||
}
|
}
|
||||||
|
|
||||||
td.insert, td.equal, td.delete, td.empty {
|
td.insert, td.equal, td.delete, td.empty {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.diff .add-comment {
|
||||||
|
position: absolute;
|
||||||
|
background: blue;
|
||||||
|
top: 0;
|
||||||
|
left: -7px;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border: solid 1px blue;
|
||||||
|
border-radius: 3px;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.diff .add-comment:hover {
|
||||||
|
padding: 4px 6px;
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.diff tbody tr.not-diff {
|
||||||
|
font-family: '"Helvetica Neue", Helvetica, Arial, sans-serif';
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.not-diff .box {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.diff tbody tr.not-diff:hover {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.diff tbody tr.not-diff:hover td{
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-diff > .comment-box-container {
|
||||||
|
white-space: initial;
|
||||||
|
line-height: initial;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff .oldline:before, .diff .newline:before {
|
||||||
|
content: attr(line-number);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diff .skipline:before {
|
||||||
|
content: "..."
|
||||||
|
}
|
||||||
|
|
||||||
/****************************************************************************/
|
/****************************************************************************/
|
||||||
/* Repository Settings */
|
/* Repository Settings */
|
||||||
/****************************************************************************/
|
/****************************************************************************/
|
||||||
@@ -1045,6 +1097,8 @@ div.markdown-body pre {
|
|||||||
div.markdown-body code {
|
div.markdown-body code {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
|
background-color: rgba(0,0,0,0.04);
|
||||||
|
rgb(51, 51, 51);
|
||||||
}
|
}
|
||||||
|
|
||||||
div.markdown-body table {
|
div.markdown-body table {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 484 B |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user