Compare commits

...

115 Commits
3.10 ... 3.11

Author SHA1 Message Date
Naoki Takezoe
afa79d01b1 3.11 Release 2016-01-30 09:51:30 +09:00
Naoki Takezoe
860fc8ef4c (refs #1080)Fix commit count presentation when over 10000 2016-01-30 02:01:04 +09:00
Naoki Takezoe
5a2224623b Merge pull request #1077 from ritschwumm/patch-1
adapt how_to_run.md to changes in build.sbt
2016-01-29 01:53:10 +09:00
ritschwumm
dda3458e80 adapt how_to_run.md to changes in build.sbt 2016-01-28 12:10:32 +01:00
Naoki Takezoe
df06f509f5 Merge pull request #1064 from lidice/add-labels-api
Extend API to allow CRUD labels
2016-01-27 01:18:16 +09:00
Naoki Takezoe
0b4d0be1b4 Merge pull request #1074 from ritschwumm/fix-concurrent-access
these fields are accessed from multiple threads
2016-01-27 01:17:02 +09:00
Naoki Takezoe
0bef8d29d5 Merge pull request #1076 from oohira/issue/1055-make-link-anchor-clickable
refs #1055 Make a header link anchor icon clickable
2016-01-27 01:03:10 +09:00
oohira
6265faa14f refs #1055 Make a header link anchor icon clickable 2016-01-26 23:53:07 +09:00
Naoki Takezoe
7593c97ad3 Fix document 2016-01-26 23:06:21 +09:00
Naoki Takezoe
7c8de834bc Merge executable.sbt with build.sbt 2016-01-26 23:01:12 +09:00
Naoki Takezoe
637e9f906b Replace build.scala with build.sbt 2016-01-26 22:08:33 +09:00
Naoki Takezoe
97035be2e5 Remove resources which are required to create executable war 2016-01-26 21:41:09 +09:00
Naoki Takezoe
80fedbd335 Merged branch master into master 2016-01-26 19:33:36 +09:00
Naoki Takezoe
3f6ebc1164 (refs #1065)Fixup 2016-01-26 19:32:46 +09:00
Naoki Takezoe
27b99319d3 Merge branch 'issue/963' of https://github.com/ritschwumm/gitbucket into ritschwumm-issue/963 2016-01-26 17:13:54 +09:00
Naoki Takezoe
a46d1ecf69 Merge pull request #1069 from oohira/limit-image-max-width
Limit maximum width of image
2016-01-26 16:54:17 +09:00
Naoki Takezoe
fb531526bd Merge pull request #1075 from ritschwumm/fix-filename
fix typo in a file name
2016-01-26 16:53:11 +09:00
Herr Ritschwumm
21d6143e40 fix typo in a file name 2016-01-26 01:54:01 +01:00
Herr Ritschwumm
129b424a3a these fields are accessed from multiple threads 2016-01-26 01:32:39 +01:00
Herr Ritschwumm
689334a599 use a constant for the jetty version 2016-01-25 21:54:42 +01:00
Naoki Takezoe
b3ee2222f3 (refs #1058)Bump markedj to 1.0.6 to solve table rendering bug 2016-01-26 01:21:58 +09:00
oohira
da5e4fe5b1 Limit maximum width of image 2016-01-24 23:40:25 +09:00
Naoki Takezoe
f3b7318453 Fix margin of icon and caret of dowpdown menu in the global header 2016-01-24 22:44:39 +09:00
Naoki Takezoe
9874eb7243 Merge pull request #1068 from x-way/api_json_contenttype
Set Content-Type to json for /api/v3/* (fix #1056)
2016-01-24 18:15:09 +09:00
Naoki Takezoe
cc6f4d70da Merge pull request #1066 from oohira/fix-typo
Fix typo in docs
2016-01-24 15:35:00 +09:00
Andreas Jaggi
9b06bfaaf5 Set Content-Type to json for /api/v3/* (fix #1056) 2016-01-23 18:40:49 +01:00
Naoki Takezoe
cdf0d06bcc (refs #1051)Don't render README folder 2016-01-23 21:35:36 +09:00
oohira
501f542982 Fix typo 2016-01-22 23:31:29 +09:00
Naoki Takezoe
1a3504e885 Merge pull request #1050 from x-way/limit_recent_repositories
Limit recent updated repositories list. #1011
2016-01-22 20:32:25 +09:00
Naoki Takezoe
fa617badc1 Merge pull request #1063 from lidice/fix/#1062
(fixes #1062) Remove a wrong link
2016-01-22 20:31:55 +09:00
Herr Ritschwumm
1a1267dc60 issue #963: build executable war with sbt, fetch jetty jars at build time. 2016-01-22 01:42:25 +01:00
lidice
361babd327 Fix labels api
Fixed invalid json format
* List all labels for this repository

Fixed wrong http status
* 200 -> 204
2016-01-22 09:33:41 +09:00
lidice
64cacb18a4 Extend API to allow CRUD labels
Add Labels API
    * List all labels for this repository
    * Get a single label
    * Create a label
    * Update a label
    * Delete a label

    Reject duplicated label name

    Add test case for LabelsService
2016-01-22 07:44:35 +09:00
lidice
833cfc3465 (fixes #1062) Remove a wrong link 2016-01-22 01:50:33 +09:00
Naoki Takezoe
95746de5aa Merge pull request #1061 from ShunsukeTadokoro/master
Fix styles
2016-01-21 12:05:01 +09:00
田所駿佑
4f3c780d05 Fix styles 2016-01-21 03:18:08 +09:00
Naoki Takezoe
33a079e55f Merge pull request #1052 from team-lab/change-issue-action-comment-style
Change action-comment styles that follow github.
2016-01-20 03:04:08 +09:00
Naoki Takezoe
27eb1c36bd Update README.md 2016-01-20 01:09:01 +09:00
Naoki Takezoe
5c12cca7f5 Merge branch 'team-lab-feature/protected-branch' 2016-01-19 15:17:52 +09:00
Naoki Takezoe
206d597d9b Fix testcase 2016-01-19 12:50:54 +09:00
Naoki Takezoe
1b4c621fef Change CommitHook to ReceiveHook 2016-01-19 12:26:20 +09:00
Naoki Takezoe
03d4b9e9c6 Change CommitHook interface 2016-01-18 19:46:30 +09:00
nazoking
82a9d9f7cf Change action-comment styles that follow github. 2016-01-18 19:36:34 +09:00
Andreas Jaggi
3e79dcf7a4 Limit recent updated repositories list. #1011 2016-01-17 13:20:37 +01:00
Naoki Takezoe
c9f3ec12a7 Update README.md 2016-01-17 15:24:11 +09:00
Naoki Takezoe
b781696803 Update README.md 2016-01-17 15:20:54 +09:00
Naoki Takezoe
8dcb8c2ecb Fix testcase 2016-01-17 01:48:56 +09:00
Naoki Takezoe
b599219852 Add commit hook extension point for plugins 2016-01-17 01:38:11 +09:00
Naoki Takezoe
58078989f8 Small fix about code style 2016-01-17 01:08:18 +09:00
Naoki Takezoe
4276c0970b Fix version and view 2016-01-17 00:18:23 +09:00
Naoki Takezoe
4abd4a4bac Merge branch 'feature/protected-branch' of https://github.com/team-lab/gitbucket into team-lab-feature/protected-branch 2016-01-17 00:01:18 +09:00
Naoki Takezoe
2b2ae718f7 Fix preview bug in online editor of repository viewer 2016-01-16 22:37:40 +09:00
Naoki Takezoe
ef201e3319 Update haeding style to bold 2016-01-16 22:37:07 +09:00
Naoki Takezoe
0ab28e6daa (refs #1031)Fix issue link processing in Markdown 2016-01-16 22:14:45 +09:00
Naoki Takezoe
9b3e8bd22b Fix Markdown preview style
However styles in markdown-body affect tab. So there are invalid margin at the top of the tab.
2016-01-16 22:00:29 +09:00
Naoki Takezoe
15e8527e01 (refs #984)Fix special character problem in repository viewer 2016-01-16 14:27:44 +09:00
Naoki Takezoe
f991f3b454 Merge branch 'cubdesign-name-truncate' 2016-01-16 14:18:12 +09:00
Naoki Takezoe
d6a39403e2 (refs #1035)Fixup 2016-01-16 14:18:03 +09:00
Naoki Takezoe
aa065fd030 Merge branch 'name-truncate' of https://github.com/cubdesign/gitbucket into cubdesign-name-truncate 2016-01-16 14:15:42 +09:00
Naoki Takezoe
b1b132dfc3 Merge pull request #1029 from team-lab/add-jrebel
Add jrebel
2016-01-15 01:23:41 +09:00
Naoki Takezoe
90c379b3b9 Update README.md 2016-01-13 14:01:40 +09:00
Naoki Takezoe
b71ff6f179 Update README.md 2016-01-13 13:55:59 +09:00
Takeo Tamura
f63542dbd0 delete white-space 2016-01-07 16:55:39 +09:00
Takeo Tamura
63e605d99c fix long file name text-overflow 2016-01-07 16:54:08 +09:00
Takeo Tamura
9e313bb2b3 fix long file name text-overflow 2016-01-07 16:40:44 +09:00
Naoki Takezoe
a03fc4cf4a Merge pull request #1032 from cubdesign/issues-badge
show side menu badge
2016-01-06 23:09:03 +09:00
Naoki Takezoe
d18f92cfbf Merge pull request #1033 from cubdesign/issues-pagination
fix pagination layout
2016-01-06 23:05:33 +09:00
Takeo Tamura
b96821ca44 fix pagination layout 2016-01-06 22:16:52 +09:00
Takeo Tamura
c61ecdef7a show badge 2016-01-06 22:07:20 +09:00
Naoki Takezoe
b50ca63c2b Merge pull request #1030 from team-lab/fix/image-diff
Fix/image diff
2016-01-05 01:09:15 +09:00
Naoki Takezoe
f6624c0002 Merge pull request #1027 from moccos/add_path_to_title
Add path and branch name to the page title
2016-01-05 01:07:47 +09:00
nazoking
d62fc6e135 fix image-diff style 2016-01-04 20:17:52 +09:00
nazoking
926c4d948f remove debug info 2016-01-04 17:11:53 +09:00
nazoking
5fd87ca764 add jrebel document 2016-01-03 22:46:15 +09:00
nazoking
1042c50cc3 add jrebel plugin 2016-01-03 22:09:52 +09:00
moccos
adff36c44b Use repository.repository.defaultBranch instead of "master" 2016-01-03 20:02:27 +09:00
moccos
e8f6d2b990 Add path and branch name to the page title 2016-01-03 05:00:02 +09:00
Naoki Takezoe
a3b3262ad5 Merge pull request #1026 from team-lab/fix/test-merge-service-more-likely-to-succeed
MergeServiceSpec more likely to succeed
2016-01-03 00:43:45 +09:00
Naoki Takezoe
9efba82e94 Merge pull request #1024 from team-lab/fix/test-specs2-junit
fix test ( add specs2-junit )
2016-01-02 23:41:51 +09:00
nazoking
781d465f8d MergeServiceSpec more likely to succeed 2016-01-02 20:58:44 +09:00
nazoking
766867f341 add test 2016-01-02 20:41:23 +09:00
nazoking
e237a44f56 move MergeStatus to service 2016-01-02 20:23:07 +09:00
nazoking
249430449b use common mapper 2016-01-02 20:23:07 +09:00
nazoking
73d935efe3 fix typo 2016-01-02 20:23:07 +09:00
nazoking
79251ef1d1 updete bootstrap 3 2016-01-02 20:23:07 +09:00
nazoking
1081c0f16c remove blank line from basic-template 2016-01-02 20:23:07 +09:00
nazoking
3302b607f1 use real database 2016-01-02 20:23:07 +09:00
nazoking
f13a3ee580 for bootstrap 3 2016-01-02 20:23:07 +09:00
nazoking
927969ac17 remove println(for debug) 2016-01-02 20:23:07 +09:00
nazoking
2d8aa4f8b5 update button 2016-01-02 20:23:07 +09:00
nazoking
645af4d2c0 add protected-branch feature on pull-request page
* show commit-status if context is require status checks to pass.
  * disable merge if new commit-id has not `commit-status` ok on `Status-checkes`.
  * if some status includes required is not success, merge button is disabled.
  * if any required status is success, and some status not includes required, merge button is active, but button color is white.
  * if any required status is success, merge button is active, and button color is green.
2016-01-02 20:20:24 +09:00
nazoking
34240d16b5 Protected mark(octicon-shield) on Branches page 2016-01-02 20:20:24 +09:00
nazoking
89210985d4 Readonly on online-editor 2016-01-02 20:20:24 +09:00
nazoking
9d6258861a add protected branch list on branch setting page. 2016-01-02 20:20:24 +09:00
nazoking
6661314be5 Add git proection api and form 2016-01-02 20:20:24 +09:00
nazoking
e187d026cc create repository setting branches page and move default branch setting to there. 2016-01-02 20:20:24 +09:00
nazoking
100b34085c fix 'cannot create a JUnit XML printer. Please check that specs2-junit.jar is on the classpath' 2016-01-02 18:54:56 +09:00
Naoki Takezoe
ba75079409 Fix repository creation error 2016-01-02 03:08:09 +09:00
Naoki Takezoe
cbb847704b Fix compilation error in testcase 2016-01-02 03:07:53 +09:00
Naoki Takezoe
739c99edce (refs #970)Display permission change 2016-01-02 02:42:38 +09:00
Naoki Takezoe
af2e2f5fd1 Merge branch 'viliamjr-wiki-sidebar' 2016-01-01 22:46:31 +09:00
Naoki Takezoe
09fcf9c003 Merge branch 'wiki-sidebar' of https://github.com/viliamjr/gitbucket into viliamjr-wiki-sidebar
# Conflicts:
#	src/main/twirl/gitbucket/core/wiki/page.scala.html
2016-01-01 22:45:54 +09:00
Naoki Takezoe
5dad0777d7 Deploy assembly jar to snapshot repository if version ends with "-SNAPSHOT" 2016-01-01 19:57:17 +09:00
Naoki Takezoe
be3aa21651 Fix compilation error caused by JGit update 2016-01-01 17:49:32 +09:00
Naoki Takezoe
abd729e45f Update depended libraries 2016-01-01 17:43:22 +09:00
Naoki Takezoe
162e9a9feb Update version to 3.11.0-SNAPSHOT 2016-01-01 15:12:06 +09:00
Naoki Takezoe
7500d017d5 Fix dropdown bug 2016-01-01 14:42:08 +09:00
Naoki Takezoe
e8c4004d5a Remove file appender 2016-01-01 13:13:30 +09:00
Naoki Takezoe
a6f4d9d7cf Upgrade scalatra-forms to 1.0.0 that supports Scalatra 2.4.0 2015-12-31 02:33:11 +09:00
Naoki Takezoe
3399278925 (refs #738)Revert content type detection
because Scalatra automatic charset detection issue has been fixed in Scalatra 2.4.0.
https://github.com/scalatra/scalatra/pull/500
2015-12-30 10:58:06 +09:00
Naoki Takezoe
4457a317e6 (refs #815)Upgrade Scalatra to 2.4.0 2015-12-30 05:27:09 +09:00
Viliam Dias
ef3b02b718 A simple wiki page (markdown file "_Footer.md") is used as footer for the wiki.
Github specification:
https://help.github.com/articles/creating-a-footer/

- the footer is rendered below the wiki page content.
- a direct link to edit the footer.
2015-12-07 13:45:37 -02:00
Viliam Dias
9777d543b1 Remove the getWikiSideBar service
And use getWikiPage to avoid code repetition.
2015-11-16 16:35:07 -02:00
Viliam Dias
51acf72e0d A sidebar page for the wiki
A simple wiki page (markdown file "_Sidebar.md") is used as a sidebar menu for the wiki.

Github specification:
https://help.github.com/articles/creating-a-sidebar/

- the sidebar is rendered below the pages index.
- if you click on page index "counter" you can collapse or expand the
  page list.
2015-11-16 14:33:02 -02:00
Viliam Dias
6ad724d80c A service to get the wiki sidebar page
- getWikiSideBar: a new service to get the wiki sidebar file "_Sidebar.md".
- getFileContent: must ignore MD files which starts with "_".
2015-11-16 14:25:22 -02:00
108 changed files with 2420 additions and 780 deletions

View File

@@ -9,28 +9,21 @@ The current version of GitBucket provides a basic features below:
- Public / Private Git repository (http and ssh access)
- Repository viewer and online file editing
- Repository search (Code and Issues)
- Wiki
- Issues
- Fork / Pull request
- Issues / Pull request
- Email notification
- Activity timeline
- Simple user and group management with LDAP integration
- Gravatar support
- Plug-in system
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/gitbucket/gitbucket/wiki).
If you want to try the development version of GitBucket, see [Developer's Guide](https://github.com/gitbucket/gitbucket/blob/master/doc/how_to_run.md).
Installation
--------
GitBucket requires **Java8**. You have to install beforehand when it's not installed.
1. Download latest **gitbucket.war** from [the release page](https://github.com/gitbucket/gitbucket/releases).
2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher.
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser.
If you are using Gitbucket behind a webserver please make sure you have increased the **client_max_body_size** (on nginx)
The default administrator account is **root** and password is **root**.
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser and logged-in with **root** / **root**.
or you can start GitBucket by `java -jar gitbucket.war` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options.
@@ -41,35 +34,7 @@ or you can start GitBucket by `java -jar gitbucket.war` without servlet containe
To upgrade GitBucket, only replace gitbucket.war after stop GitBucket. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
For Installation on Windows Server with IIS see [this wiki page](https://github.com/gitbucket/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo)
### Mac OS X
#### Installing Via Homebrew
```
$ brew install gitbucket
==> Downloading https://github.com/takezoe/gitbucket/releases/download/1.10/gitbucket.war
######################################################################## 100.0%
==> Caveats
Note: When using launchctl the port will be 8080.
To have launchd start gitbucket at login:
ln -sfv /usr/local/opt/gitbucket/*.plist ~/Library/LaunchAgents
Then to load gitbucket now:
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.gitbucket.plist
Or, if you don't want/need launchctl, you can just run:
java -jar /usr/local/opt/gitbucket/libexec/gitbucket.war
==> Summary
/usr/local/Cellar/gitbucket/1.10: 3 files, 42M, built in 11 seconds
```
#### Manual Installation
On OS X, generate `gitbucket.plist` by [this script](https://raw.githubusercontent.com/gitbucket/gitbucket/master/contrib/macosx/makePlist) and copy it to `~/Library/LaunchAgents/`
Run the following commands in `Terminal` to
- start gitbucket: `launchctl load ~/Library/LaunchAgents/gitbucket.plist`
- stop gitbucket: `launchctl unload ~/Library/LaunchAgents/gitbucket.plist`
About installation on Mac or Windows Server (with IIS), configuration of Apache or Nginx and also integration with other tools or services such as Jenkins or Slack, see [Wiki](https://github.com/gitbucket/gitbucket/wiki).
Plug-ins
--------
@@ -95,10 +60,18 @@ Support
Release Notes
--------
### 3.11 - 30 Jan 2016
- Upgrade Scalatra to 2.4
- Sidebar and Footer for Wiki
- Branch protection and receive hook extension point for plug-in
- Limit recent updated repositories list
- Issue actions look-alike GitHub
- Web API for labels
### 3.10 - 30 Dec 2015
- Move to Bootstrap3
- New URL for raw contents (`raw/master/doc/activity.md` instead of `blob/master/doc/activity.md?raw=true`)
- Update xsbt-web-pligin
- Update xsbt-web-plugin
- Update H2 database
### 3.9 - 5 Dec 2015
@@ -342,7 +315,3 @@ Release Notes
### 1.0 - 04 Jul 2013
- This is a first public release
Sponsors
--------
[![IntelliJ IDEA](https://www.jetbrains.com/idea/docs/logo_intellij_idea.png)](https://www.jetbrains.com/idea/)

159
build.sbt Normal file
View File

@@ -0,0 +1,159 @@
val Organization = "gitbucket"
val Name = "gitbucket"
val GitBucketVersion = "3.11.0"
val ScalatraVersion = "2.4.0"
val JettyVersion = "9.3.6.v20151106"
lazy val root = (project in file(".")).enablePlugins(SbtTwirl, JettyPlugin)
sourcesInBase := false
organization := Organization
name := Name
version := GitBucketVersion
scalaVersion := "2.11.6"
// dependency settings
resolvers ++= Seq(
Classpaths.typesafeReleases,
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/",
"amateras-snapshot-repo" at "http://amateras.sourceforge.jp/mvn-snapshot/"
)
libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.1.1.201511131810-r",
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.1.1.201511131810-r",
"org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "3.3.0",
"io.github.gitbucket" %% "scalatra-forms" % "1.0.0",
"commons-io" % "commons-io" % "2.4",
"io.github.gitbucket" % "markedj" % "1.0.6",
"org.apache.commons" % "commons-compress" % "1.10",
"org.apache.commons" % "commons-email" % "1.4",
"org.apache.httpcomponents" % "httpclient" % "4.5.1",
"org.apache.sshd" % "apache-sshd" % "1.0.0",
"org.apache.tika" % "tika-core" % "1.11",
"com.typesafe.slick" %% "slick" % "2.1.0",
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.4.190",
"ch.qos.logback" % "logback-classic" % "1.1.1",
"com.mchange" % "c3p0" % "0.9.5.2",
"com.typesafe" % "config" % "1.3.0",
"com.typesafe.akka" %% "akka-actor" % "2.3.14",
"com.enragedginger" %% "akka-quartz-scheduler" % "1.4.0-akka-2.3.x" exclude("c3p0","c3p0"),
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
"junit" % "junit" % "4.12" % "test",
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
"org.specs2" %% "specs2-junit" % "3.6.6" % "test"
)
// Twirl settings
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._"
// Compiler settings
scalacOptions := Seq("-deprecation", "-language:postfixOps")
javacOptions in compile ++= Seq("-target", "7", "-source", "7")
javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml"
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console")
javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test"
testOptions in Test += Tests.Setup( () => new java.io.File("target/gitbucket_home_for_test").mkdir() )
fork in Test := true
packageOptions += Package.MainClass("JettyLauncher")
// Assembly settings
test in assembly := {}
assemblyMergeStrategy in assembly := {
case PathList("META-INF", xs @ _*) =>
(xs map {_.toLowerCase}) match {
case ("manifest.mf" :: Nil) => MergeStrategy.discard
case _ => MergeStrategy.discard
}
case x => MergeStrategy.first
}
// JRebel
jrebel.webLinks += (target in webappPrepare).value
jrebel.enabled := System.getenv().get("JREBEL") != null
javaOptions in Jetty ++= Option(System.getenv().get("JREBEL")).toSeq.flatMap { path =>
Seq("-noverify", "-XX:+UseConcMarkSweepGC", "-XX:+CMSClassUnloadingEnabled", s"-javaagent:${path}")
}
jrebelSettings
// Create executable war file
val executableConfig = config("executable").hide
Keys.ivyConfigurations += executableConfig
libraryDependencies ++= Seq(
"org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-continuation" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable"
)
val executableKey = TaskKey[File]("executable")
executableKey := {
import org.apache.ivy.util.ChecksumHelper
import java.util.jar.{ Manifest => JarManifest }
import java.util.jar.Attributes.{ Name => AttrName }
val workDir = Keys.target.value / "executable"
val warName = Keys.name.value + ".war"
val log = streams.value.log
log info s"building executable webapp in ${workDir}"
// initialize temp directory
val temp = workDir / "webapp"
IO delete temp
// include jetty classes
val jettyJars = Keys.update.value select configurationFilter(name = executableConfig.name)
jettyJars foreach { jar =>
IO unzip (jar, temp, (name:String) =>
(name startsWith "javax/") ||
(name startsWith "org/")
)
}
// include original war file
val warFile = (Keys.`package`).value
IO unzip (warFile, temp)
// include launcher classes
val classDir = (Keys.classDirectory in Compile).value
val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */)
launchClasses foreach { name =>
IO copyFile (classDir / name, temp / name)
}
// zip it up
IO delete (temp / "META-INF" / "MANIFEST.MF")
val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp)
val manifest = new JarManifest
manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0")
manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher")
val outputFile = workDir / warName
IO jar (contentMappings, outputFile, manifest)
// generate checksums
Seq("md5", "sha1") foreach { algorithm =>
IO.write(
workDir / (warName + "." + algorithm),
ChecksumHelper computeAsString (outputFile, algorithm)
)
}
// done
log info s"built executable webapp ${outputFile}"
outputFile
}
/*
Keys.artifact in (Compile, executableKey) ~= {
_ copy (`type` = "war", extension = "war"))
}
addArtifact(Keys.artifact in (Compile, executableKey), executableKey)
*/

View File

@@ -8,7 +8,7 @@ To release a new version of GitBucket, add the version definition to the [gitbuc
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 GitBucket version.
*/
val versions = Seq(
Version(1, 0)
@@ -20,7 +20,7 @@ Next, add a SQL file which updates database schema into [/src/main/resources/upd
GitBucket stores the current version to ```GITBUCKET_HOME/version``` and checks it at start-up. If the stored version differs from the actual version, it executes differences of SQL files between the stored version and the actual version. And ```GITBUCKET_HOME/version``` is updated by the actual version.
We can also add any Scala code for upgrade GitBucket which modifies esources other than database. Override ```Version.update``` like below:
We can also add any Scala code for upgrade GitBucket which modifies resources other than database. Override ```Version.update``` like below:
```scala
val versions = Seq(

View File

@@ -8,10 +8,10 @@ This directory has following structure:
* /HOME/gitbucket
* /repositories
* /USER_NAME
* / REPO_NAME.git (substance of repository. GitServlet sees this directory)
* / REPO_NAME
* /REPO_NAME.git (substance of repository. GitServlet sees this directory)
* /REPO_NAME
* /issues (files which are attached to issue)
* / REPO_NAME.wiki.git (wiki repository)
* /REPO_NAME.wiki.git (wiki repository)
* /data
* /USER_NAME
* /files

View File

@@ -27,7 +27,8 @@ $ sbt package
To build executable war file, run
* Windows: Not available
* Linux: `./release/make-release-war.sh`
```
$ sbt executable
```
at the top of the source tree. It generates executable `gitbucket.war` into `target/scala-2.11`. We release this war file as release artifact.
at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`. We release this war file as release artifact.

148
doc/jrebel.md Normal file
View File

@@ -0,0 +1,148 @@
JRebel integration (optional)
=============================
[JRebel](http://zeroturnaround.com/software/jrebel/) is a JVM plugin that makes developing web apps much faster.
JRebel is generally able to eliminate the need for the following slow "app restart" in sbt following a code change:
```
> jetty:start
```
While JRebel is not open source, it does reload your code faster than the `~;copy-resources;aux-compile` way of doing things using `sbt`.
It's only used during development, and doesn't change your deployed app in any way.
JRebel used to be free for Scala developers, but that changed recently, and now there's a cost associated with usage for Scala. There are trial plans and free non-commercial licenses available if you just want to try it out.
----
## 1. Get a JRebel license
Sign up for a [usage plan](https://my.jrebel.com/). You will need to create an account.
## 2. Download JRebel
Download the most recent ["nosetup" JRebel zip](http://zeroturnaround.com/software/jrebel/download/prev-releases/).
Next, unzip the downloaded file.
## 3. Activate
Follow the [instructions on the JRebel website](http://zeroturnaround.com/software/jrebel/download/prev-releases/) to activate your downloaded JRebel.
You can use the default settings for all the configurations.
You don't need to integrate with your IDE, since we're using sbt to do the servlet deployment.
## 4. Tell jvm where JRebel is
Fortunately, the gitbucket project is already set up to use JRebel.
You only need to tell jvm where to find the jrebel jar.
To do so, edit your shell resource file (usually `~/.bash_profile` on Mac, and `~/.bashrc` on Linux), and add the following line:
```bash
export JREBEL=/path/to/jrebel/jrebel.jar
```
For example, if you unzipped your JRebel download in your home directory, you whould use:
```bash
export JREBEL=~/jrebel/jrebel.jar
```
Now reload your shell:
```
$ source ~/.bash_profile # on Mac
$ source ~/.bashrc # on Linux
```
## 5. See it in action!
Now you're ready to use JRebel with the gitbucket.
When you run sbt as normal, you will see a long message from JRebel, indicating it has loaded.
Here's an abbreviated version of what you will see:
```
$ ./sbt
[info] Loading project definition from /git/gitbucket/project
[info] Set current project to gitbucket (in build file:/git/gitbucket/)
>
```
You will start the servlet container slightly differently now that you're using sbt.
```
> jetty:start
:
[info] starting server ...
[success] Total time: 3 s, completed Jan 3, 2016 9:47:55 PM
2016-01-03 21:47:57 JRebel:
2016-01-03 21:47:57 JRebel: A newer version '6.3.1' is available for download
2016-01-03 21:47:57 JRebel: from http://zeroturnaround.com/software/jrebel/download/
2016-01-03 21:47:57 JRebel:
2016-01-03 21:47:58 JRebel: Contacting myJRebel server ..
2016-01-03 21:47:59 JRebel: Directory '/git/gitbucket/target/scala-2.11/classes' will be monitored for changes.
2016-01-03 21:47:59 JRebel: Directory '/git/gitbucket/target/scala-2.11/test-classes' will be monitored for changes.
2016-01-03 21:47:59 JRebel: Directory '/git/gitbucket/target/webapp' will be monitored for changes.
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel: #############################################################
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel: JRebel Legacy Agent 6.2.5 (201509291538)
2016-01-03 21:48:00 JRebel: (c) Copyright ZeroTurnaround AS, Estonia, Tartu.
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel: Over the last 30 days JRebel prevented
2016-01-03 21:48:00 JRebel: at least 182 redeploys/restarts saving you about 7.4 hours.
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel: Over the last 324 days JRebel prevented
2016-01-03 21:48:00 JRebel: at least 1538 redeploys/restarts saving you about 62.4 hours.
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel: Licensed to nazo king (using myJRebel).
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel: #############################################################
2016-01-03 21:48:00 JRebel:
:
> ~ copy-resources
[success] Total time: 0 s, completed Jan 3, 2016 9:13:54 PM
1. Waiting for source changes... (press enter to interrupt)
```
Finally, change your code.
For example, you can change the title on `src/main/twirl/gitbucket/core/main.scala.html` like this:
```html
:
<a class="navbar-brand" href="@path/">
<img src="@assets/common/images/gitbucket.png" style="width: 24px; height: 24px;"/>GitBucket
@defining(AutoUpdate.getCurrentVersion){ version =>
<span class="header-version">@version.majorVersion.@version.minorVersion</span>
}
change code !!!!!!!!!!!!!!!!
</a>
:
```
If JRebel is doing is correctly installed you will see a notice for you:
```
1. Waiting for source changes... (press enter to interrupt)
2016-01-03 21:48:42 JRebel: Reloading class 'gitbucket.core.html.main$'.
[info] Wrote rebel.xml to /git/gitbucket/target/scala-2.11/resource_managed/main/rebel.xml
[info] Compiling 1 Scala source to /git/gitbucket/target/scala-2.11/classes...
[success] Total time: 3 s, completed Jan 3, 2016 9:48:55 PM
2. Waiting for source changes... (press enter to interrupt)
```
And you reload browser, JRebel give notice of that it has reloaded classes:
```
[success] Total time: 3 s, completed Jan 3, 2016 9:48:55 PM
2. Waiting for source changes... (press enter to interrupt)
2016-01-03 21:49:13 JRebel: Reloading class 'gitbucket.core.html.main$'.
```
## 6. Limitations
JRebel is nearly always able to eliminate the need to explicitly reload your container after a code change. However, if you change any of your routes patterns, there is nothing JRebel can do, you will have to run `jetty:start`.

View File

@@ -9,3 +9,4 @@ Developer's Guide
* [Notification Email](notification.md)
* [Automatic Schema Updating](auto_update.md)
* [Release Operation](release.md)
* [JRebel integration (optional)](jrebel.md)

View File

@@ -23,7 +23,7 @@ object MyBuild extends Build {
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 GitBucket version.
*/
val versions = Seq(
new Version(3, 3), // <---- add this line!!
@@ -37,11 +37,10 @@ Note: Release operation requires [Ant](http://ant.apache.org/) and [Maven](https
### Make release war file
Run `release/make-release-war.sh`. The release war file is generated into `target/scala-2.11/gitbucket.war`.
Run `sbt executable`. The release war file and fingerprint are generated into `target/executable/gitbucket.war`.
```bash
$ cd release
$ ./make-release-war.sh
$sbt executable
```
### Deploy assembly jar file

View File

@@ -1,11 +0,0 @@
#!/bin/bash
version=$1
output_dir=`dirname $0`
git rm -f ${output_dir}/jetty-*.jar
for name in 'io' 'servlet' 'xml' 'continuation' 'security' 'util' 'http' 'server' 'webapp'
do
jar_filename="jetty-${name}-${version}.jar"
wget "http://repo1.maven.org/maven2/org/eclipse/jetty/jetty-${name}/${version}/${jar_filename}" -O ${output_dir}/${jar_filename}
done
git add ${output_dir}/*.jar
git commit

View File

@@ -1,80 +0,0 @@
import com.earldouglas.xwp.JettyPlugin
import play.twirl.sbt.SbtTwirl
import sbt.Keys._
import sbt._
import sbtassembly.AssemblyKeys._
import sbtassembly._
import JettyPlugin.autoImport._
object MyBuild extends Build {
val Organization = "gitbucket"
val Name = "gitbucket"
val Version = "3.10.0"
val ScalaVersion = "2.11.6"
val ScalatraVersion = "2.3.1"
lazy val project = Project (
"gitbucket",
file(".")
)
// .settings(ScalatraPlugin.scalatraWithJRebel: _*)
.settings(
test in assembly := {},
assemblyMergeStrategy in assembly := {
case PathList("META-INF", xs @ _*) =>
(xs map {_.toLowerCase}) match {
case ("manifest.mf" :: Nil) => MergeStrategy.discard
case _ => MergeStrategy.discard
}
case x => MergeStrategy.first
}
)
.settings(
sourcesInBase := false,
organization := Organization,
name := Name,
version := Version,
scalaVersion := ScalaVersion,
resolvers ++= Seq(
Classpaths.typesafeReleases,
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/",
"amateras-snapshot-repo" at "http://amateras.sourceforge.jp/mvn-snapshot/"
),
scalacOptions := Seq("-deprecation", "-language:postfixOps"),
libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.4.2.201412180340-r",
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "3.4.2.201412180340-r",
"org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "3.2.11",
"jp.sf.amateras" %% "scalatra-forms" % "0.2.0",
"commons-io" % "commons-io" % "2.4",
"io.github.gitbucket" % "markedj" % "1.0.6-SNAPSHOT",
"org.apache.commons" % "commons-compress" % "1.9",
"org.apache.commons" % "commons-email" % "1.3.3",
"org.apache.httpcomponents" % "httpclient" % "4.3.6",
"org.apache.sshd" % "apache-sshd" % "1.0.0",
"org.apache.tika" % "tika-core" % "1.10",
"com.typesafe.slick" %% "slick" % "2.1.0",
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.4.190",
"ch.qos.logback" % "logback-classic" % "1.1.1",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.16.v20140903" % "provided",
"javax.servlet" % "javax.servlet-api" % "3.0.1" % "provided",
"junit" % "junit" % "4.12" % "test",
"com.mchange" % "c3p0" % "0.9.5.2",
"com.typesafe" % "config" % "1.2.1",
"com.typesafe.akka" %% "akka-actor" % "2.3.10",
"com.enragedginger" %% "akka-quartz-scheduler" % "1.3.0-akka-2.3.x" exclude("c3p0","c3p0")
),
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._",
javacOptions in compile ++= Seq("-target", "7", "-source", "7"),
javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml",
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test",
testOptions in Test += Tests.Setup( () => new java.io.File("target/gitbucket_home_for_test").mkdir() ),
fork in Test := true,
packageOptions += Package.MainClass("JettyLauncher")
).enablePlugins(SbtTwirl, JettyPlugin)
}

View File

@@ -2,4 +2,5 @@ scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.4")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0")
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "2.1.0")
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "2.1.0")
addSbtPlugin("fi.gekkio.sbtplugins" % "sbt-jrebel-plugin" % "0.10.0")

View File

@@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<project name="gitbucket" default="all" basedir="..">
<property environment="env"/>
<property name="target.dir" value="target"/>
<property name="embed.classes.dir" value="${target.dir}/embed-classes"/>
<property name="jetty.dir" value="embed-jetty"/>
<property name="scala.version" value="2.11"/>
<property name="gitbucket.version" value="${env.GITBUCKET_VERSION}"/>
<property name="jetty.version" value="8.1.16.v20140903"/>
<property name="servlet.version" value="3.0.0.v201112011016"/>
<condition property="sbt.exec" value="sbt.bat" else="sbt.sh">
<os family="windows" />
</condition>
<target name="clean">
<delete dir="${embed.classes.dir}"/>
<delete file="${target.dir}/scala-${scala.version}/gitbucket.war"/>
</target>
<target name="war" depends="clean">
<exec executable="${sbt.exec}" resolveexecutable="true" failonerror="true">
<arg line="clean compile test package" />
</exec>
</target>
<target name="embed" depends="war">
<mkdir dir="${embed.classes.dir}"/>
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/javax.servlet-${servlet.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-continuation-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-http-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-io-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-security-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-server-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-servlet-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-util-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-webapp-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-xml-${jetty.version}.jar" />
<zip destfile="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
basedir="${embed.classes.dir}"
update = "true"
includes="javax/**,org/**"/>
<zip destfile="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
basedir="${target.dir}/scala-${scala.version}/classes"
update = "true"
includes="JettyLauncher.class,HttpsSupportConnector.class"/>
</target>
<target name="rename" depends="embed">
<move file="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
tofile="${target.dir}/scala-${scala.version}/gitbucket.war"/>
</target>
<target name="checksum" depends="rename">
<checksum file="${target.dir}/scala-${scala.version}/gitbucket.war" algorithm="MD5" format="MD5SUM" forceOverwrite="yes" fileext=".md5"/>
<checksum file="${target.dir}/scala-${scala.version}/gitbucket.war" algorithm="SHA" format="MD5SUM" forceOverwrite="yes" fileext=".sha1"/>
</target>
<target name="all" depends="checksum">
</target>
</project>

View File

@@ -5,6 +5,15 @@ cd ../
./sbt.sh clean assembly
cd release
if [[ "$GITBUCKET_VERSION" =~ -SNAPSHOT$ ]]; then
MVN_DEPLOY_PATH=mvn-snapshot
else
MVN_DEPLOY_PATH=mvn
fi
echo $MVN_DEPLOY_PATH
mvn deploy:deploy-file \
-DgroupId=gitbucket\
-DartifactId=gitbucket-assembly\
@@ -12,4 +21,4 @@ mvn deploy:deploy-file \
-Dpackaging=jar\
-Dfile=../target/scala-2.11/gitbucket-assembly-$GITBUCKET_VERSION.jar\
-DrepositoryId=sourceforge.jp\
-Durl=scp://shell.sourceforge.jp/home/groups/a/am/amateras/htdocs/mvn/
-Durl=scp://shell.sourceforge.jp/home/groups/a/am/amateras/htdocs/$MVN_DEPLOY_PATH/

View File

@@ -1,15 +0,0 @@
#!/bin/sh
D="$(dirname "$0")"
D="$(cd "${D}"; pwd)"
DD="$(dirname "${D}")"
(
for f in "${D}/env.sh" "${D}/build.xml"; do
if [ ! -s "${f}" ]; then
echo >&2 "$0: Unable to access file '${f}'"
exit 1
fi
done
. "${D}/env.sh"
cd "${DD}"
ant -f "${D}/build.xml" all
)

View File

@@ -1,5 +1,4 @@
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.webapp.WebAppContext;
import java.io.File;
@@ -30,16 +29,16 @@ public class JettyLauncher {
}
}
Server server = new Server();
Server server = new Server(port);
SelectChannelConnector connector = new SelectChannelConnector();
if(host != null) {
connector.setHost(host);
}
connector.setMaxIdleTime(1000 * 60 * 60);
connector.setSoLingerTime(-1);
connector.setPort(port);
server.addConnector(connector);
// SelectChannelConnector connector = new SelectChannelConnector();
// if(host != null) {
// connector.setHost(host);
// }
// connector.setMaxIdleTime(1000 * 60 * 60);
// connector.setSoLingerTime(-1);
// connector.setPort(port);
// server.addConnector(connector);
WebAppContext context = new WebAppContext();

View File

@@ -6,12 +6,14 @@
</encoder>
</appender>
<!--
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>gitbucket.log</file>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
-->
<root level="INFO">
<appender-ref ref="STDOUT" />

View File

@@ -0,0 +1,25 @@
DROP TABLE IF EXISTS PROTECTED_BRANCH;
CREATE TABLE PROTECTED_BRANCH(
USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL,
BRANCH VARCHAR(100) NOT NULL,
STATUS_CHECK_ADMIN BOOLEAN NOT NULL DEFAULT false
);
ALTER TABLE PROTECTED_BRANCH ADD CONSTRAINT IDX_PROTECTED_BRANCH_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, BRANCH);
ALTER TABLE PROTECTED_BRANCH ADD CONSTRAINT IDX_PROTECTED_BRANCH_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME)
ON DELETE CASCADE ON UPDATE CASCADE;
DROP TABLE IF EXISTS PROTECTED_BRANCH_REQUIRE_CONTEXT;
CREATE TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT(
USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL,
BRANCH VARCHAR(100) NOT NULL,
CONTEXT VARCHAR(255) NOT NULL
);
ALTER TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT ADD CONSTRAINT IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, BRANCH, CONTEXT);
ALTER TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT ADD CONSTRAINT IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, BRANCH) REFERENCES PROTECTED_BRANCH (USER_NAME, REPOSITORY_NAME, BRANCH)
ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,16 @@
package gitbucket.core.api
import gitbucket.core.util.RepositoryName
/**
* https://developer.github.com/v3/repos/#get-branch
* https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection
*/
case class ApiBranch(
name: String,
// commit: ApiBranchCommit,
protection: ApiBranchProtection)(repositoryName:RepositoryName) extends FieldSerializable {
def _links = Map(
"self" -> ApiPath(s"/api/v3/repos/${repositoryName.fullName}/branches/${name}"),
"html" -> ApiPath(s"/${repositoryName.fullName}/tree/${name}"))
}

View File

@@ -0,0 +1,47 @@
package gitbucket.core.api
import gitbucket.core.service.ProtectedBranchService
import org.json4s._
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
case class ApiBranchProtection(enabled: Boolean, required_status_checks: Option[ApiBranchProtection.Status]){
def status: ApiBranchProtection.Status = required_status_checks.getOrElse(ApiBranchProtection.statusNone)
}
object ApiBranchProtection{
/** form for enabling-and-disabling-branch-protection */
case class EnablingAndDisabling(protection: ApiBranchProtection)
def apply(info: ProtectedBranchService.ProtectedBranchInfo): ApiBranchProtection = ApiBranchProtection(
enabled = info.enabled,
required_status_checks = Some(Status(EnforcementLevel(info.enabled, info.includeAdministrators), info.contexts)))
val statusNone = Status(Off, Seq.empty)
case class Status(enforcement_level: EnforcementLevel, contexts: Seq[String])
sealed class EnforcementLevel(val name: String)
case object Off extends EnforcementLevel("off")
case object NonAdmins extends EnforcementLevel("non_admins")
case object Everyone extends EnforcementLevel("everyone")
object EnforcementLevel {
def apply(enabled: Boolean, includeAdministrators: Boolean): EnforcementLevel = if(enabled){
if(includeAdministrators){
Everyone
}else{
NonAdmins
}
}else{
Off
}
}
implicit val enforcementLevelSerializer = new CustomSerializer[EnforcementLevel](format => (
{
case JString("off") => Off
case JString("non_admins") => NonAdmins
case JString("everyone") => Everyone
},
{
case x: EnforcementLevel => JString(x.name)
}
))
}

View File

@@ -0,0 +1,21 @@
package gitbucket.core.api
import gitbucket.core.model.Label
import gitbucket.core.util.RepositoryName
/**
* https://developer.github.com/v3/issues/labels/
*/
case class ApiLabel(
name: String,
color: String)(repositoryName: RepositoryName){
var url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/labels/${name}")
}
object ApiLabel{
def apply(label:Label, repositoryName: RepositoryName): ApiLabel =
ApiLabel(
name = label.labelName,
color = label.color
)(repositoryName)
}

View File

@@ -0,0 +1,18 @@
package gitbucket.core.api
/**
* https://developer.github.com/v3/issues/labels/#create-a-label
* api form
*/
case class CreateALabel(
name: String,
color: String
) {
def isValid: Boolean = {
name.length<=100 &&
!name.startsWith("_") &&
!name.startsWith("-") &&
color.length==6 &&
color.matches("[a-fA-F0-9+_.]+")
}
}

View File

@@ -30,7 +30,9 @@ object JsonFormat {
FieldSerializer[ApiCombinedCommitStatus]() +
FieldSerializer[ApiPullRequest.Commit]() +
FieldSerializer[ApiIssue]() +
FieldSerializer[ApiComment]()
FieldSerializer[ApiComment]() +
FieldSerializer[ApiLabel]() +
ApiBranchProtection.enforcementLevelSerializer
def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](format =>
(

View File

@@ -12,7 +12,7 @@ import gitbucket.core.util.Implicits._
import gitbucket.core.util.StringUtil._
import gitbucket.core.util._
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.dircache.DirCache

View File

@@ -8,7 +8,7 @@ import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import gitbucket.core.util._
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.json4s._
import org.scalatra._
@@ -28,7 +28,11 @@ abstract class ControllerBase extends ScalatraFilter
with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
with SystemSettingsService {
implicit val jsonFormats = DefaultFormats
implicit val jsonFormats = gitbucket.core.api.JsonFormat.jsonFormats
before("/api/v3/*") {
contentType = formats("json")
}
// TODO Scala 2.11
// // Don't set content type via Accept header.

View File

@@ -8,7 +8,7 @@ import gitbucket.core.service.{RepositoryService, ActivityService, AccountServic
import gitbucket.core.util.Implicits._
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator}
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
class IndexController extends IndexControllerBase

View File

@@ -11,7 +11,7 @@ import gitbucket.core.util._
import gitbucket.core.view
import gitbucket.core.view.Markdown
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
import org.scalatra.Ok

View File

@@ -1,12 +1,13 @@
package gitbucket.core.controller
import gitbucket.core.api.{ApiError, CreateALabel, ApiLabel, JsonFormat}
import gitbucket.core.issues.labels.html
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService}
import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import gitbucket.core.util.{LockUtil, RepositoryName, ReferrerAuthenticator, CollaboratorsAuthenticator}
import gitbucket.core.util.Implicits._
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
import org.scalatra.i18n.Messages
import org.scalatra.Ok
import org.scalatra.{NoContent, UnprocessableEntity, Created, Ok}
class LabelsController extends LabelsControllerBase
with LabelsService with IssuesService with RepositoryService with AccountService
@@ -19,7 +20,7 @@ trait LabelsControllerBase extends ControllerBase {
case class LabelForm(labelName: String, color: String)
val labelForm = mapping(
"labelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
"labelName" -> trim(label("Label name", text(required, labelName, uniqueLabelName, maxlength(100)))),
"labelColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply)
@@ -31,6 +32,26 @@ trait LabelsControllerBase extends ControllerBase {
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
/**
* List all labels for this repository
* https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository
*/
get("/api/v3/repos/:owner/:repository/labels")(referrersOnly { repository =>
JsonFormat(getLabels(repository.owner, repository.name).map { label =>
ApiLabel(label, RepositoryName(repository))
})
})
/**
* Get a single label
* https://developer.github.com/v3/issues/labels/#get-a-single-label
*/
get("/api/v3/repos/:owner/:repository/labels/:labelName")(referrersOnly { repository =>
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
JsonFormat(ApiLabel(label, RepositoryName(repository)))
} getOrElse NotFound()
})
ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository =>
html.edit(None, repository)
})
@@ -45,6 +66,31 @@ trait LabelsControllerBase extends ControllerBase {
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
/**
* Create a label
* https://developer.github.com/v3/issues/labels/#create-a-label
*/
post("/api/v3/repos/:owner/:repository/labels")(collaboratorsOnly { repository =>
(for{
data <- extractFromJsonBody[CreateALabel] if data.isValid
} yield {
LockUtil.lock(RepositoryName(repository).fullName) {
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
val labelId = createLabel(repository.owner, repository.name, data.name, data.color)
getLabel(repository.owner, repository.name, labelId).map { label =>
Created(JsonFormat(ApiLabel(label, RepositoryName(repository))))
} getOrElse NotFound()
} else {
// TODO ApiError should support errors field to enhance compatibility of GitHub API
UnprocessableEntity(ApiError(
"Validation Failed",
Some("https://developer.github.com/v3/issues/labels/#create-a-label")
))
}
}
}) getOrElse NotFound()
})
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository =>
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
html.edit(Some(label), repository)
@@ -61,11 +107,50 @@ trait LabelsControllerBase extends ControllerBase {
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
/**
* Update a label
* https://developer.github.com/v3/issues/labels/#update-a-label
*/
patch("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
(for{
data <- extractFromJsonBody[CreateALabel] if data.isValid
} yield {
LockUtil.lock(RepositoryName(repository).fullName) {
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color)
JsonFormat(ApiLabel(
getLabel(repository.owner, repository.name, label.labelId).get,
RepositoryName(repository)))
} else {
// TODO ApiError should support errors field to enhance compatibility of GitHub API
UnprocessableEntity(ApiError(
"Validation Failed",
Some("https://developer.github.com/v3/issues/labels/#create-a-label")))
}
} getOrElse NotFound()
}
}) getOrElse NotFound()
})
ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository =>
deleteLabel(repository.owner, repository.name, params("labelId").toInt)
Ok()
})
/**
* Delete a label
* https://developer.github.com/v3/issues/labels/#delete-a-label
*/
delete("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
LockUtil.lock(RepositoryName(repository).fullName) {
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
deleteLabel(repository.owner, repository.name, label.labelId)
NoContent()
} getOrElse NotFound()
}
})
/**
* Constraint for the identifier such as user name, repository name or page name.
*/
@@ -80,4 +165,12 @@ trait LabelsControllerBase extends ControllerBase {
}
}
private def uniqueLabelName: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = {
val owner = params("owner")
val repository = params("repository")
getLabel(owner, repository, value).map(_ => "Name has already been taken.")
}
}
}

View File

@@ -4,7 +4,7 @@ import gitbucket.core.issues.milestones.html
import gitbucket.core.service.{RepositoryService, MilestonesService, AccountService}
import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import gitbucket.core.util.Implicits._
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
class MilestonesController extends MilestonesControllerBase
with MilestonesService with RepositoryService with AccountService

View File

@@ -1,7 +1,7 @@
package gitbucket.core.controller
import gitbucket.core.api._
import gitbucket.core.model.{Account, CommitState, Repository, PullRequest, Issue}
import gitbucket.core.model.{Account, CommitStatus, CommitState, Repository, PullRequest, Issue, WebHook}
import gitbucket.core.pulls.html
import gitbucket.core.service.CommitStatusService
import gitbucket.core.service.MergeService
@@ -16,7 +16,7 @@ import gitbucket.core.util._
import gitbucket.core.view
import gitbucket.core.view.helpers
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.PersonIdent
import org.slf4j.LoggerFactory
@@ -27,13 +27,13 @@ import scala.collection.JavaConverters._
class PullRequestsController extends PullRequestsControllerBase
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
with CommitsService with ActivityService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
with CommitStatusService with MergeService
with CommitStatusService with MergeService with ProtectedBranchService
trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
with CommitStatusService with MergeService =>
with CommitStatusService with MergeService with ProtectedBranchService =>
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
@@ -119,7 +119,8 @@ trait PullRequestsControllerBase extends ControllerBase {
commits,
diffs,
hasWritePermission(owner, name, context.loginAccount),
repository)
repository,
flash.toMap.map(f => f._1 -> f._2.toString))
}
}
} getOrElse NotFound
@@ -166,22 +167,34 @@ trait PullRequestsControllerBase extends ControllerBase {
} getOrElse NotFound
})
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository =>
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository =>
params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner
val name = repository.name
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
val statuses = getCommitStatues(owner, name, pullreq.commitIdTo)
val hasConfrict = LockUtil.lock(s"${owner}/${name}"){
val hasConflict = LockUtil.lock(s"${owner}/${name}"){
checkConflict(owner, name, pullreq.branch, issueId)
}
val hasProblem = hasConfrict || (!statuses.isEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
val hasMergePermission = hasWritePermission(owner, name, context.loginAccount)
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.branch)
val mergeStatus = PullRequestService.MergeStatus(
hasConflict = hasConflict,
commitStatues = getCommitStatues(owner, name, pullreq.commitIdTo),
branchProtection = branchProtection,
branchIsOutOfDate = JGitUtil.getShaByRef(owner, name, pullreq.branch) != Some(pullreq.commitIdFrom),
needStatusCheck = context.loginAccount.map{ u =>
branchProtection.needStatusCheck(u.userName)
}.getOrElse(true),
hasUpdatePermission = hasWritePermission(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount) &&
context.loginAccount.map{ u =>
!getProtectedBranchInfo(pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch).needStatusCheck(u.userName)
}.getOrElse(false),
hasMergePermission = hasMergePermission,
commitIdTo = pullreq.commitIdTo)
html.mergeguide(
hasConfrict,
hasProblem,
mergeStatus,
issue,
pullreq,
statuses,
repository,
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName, context.baseUrl).get)
}
@@ -203,6 +216,75 @@ trait PullRequestsControllerBase extends ControllerBase {
} getOrElse NotFound
})
post("/:owner/:repository/pull/:id/update_branch")(referrersOnly { baseRepository =>
(for {
issueId <- params("id").toIntOpt
loginAccount <- context.loginAccount
(issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId)
owner = pullreq.requestUserName
name = pullreq.requestRepositoryName
if hasWritePermission(owner, name, context.loginAccount)
} yield {
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch)
if(branchProtection.needStatusCheck(loginAccount.userName)){
flash += "error" -> s"branch ${pullreq.requestBranch} is protected need status check."
} else {
val repository = getRepository(owner, name, context.baseUrl).get
LockUtil.lock(s"${owner}/${name}"){
val alias = if(pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName){
pullreq.branch
}else{
s"${pullreq.userName}:${pullreq.branch}"
}
val existIds = using(Git.open(Directory.getRepositoryDir(owner, name))) { git => JGitUtil.getAllCommitIds(git) }.toSet
pullRemote(owner, name, pullreq.requestBranch, pullreq.userName, pullreq.repositoryName, pullreq.branch, loginAccount,
"Merge branch '${alias}' into ${pullreq.requestBranch}") match {
case None => // conflict
flash += "error" -> s"Can't automatic merging branch '${alias}' into ${pullreq.requestBranch}."
case Some(oldId) =>
// update pull request
updatePullRequests(owner, name, pullreq.requestBranch)
using(Git.open(Directory.getRepositoryDir(owner, name))) { git =>
// after update branch
val newCommitId = git.getRepository.resolve(s"refs/heads/${pullreq.requestBranch}")
val commits = git.log.addRange(oldId, newCommitId).call.iterator.asScala.map(c => new JGitUtil.CommitInfo(c)).toList
commits.foreach{ commit =>
if(!existIds.contains(commit.id)){
createIssueComment(owner, name, commit)
}
}
// record activity
recordPushActivity(owner, name, loginAccount.userName, pullreq.branch, commits)
// close issue by commit message
if(pullreq.requestBranch == repository.repository.defaultBranch){
commits.map{ commit =>
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
}
}
// call web hook
callPullRequestWebHookByRequestBranch("synchronize", repository, pullreq.requestBranch, baseUrl, loginAccount)
callWebHookOf(owner, name, WebHook.Push) {
for {
ownerAccount <- getAccountByUserName(owner)
} yield {
WebHookService.WebHookPushPayload(git, loginAccount, pullreq.requestBranch, repository, commits, ownerAccount, oldId = oldId, newId = newCommitId)
}
}
}
flash += "info" -> s"Merge branch '${alias}' into ${pullreq.requestBranch}"
}
}
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
}
}) getOrElse NotFound
})
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
params("id").toIntOpt.flatMap { issueId =>
val owner = repository.owner
@@ -528,4 +610,15 @@ trait PullRequestsControllerBase extends ControllerBase {
repository,
hasWritePermission(owner, repoName, context.loginAccount))
}
// TODO: same as gitbucket.core.servlet.CommitLogHook ...
private def createIssueComment(owner: String, repository: String, commit: CommitInfo) = {
StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
if(getIssue(owner, repository, issueId).isDefined){
getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
}
}
}
}
}

View File

@@ -2,14 +2,14 @@ package gitbucket.core.controller
import gitbucket.core.settings.html
import gitbucket.core.model.WebHook
import gitbucket.core.service.{RepositoryService, AccountService, WebHookService}
import gitbucket.core.service.{RepositoryService, AccountService, WebHookService, ProtectedBranchService, CommitStatusService}
import gitbucket.core.service.WebHookService._
import gitbucket.core.util._
import gitbucket.core.util.JGitUtil._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages
import org.eclipse.jgit.api.Git
@@ -18,23 +18,29 @@ import org.eclipse.jgit.lib.ObjectId
class RepositorySettingsController extends RepositorySettingsControllerBase
with RepositoryService with AccountService with WebHookService
with RepositoryService with AccountService with WebHookService with ProtectedBranchService with CommitStatusService
with OwnerAuthenticator with UsersAuthenticator
trait RepositorySettingsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with WebHookService
self: RepositoryService with AccountService with WebHookService with ProtectedBranchService with CommitStatusService
with OwnerAuthenticator with UsersAuthenticator =>
// for repository options
case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean)
case class OptionsForm(repositoryName: String, description: Option[String], isPrivate: Boolean)
val optionsForm = mapping(
"repositoryName" -> trim(label("Repository Name", text(required, maxlength(40), identifier, renameRepositoryName))),
"description" -> trim(label("Description" , optional(text()))),
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
"isPrivate" -> trim(label("Repository Type", boolean()))
)(OptionsForm.apply)
// for default branch
case class DefaultBranchForm(defaultBranch: String)
val defaultBranchForm = mapping(
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100))))
)(DefaultBranchForm.apply)
// for collaborator addition
case class CollaboratorForm(userName: String)
@@ -75,12 +81,10 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Save the repository options.
*/
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
val defaultBranch = if(repository.branchList.isEmpty) "master" else form.defaultBranch
saveRepositoryOptions(
repository.owner,
repository.name,
form.description,
defaultBranch,
repository.repository.parentUserName.map { _ =>
repository.repository.isPrivate
} getOrElse form.isPrivate
@@ -98,14 +102,61 @@ trait RepositorySettingsControllerBase extends ControllerBase {
FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
}
}
// Change repository HEAD
using(Git.open(getRepositoryDir(repository.owner, form.repositoryName))) { git =>
git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + defaultBranch)
}
flash += "info" -> "Repository settings has been updated."
redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
})
/** branch settings */
get("/:owner/:repository/settings/branches")(ownerOnly { repository =>
val protecteions = getProtectedBranchList(repository.owner, repository.name)
html.branches(repository, protecteions, flash.get("info"))
});
/** Update default branch */
post("/:owner/:repository/settings/update_default_branch", defaultBranchForm)(ownerOnly { (form, repository) =>
if(repository.branchList.find(_ == form.defaultBranch).isEmpty){
redirect(s"/${repository.owner}/${repository.name}/settings/options")
} else {
saveRepositoryDefaultBranch(repository.owner, repository.name, form.defaultBranch)
// Change repository HEAD
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + form.defaultBranch)
}
flash += "info" -> "Repository default branch has been updated."
redirect(s"/${repository.owner}/${repository.name}/settings/branches")
}
})
/** Branch protection for branch */
get("/:owner/:repository/settings/branches/:branch")(ownerOnly { repository =>
import gitbucket.core.api._
val branch = params("branch")
if(repository.branchList.find(_ == branch).isEmpty){
redirect(s"/${repository.owner}/${repository.name}/settings/branches")
} else {
val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch))
val lastWeeks = getRecentStatuesContexts(repository.owner, repository.name, org.joda.time.LocalDateTime.now.minusWeeks(1).toDate).toSet
val knownContexts = (lastWeeks ++ protection.status.contexts).toSeq.sortBy(identity)
html.branchprotection(repository, branch, protection, knownContexts, flash.get("info"))
}
})
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
patch("/api/v3/repos/:owner/:repo/branches/:branch")(ownerOnly { repository =>
import gitbucket.core.api._
(for{
branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined
protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection)
} yield {
if(protection.enabled){
enableBranchProtection(repository.owner, repository.name, branch, protection.status.enforcement_level == ApiBranchProtection.Everyone, protection.status.contexts)
} else {
disableBranchProtection(repository.owner, repository.name, branch)
}
JsonFormat(ApiBranch(branch, protection)(RepositoryName(repository)))
}) getOrElse NotFound
})
/**
* Display the Collaborators page.
*/

View File

@@ -14,12 +14,11 @@ import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import gitbucket.core.model.{Account, CommitState, WebHook}
import gitbucket.core.service.CommitStatusService
import gitbucket.core.service.WebHookService._
import gitbucket.core.view
import gitbucket.core.view.helpers
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.{ArchiveCommand, Git}
import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
@@ -34,7 +33,7 @@ import org.scalatra._
class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
with WebHookPullRequestService with WebHookPullRequestReviewCommentService
with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBranchService
/**
* The repository viewer.
@@ -42,7 +41,7 @@ class RepositoryViewerController extends RepositoryViewerControllerBase
trait RepositoryViewerControllerBase extends ControllerBase {
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
with WebHookPullRequestService with WebHookPullRequestReviewCommentService =>
with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBranchService =>
ArchiveCommand.registerFormat("zip", new ZipFormat)
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
@@ -111,6 +110,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
enableRefsLink = params("enableRefsLink").toBoolean,
enableLineBreaks = params("enableLineBreaks").toBoolean,
enableTaskList = params("enableTaskList").toBoolean,
enableAnchor = false,
hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount)
)
})
@@ -221,12 +221,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head)
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
None, JGitUtil.ContentInfo("text", None, Some("UTF-8")))
None, JGitUtil.ContentInfo("text", None, Some("UTF-8")),
protectedBranch)
})
get("/:owner/:repository/edit/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head)
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
@@ -234,7 +237,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
getPathObjectId(git, path, revCommit).map { objectId =>
val paths = path.split("/")
html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last),
JGitUtil.getContentInfo(git, path, objectId))
JGitUtil.getContentInfo(git, path, objectId),
protectedBranch)
} getOrElse NotFound
}
})
@@ -303,7 +307,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
getPathObjectId(git, path, revCommit).flatMap { objectId =>
JGitUtil.getObjectLoaderFromId(git, objectId){ loader =>
contentType = "application/octet-stream"
contentType = FileUtil.getMimeType(path)
response.setContentLength(loader.getSize.toInt)
loader.copyTo(response.outputStream)
()
@@ -324,7 +328,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
if(raw){
// Download (This route is left for backword compatibility)
JGitUtil.getObjectLoaderFromId(git, objectId){ loader =>
contentType = "application/octet-stream"
contentType = FileUtil.getMimeType(path)
response.setContentLength(loader.getSize.toInt)
loader.copyTo(response.outputStream)
()
@@ -485,6 +489,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
* Displays branches.
*/
get("/:owner/:repository/branches")(referrersOnly { repository =>
val protectedBranches = getProtectedBranchList(repository.owner, repository.name).toSet
val branches = JGitUtil.getBranches(
owner = repository.owner,
name = repository.name,
@@ -492,7 +497,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
origin = repository.repository.originUserName.isEmpty
)
.sortBy(br => (br.mergeInfo.isEmpty, br.commitTime))
.map(br => br -> getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId))
.map(br => (br, getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId), protectedBranches.contains(br.name)))
.reverse
html.branches(branches, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
@@ -629,7 +634,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val parentPath = if (path == ".") Nil else path.split("/").toList
// process README.md or README.markdown
val readme = files.find { file =>
readmeFiles.contains(file.name.toLowerCase)
!file.isDirectory && readmeFiles.contains(file.name.toLowerCase)
}.map { file =>
val path = (file.name :: parentPath.reverse).reverse
path -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId(
@@ -683,7 +688,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
headName, loginAccount.fullName, loginAccount.mailAddress, message)
inserter.flush()
inserter.release()
inserter.close()
// update refs
val refUpdate = git.getRepository.updateRef(headName)

View File

@@ -5,7 +5,7 @@ import gitbucket.core.service._
import gitbucket.core.util.{StringUtil, ControlUtil, ReferrerAuthenticator, Implicits}
import ControlUtil._
import Implicits._
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
class SearchController extends SearchControllerBase
with RepositoryService with AccountService with ActivityService with RepositorySearchService with IssuesService with ReferrerAuthenticator

View File

@@ -5,7 +5,7 @@ import gitbucket.core.service.{AccountService, SystemSettingsService}
import gitbucket.core.util.AdminAuthenticator
import gitbucket.core.ssh.SshServer
import SystemSettingsService._
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
class SystemSettingsController extends SystemSettingsControllerBase
with AccountService with AdminAuthenticator

View File

@@ -7,7 +7,7 @@ import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.StringUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
import org.scalatra.i18n.Messages
import org.apache.commons.io.FileUtils

View File

@@ -7,7 +7,7 @@ import gitbucket.core.util.StringUtil._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
import org.eclipse.jgit.api.Git
import org.scalatra.i18n.Messages
@@ -38,7 +38,9 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki")(referrersOnly { repository =>
getWikiPage(repository.owner, repository.name, "Home").map { page =>
html.page("Home", page, getWikiPageList(repository.owner, repository.name),
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount),
getWikiPage(repository.owner, repository.name, "_Sidebar"),
getWikiPage(repository.owner, repository.name, "_Footer"))
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit")
})
@@ -47,7 +49,9 @@ trait WikiControllerBase extends ControllerBase {
getWikiPage(repository.owner, repository.name, pageName).map { page =>
html.page(pageName, page, getWikiPageList(repository.owner, repository.name),
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount),
getWikiPage(repository.owner, repository.name, "_Sidebar"),
getWikiPage(repository.owner, repository.name, "_Footer"))
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
})
@@ -124,7 +128,11 @@ trait WikiControllerBase extends ControllerBase {
updateLastActivityDate(repository.owner, repository.name)
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
}
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
if(notReservedPageName(form.pageName)) {
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
} else {
redirect(s"/${repository.owner}/${repository.name}/wiki")
}
}
})
@@ -140,7 +148,11 @@ trait WikiControllerBase extends ControllerBase {
updateLastActivityDate(repository.owner, repository.name)
recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
if(notReservedPageName(form.pageName)) {
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
} else {
redirect(s"/${repository.owner}/${repository.name}/wiki")
}
}
})
@@ -186,13 +198,15 @@ trait WikiControllerBase extends ControllerBase {
override def validate(name: String, value: String, messages: Messages): Option[String] =
if(value.exists("\\/:*?\"<>|".contains(_))){
Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){
} else if(notReservedPageName(value) && (value.startsWith("_") || value.startsWith("-"))){
Some(s"${name} starts with invalid character.")
} else {
None
}
}
private def notReservedPageName(value: String) = ! (Array[String]("_Sidebar","_Footer") contains value)
private def conflictForNew: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
targetWikiPage.map { _ =>

View File

@@ -26,12 +26,16 @@ protected[model] trait TemplateComponent { self: Profile =>
trait LabelTemplate extends BasicTemplate { self: Table[_] =>
val labelId = column[Int]("LABEL_ID")
val labelName = column[String]("LABEL_NAME")
def byLabel(owner: String, repository: String, labelId: Int) =
byRepository(owner, repository) && (this.labelId === labelId.bind)
def byLabel(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) =
byRepository(userName, repositoryName) && (this.labelId === labelId)
def byLabel(owner: String, repository: String, labelName: String) =
byRepository(userName, repositoryName) && (this.labelName === labelName.bind)
}
trait MilestoneTemplate extends BasicTemplate { self: Table[_] =>
@@ -54,4 +58,9 @@ protected[model] trait TemplateComponent { self: Profile =>
byRepository(userName, repositoryName) && (this.commitId === commitId)
}
trait BranchTemplate extends BasicTemplate{ self: Table[_] =>
val branch = column[String]("BRANCH")
def byBranch(owner: String, repository: String, branchName: String) = byRepository(owner, repository) && (branch === branchName.bind)
def byBranch(owner: Column[String], repository: Column[String], branchName: Column[String]) = byRepository(owner, repository) && (this.branch === branchName)
}
}

View File

@@ -19,7 +19,7 @@ trait CommitStatusComponent extends TemplateComponent { self: Profile =>
val creator = column[String]("CREATOR")
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
val updatedDate = column[java.util.Date]("UPDATED_DATE")
def * = (commitStatusId, userName, repositoryName, commitId, context, state, targetUrl, description, creator, registeredDate, updatedDate) <> (CommitStatus.tupled, CommitStatus.unapply)
def * = (commitStatusId, userName, repositoryName, commitId, context, state, targetUrl, description, creator, registeredDate, updatedDate) <> ((CommitStatus.apply _).tupled, CommitStatus.unapply)
def byPrimaryKey(id: Int) = commitStatusId === id.bind
}
}
@@ -38,7 +38,20 @@ case class CommitStatus(
registeredDate: java.util.Date,
updatedDate: java.util.Date
)
object CommitStatus {
def pending(owner: String, repository: String, context: String) = CommitStatus(
commitStatusId = 0,
userName = owner,
repositoryName = repository,
commitId = "",
context = context,
state = CommitState.PENDING,
targetUrl = None,
description = Some("Waiting for status to be reported"),
creator = "",
registeredDate = new java.util.Date(),
updatedDate = new java.util.Date())
}
sealed abstract class CommitState(val name: String)

View File

@@ -7,7 +7,7 @@ trait LabelComponent extends TemplateComponent { self: Profile =>
class Labels(tag: Tag) extends Table[Label](tag, "LABEL") with LabelTemplate {
override val labelId = column[Int]("LABEL_ID", O AutoInc)
val labelName = column[String]("LABEL_NAME")
override val labelName = column[String]("LABEL_NAME")
val color = column[String]("COLOR")
def * = (userName, repositoryName, labelId, labelName, color) <> (Label.tupled, Label.unapply)

View File

@@ -50,5 +50,6 @@ trait CoreProfile extends ProfileProvider with Profile
with WebHookComponent
with WebHookEventComponent
with PluginComponent
with ProtectedBranchComponent
object Profile extends CoreProfile

View File

@@ -0,0 +1,37 @@
package gitbucket.core.model
import scala.slick.lifted.MappedTo
import scala.slick.jdbc._
trait ProtectedBranchComponent extends TemplateComponent { self: Profile =>
import profile.simple._
import self._
lazy val ProtectedBranches = TableQuery[ProtectedBranches]
class ProtectedBranches(tag: Tag) extends Table[ProtectedBranch](tag, "PROTECTED_BRANCH") with BranchTemplate {
val statusCheckAdmin = column[Boolean]("STATUS_CHECK_ADMIN")
def * = (userName, repositoryName, branch, statusCheckAdmin) <> (ProtectedBranch.tupled, ProtectedBranch.unapply)
def byPrimaryKey(userName: String, repositoryName: String, branch: String) = byBranch(userName, repositoryName, branch)
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], branch: Column[String]) = byBranch(userName, repositoryName, branch)
}
lazy val ProtectedBranchContexts = TableQuery[ProtectedBranchContexts]
class ProtectedBranchContexts(tag: Tag) extends Table[ProtectedBranchContext](tag, "PROTECTED_BRANCH_REQUIRE_CONTEXT") with BranchTemplate {
val context = column[String]("CONTEXT")
def * = (userName, repositoryName, branch, context) <> (ProtectedBranchContext.tupled, ProtectedBranchContext.unapply)
}
}
case class ProtectedBranch(
userName: String,
repositoryName: String,
branch: String,
statusCheckAdmin: Boolean)
case class ProtectedBranchContext(
userName: String,
repositoryName: String,
branch: String,
context: String)

View File

@@ -67,6 +67,16 @@ trait Plugin {
*/
def repositoryRoutings(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[GitRepositoryRouting] = Nil
/**
* Override to add receive hooks.
*/
val receiveHooks: Seq[ReceiveHook] = Nil
/**
* Override to add receive hooks.
*/
def receiveHooks(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[ReceiveHook] = Nil
/**
* This method is invoked in initialization of plugin system.
* Register plugin functionality to PluginRegistry.
@@ -87,6 +97,9 @@ trait Plugin {
(repositoryRoutings ++ repositoryRoutings(registry, context, settings)).foreach { routing =>
registry.addRepositoryRouting(routing)
}
(receiveHooks ++ receiveHooks(registry, context, settings)).foreach { receiveHook =>
registry.addReceiveHook(receiveHook)
}
}
/**

View File

@@ -6,6 +6,7 @@ import javax.servlet.ServletContext
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import gitbucket.core.controller.{Context, ControllerBase}
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.util.ControlUtil._
@@ -29,6 +30,8 @@ class PluginRegistry {
"md" -> MarkdownRenderer, "markdown" -> MarkdownRenderer
)
private val repositoryRoutings = new ListBuffer[GitRepositoryRouting]
private val receiveHooks = new ListBuffer[ReceiveHook]
receiveHooks += new ProtectedBranchReceiveHook()
def addPlugin(pluginInfo: PluginInfo): Unit = {
plugins += pluginInfo
@@ -98,6 +101,12 @@ class PluginRegistry {
}
}
def addReceiveHook(commitHook: ReceiveHook): Unit = {
receiveHooks += commitHook
}
def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.toSeq
private case class GlobalAction(
method: String,
path: String,
@@ -169,7 +178,7 @@ object PluginRegistry {
))
} catch {
case e: Exception => {
case e: Throwable => {
logger.error(s"Error during plugin initialization", e)
}
}

View File

@@ -0,0 +1,15 @@
package gitbucket.core.plugin
import gitbucket.core.model.Profile._
import org.eclipse.jgit.transport.{ReceivePack, ReceiveCommand}
import profile.simple._
trait ReceiveHook {
def preReceive(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)
(implicit session: Session): Option[String] = None
def postReceive(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)
(implicit session: Session): Unit = ()
}

View File

@@ -7,46 +7,51 @@ import gitbucket.core.model.{CommitState, CommitStatus, Account}
import gitbucket.core.util.Implicits._
import gitbucket.core.util.StringUtil._
import gitbucket.core.service.RepositoryService.RepositoryInfo
import org.joda.time.LocalDateTime
import gitbucket.core.model.Profile.dateColumnType
trait CommitStatusService {
/** insert or update */
def createCommitStatus(userName: String, repositoryName: String, sha:String, context:String, state:CommitState, targetUrl:Option[String], description:Option[String], now:java.util.Date, creator:Account)(implicit s: Session): Int =
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) && t.context===context.bind )
def createCommitStatus(userName: String, repositoryName: String, sha: String, context: String, state: CommitState,
targetUrl: Option[String], description: Option[String], now: java.util.Date, creator: Account)(implicit s: Session): Int =
CommitStatuses
.filter(t => t.byCommit(userName, repositoryName, sha) && t.context === context.bind )
.map(_.commitStatusId).firstOption match {
case Some(id:Int) => {
CommitStatuses.filter(_.byPrimaryKey(id)).map{
t => (t.state , t.targetUrl , t.updatedDate , t.creator, t.description)
}.update( (state, targetUrl, now, creator.userName, description) )
id
}
case None => (CommitStatuses returning CommitStatuses.map(_.commitStatusId)) += CommitStatus(
userName = userName,
repositoryName = repositoryName,
commitId = sha,
context = context,
state = state,
targetUrl = targetUrl,
description = description,
creator = creator.userName,
registeredDate = now,
updatedDate = now)
case Some(id: Int) => {
CommitStatuses.filter(_.byPrimaryKey(id)).map { t =>
(t.state , t.targetUrl , t.updatedDate , t.creator, t.description)
}.update((state, targetUrl, now, creator.userName, description))
id
}
case None => (CommitStatuses returning CommitStatuses.map(_.commitStatusId)) += CommitStatus(
userName = userName,
repositoryName = repositoryName,
commitId = sha,
context = context,
state = state,
targetUrl = targetUrl,
description = description,
creator = creator.userName,
registeredDate = now,
updatedDate = now)
}
def getCommitStatus(userName: String, repositoryName: String, id: Int)(implicit s: Session) :Option[CommitStatus] =
CommitStatuses.filter(t => t.byPrimaryKey(id) && t.byRepository(userName, repositoryName)).firstOption
def getCommitStatus(userName: String, repositoryName: String, sha: String, context: String)(implicit s: Session) :Option[CommitStatus] =
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) && t.context===context.bind ).firstOption
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) && t.context === context.bind).firstOption
def getCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session) :List[CommitStatus] =
byCommitStatues(userName, repositoryName, sha).list
def getRecentStatuesContexts(userName: String, repositoryName: String, time: java.util.Date)(implicit s: Session) :List[String] =
CommitStatuses.filter(t => t.byRepository(userName, repositoryName)).filter(t => t.updatedDate > time.bind).groupBy(_.context).map(_._1).list
def getCommitStatuesWithCreator(userName: String, repositoryName: String, sha: String)(implicit s: Session) :List[(CommitStatus, Account)] =
byCommitStatues(userName, repositoryName, sha).innerJoin(Accounts)
.filter{ case (t,a) => t.creator === a.userName }.list
byCommitStatues(userName, repositoryName, sha).innerJoin(Accounts).filter { case (t, a) => t.creator === a.userName }.list
protected def byCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session) =
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) ).sortBy(_.updatedDate desc)
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha)).sortBy(_.updatedDate desc)
}

View File

@@ -12,6 +12,9 @@ trait LabelsService {
def getLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Option[Label] =
Labels.filter(_.byPrimaryKey(owner, repository, labelId)).firstOption
def getLabel(owner: String, repository: String, labelName: String)(implicit s: Session): Option[Label] =
Labels.filter(_.byLabel(owner, repository, labelName)).firstOption
def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Int =
Labels returning Labels.map(_.labelId) += Label(
userName = owner,

View File

@@ -10,10 +10,9 @@ import org.eclipse.jgit.merge.MergeStrategy
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.transport.RefSpec
import org.eclipse.jgit.errors.NoMergeBaseException
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent, Repository}
import org.eclipse.jgit.revwalk.RevWalk
trait MergeService {
import MergeService._
/**
@@ -52,26 +51,30 @@ trait MergeService {
/**
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
*/
def checkConflict(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
val remoteRefName = s"refs/heads/${branch}"
val tmpRefName = s"refs/merge-check/${userName}/${branch}"
def tryMergeRemote(localUserName: String, localRepositoryName: String, localBranch: String,
remoteUserName: String, remoteRepositoryName: String, remoteBranch: String): Option[(ObjectId, ObjectId, ObjectId)] = {
using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git =>
val remoteRefName = s"refs/heads/${remoteBranch}"
val tmpRefName = s"refs/remote-temp/${remoteUserName}/${remoteRepositoryName}/${remoteBranch}"
val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
try {
// fetch objects from origin repository branch
git.fetch
.setRemote(getRepositoryDir(userName, repositoryName).toURI.toString)
.setRemote(getRepositoryDir(remoteUserName, remoteRepositoryName).toURI.toString)
.setRefSpecs(refSpec)
.call
// merge conflict check
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${requestBranch}")
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${localBranch}")
val mergeTip = git.getRepository.resolve(tmpRefName)
try {
!merger.merge(mergeBaseTip, mergeTip)
if(merger.merge(mergeBaseTip, mergeTip)){
Some((merger.getResultTreeId, mergeBaseTip, mergeTip))
} else {
None
}
} catch {
case e: NoMergeBaseException => true
case e: NoMergeBaseException => None
}
} finally {
val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
@@ -80,8 +83,54 @@ trait MergeService {
}
}
}
/**
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
*/
def checkConflict(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean =
tryMergeRemote(userName, repositoryName, branch, requestUserName, requestRepositoryName, requestBranch).isEmpty
def pullRemote(localUserName: String, localRepositoryName: String, localBranch: String,
remoteUserName: String, remoteRepositoryName: String, remoteBranch: String,
loginAccount: Account, message: String): Option[ObjectId] = {
tryMergeRemote(localUserName, localRepositoryName, localBranch, remoteUserName, remoteRepositoryName, remoteBranch).map{ case (newTreeId, oldBaseId, oldHeadId) =>
using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git =>
val committer = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
val newCommit = Util.createMergeCommit(git.getRepository, newTreeId, committer, message, Seq(oldBaseId, oldHeadId))
Util.updateRefs(git.getRepository, s"refs/heads/${localBranch}", newCommit, false, committer, Some("merge"))
}
oldBaseId
}
}
}
object MergeService{
object Util{
// return treeId
def createMergeCommit(repository: Repository, treeId: ObjectId, committer: PersonIdent, message: String, parents: Seq[ObjectId]): ObjectId = {
val mergeCommit = new CommitBuilder()
mergeCommit.setTreeId(treeId)
mergeCommit.setParentIds(parents:_*)
mergeCommit.setAuthor(committer)
mergeCommit.setCommitter(committer)
mergeCommit.setMessage(message)
// insertObject and got mergeCommit Object Id
val inserter = repository.newObjectInserter
val mergeCommitId = inserter.insert(mergeCommit)
inserter.flush()
inserter.close()
mergeCommitId
}
def updateRefs(repository: Repository, ref: String, newObjectId: ObjectId, force: Boolean, committer: PersonIdent, refLogMessage: Option[String] = None):Unit = {
// update refs
val refUpdate = repository.updateRef(ref)
refUpdate.setNewObjectId(newObjectId)
refUpdate.setForceUpdate(force)
refUpdate.setRefLogIdent(committer)
refLogMessage.map(refUpdate.setRefLogMessage(_, true))
refUpdate.update()
}
}
case class MergeCacheInfo(git:Git, branch:String, issueId:Int){
val repository = git.getRepository
val mergedBranchName = s"refs/pull/${issueId}/merge"
@@ -90,17 +139,17 @@ object MergeService{
lazy val mergeTip = repository.resolve(s"refs/pull/${issueId}/head")
def checkConflictCache(): Option[Boolean] = {
Option(repository.resolve(mergedBranchName)).flatMap{ merged =>
if(parseCommit( merged ).getParents().toSet == Set( mergeBaseTip, mergeTip )){
if(parseCommit(merged).getParents().toSet == Set( mergeBaseTip, mergeTip )){
// merged branch exists
Some(false)
}else{
} else {
None
}
}.orElse(Option(repository.resolve(conflictedBranchName)).flatMap{ conflicted =>
if(parseCommit( conflicted ).getParents().toSet == Set( mergeBaseTip, mergeTip )){
if(parseCommit(conflicted).getParents().toSet == Set( mergeBaseTip, mergeTip )){
// conflict branch exists
Some(true)
}else{
} else {
None
}
})
@@ -120,17 +169,12 @@ object MergeService{
def updateBranch(treeId:ObjectId, message:String, branchName:String){
// creates merge commit
val mergeCommitId = createMergeCommit(treeId, committer, message)
// update refs
val refUpdate = repository.updateRef(branchName)
refUpdate.setNewObjectId(mergeCommitId)
refUpdate.setForceUpdate(true)
refUpdate.setRefLogIdent(committer)
refUpdate.update()
Util.updateRefs(repository, branchName, mergeCommitId, true, committer)
}
if(!conflicted){
updateBranch(merger.getResultTreeId, s"Merge ${mergeTip.name} into ${mergeBaseTip.name}", mergedBranchName)
git.branchDelete().setForce(true).setBranchNames(conflictedBranchName).call()
}else{
} else {
updateBranch(mergeTipCommit.getTree().getId(), s"can't merge ${mergeTip.name} into ${mergeBaseTip.name}", conflictedBranchName)
git.branchDelete().setForce(true).setBranchNames(mergedBranchName).call()
}
@@ -145,28 +189,12 @@ object MergeService{
// creates merge commit
val mergeCommitId = createMergeCommit(mergeResultCommit.getTree().getId(), committer, message)
// update refs
val refUpdate = repository.updateRef(s"refs/heads/${branch}")
refUpdate.setNewObjectId(mergeCommitId)
refUpdate.setForceUpdate(false)
refUpdate.setRefLogIdent(committer)
refUpdate.setRefLogMessage("merged", true)
refUpdate.update()
Util.updateRefs(repository, s"refs/heads/${branch}", mergeCommitId, false, committer, Some("merged"))
}
// return treeId
private def createMergeCommit(treeId:ObjectId, committer:PersonIdent, message:String) = {
val mergeCommit = new CommitBuilder()
mergeCommit.setTreeId(treeId)
mergeCommit.setParentIds(Array[ObjectId](mergeBaseTip, mergeTip): _*)
mergeCommit.setAuthor(committer)
mergeCommit.setCommitter(committer)
mergeCommit.setMessage(message)
// insertObject and got mergeCommit Object Id
val inserter = repository.newObjectInserter
val mergeCommitId = inserter.insert(mergeCommit)
inserter.flush()
inserter.release()
mergeCommitId
}
private def createMergeCommit(treeId: ObjectId, committer: PersonIdent, message: String) =
Util.createMergeCommit(repository, treeId, committer, message, Seq[ObjectId](mergeBaseTip, mergeTip))
private def parseCommit(id:ObjectId) = using(new RevWalk( repository ))(_.parseCommit(id))
}
}

View File

@@ -0,0 +1,121 @@
package gitbucket.core.service
import gitbucket.core.model._
import gitbucket.core.model.Profile._
import gitbucket.core.plugin.ReceiveHook
import profile.simple._
import org.eclipse.jgit.transport.{ReceivePack, ReceiveCommand}
trait ProtectedBranchService {
import ProtectedBranchService._
private def getProtectedBranchInfoOpt(owner: String, repository: String, branch: String)(implicit session: Session): Option[ProtectedBranchInfo] =
ProtectedBranches
.leftJoin(ProtectedBranchContexts)
.on{ case (pb, c) => pb.byBranch(c.userName, c.repositoryName, c.branch) }
.map{ case (pb, c) => pb -> c.context.? }
.filter(_._1.byPrimaryKey(owner, repository, branch))
.list
.groupBy(_._1)
.map(p => p._1 -> p._2.flatMap(_._2))
.map{ case (t1, contexts) =>
new ProtectedBranchInfo(t1.userName, t1.repositoryName, true, contexts, t1.statusCheckAdmin)
}.headOption
def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit session: Session): ProtectedBranchInfo =
getProtectedBranchInfoOpt(owner, repository, branch).getOrElse(ProtectedBranchInfo.disabled(owner, repository))
def getProtectedBranchList(owner: String, repository: String)(implicit session: Session): List[String] =
ProtectedBranches.filter(_.byRepository(owner, repository)).map(_.branch).list
def enableBranchProtection(owner: String, repository: String, branch:String, includeAdministrators: Boolean, contexts: Seq[String])
(implicit session: Session): Unit = {
disableBranchProtection(owner, repository, branch)
ProtectedBranches.insert(new ProtectedBranch(owner, repository, branch, includeAdministrators && contexts.nonEmpty))
contexts.map{ context =>
ProtectedBranchContexts.insert(new ProtectedBranchContext(owner, repository, branch, context))
}
}
def disableBranchProtection(owner: String, repository: String, branch:String)(implicit session: Session): Unit =
ProtectedBranches.filter(_.byPrimaryKey(owner, repository, branch)).delete
}
object ProtectedBranchService {
class ProtectedBranchReceiveHook extends ReceiveHook with ProtectedBranchService {
override def preReceive(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)
(implicit session: Session): Option[String] = {
val branch = command.getRefName.stripPrefix("refs/heads/")
if(branch != command.getRefName){
getProtectedBranchInfo(owner, repository, branch).getStopReason(receivePack.isAllowNonFastForwards, command, pusher)
} else {
None
}
}
}
case class ProtectedBranchInfo(
owner: String,
repository: String,
enabled: Boolean,
/**
* Require status checks to pass before merging
* Choose which status checks must pass before branches can be merged into test.
* When enabled, commits must first be pushed to another branch,
* then merged or pushed directly to test after status checks have passed.
*/
contexts: Seq[String],
/**
* Include administrators
* Enforce required status checks for repository administrators.
*/
includeAdministrators: Boolean) extends AccountService with CommitStatusService {
def isAdministrator(pusher: String)(implicit session: Session): Boolean =
pusher == owner || getGroupMembers(owner).filter(gm => gm.userName == pusher && gm.isManager).nonEmpty
/**
* Can't be force pushed
* Can't be deleted
* Can't have changes merged into them until required status checks pass
*/
def getStopReason(isAllowNonFastForwards: Boolean, command: ReceiveCommand, pusher: String)(implicit session: Session): Option[String] = {
if(enabled){
command.getType() match {
case ReceiveCommand.Type.UPDATE|ReceiveCommand.Type.UPDATE_NONFASTFORWARD if isAllowNonFastForwards =>
Some("Cannot force-push to a protected branch")
case ReceiveCommand.Type.UPDATE|ReceiveCommand.Type.UPDATE_NONFASTFORWARD if needStatusCheck(pusher) =>
unSuccessedContexts(command.getNewId.name) match {
case s if s.size == 1 => Some(s"""Required status check "${s.toSeq(0)}" is expected""")
case s if s.size >= 1 => Some(s"${s.size} of ${contexts.size} required status checks are expected")
case _ => None
}
case ReceiveCommand.Type.DELETE =>
Some("Cannot delete a protected branch")
case _ => None
}
}else{
None
}
}
def unSuccessedContexts(sha1: String)(implicit session: Session): Set[String] = if(contexts.isEmpty){
Set.empty
} else {
contexts.toSet -- getCommitStatues(owner, repository, sha1).filter(_.state == CommitState.SUCCESS).map(_.context).toSet
}
def needStatusCheck(pusher: String)(implicit session: Session): Boolean = pusher match {
case _ if !enabled => false
case _ if contexts.isEmpty => false
case _ if includeAdministrators => true
case p if isAdministrator(p) => false
case _ => true
}
}
object ProtectedBranchInfo{
def disabled(owner: String, repository: String): ProtectedBranchInfo = ProtectedBranchInfo(owner, repository, false, Nil, false)
}
}

View File

@@ -1,6 +1,6 @@
package gitbucket.core.service
import gitbucket.core.model.{Account, Issue, PullRequest, WebHook}
import gitbucket.core.model.{Account, Issue, PullRequest, WebHook, CommitStatus, CommitState}
import gitbucket.core.model.Profile._
import gitbucket.core.util.JGitUtil
import profile.simple._
@@ -145,4 +145,29 @@ object PullRequestService {
case class PullRequestCount(userName: String, count: Int)
case class MergeStatus(
hasConflict: Boolean,
commitStatues:List[CommitStatus],
branchProtection: ProtectedBranchService.ProtectedBranchInfo,
branchIsOutOfDate: Boolean,
hasUpdatePermission: Boolean,
needStatusCheck: Boolean,
hasMergePermission: Boolean,
commitIdTo: String){
val statuses: List[CommitStatus] =
commitStatues ++ (branchProtection.contexts.toSet -- commitStatues.map(_.context).toSet).map(CommitStatus.pending(branchProtection.owner, branchProtection.repository, _))
val hasRequiredStatusProblem = needStatusCheck && branchProtection.contexts.exists(context => statuses.find(_.context == context).map(_.state) != Some(CommitState.SUCCESS))
val hasProblem = hasRequiredStatusProblem || hasConflict || (!statuses.isEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
val canUpdate = branchIsOutOfDate && !hasConflict
val canMerge = hasMergePermission && !hasConflict && !hasRequiredStatusProblem
lazy val commitStateSummary:(CommitState, String) = {
val stateMap = statuses.groupBy(_.state)
val state = CommitState.combine(stateMap.keySet)
val summary = stateMap.map{ case (keyState, states) => states.size+" "+keyState.name }.mkString(", ")
state -> summary
}
lazy val statusesAndRequired:List[(CommitStatus, Boolean)] = statuses.map{ s => s -> branchProtection.contexts.exists(_==s.context) }
lazy val isAllSuccess = commitStateSummary._1==CommitState.SUCCESS
}
}

View File

@@ -79,8 +79,8 @@ trait RepositorySearchService { self: IssuesService =>
}
}
}
treeWalk.release
revWalk.release
treeWalk.close()
revWalk.close()
list.toList
}

View File

@@ -46,18 +46,20 @@ trait RepositoryService { self: AccountService =>
(Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val webHookEvents = WebHookEvents .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val commitComments = CommitComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val commitStatuses = CommitStatuses.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val collaborators = Collaborators .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val webHookEvents = WebHookEvents .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val commitComments = CommitComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val commitStatuses = CommitStatuses .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val collaborators = Collaborators .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val protectedBranches = ProtectedBranches .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val protectedBranchContexts = ProtectedBranchContexts.filter(_.byRepository(oldUserName, oldRepositoryName)).list
Repositories.filter { t =>
(t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind)
@@ -90,11 +92,13 @@ trait RepositoryService { self: AccountService =>
}
)} :_*)
PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitStatuses.insertAll(commitStatuses.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitComments .insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitStatuses .insertAll(commitStatuses.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
ProtectedBranches .insertAll(protectedBranches.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
ProtectedBranchContexts.insertAll(protectedBranchContexts.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
// Update source repository of pull requests
PullRequests.filter { t =>
@@ -309,11 +313,17 @@ trait RepositoryService { self: AccountService =>
/**
* Save repository options.
*/
def saveRepositoryOptions(userName: String, repositoryName: String,
description: Option[String], defaultBranch: String, isPrivate: Boolean)(implicit s: Session): Unit =
def saveRepositoryOptions(userName: String, repositoryName: String,
description: Option[String], isPrivate: Boolean)(implicit s: Session): Unit =
Repositories.filter(_.byRepository(userName, repositoryName))
.map { r => (r.description.?, r.defaultBranch, r.isPrivate, r.updatedDate) }
.update (description, defaultBranch, isPrivate, currentDate)
.map { r => (r.description.?, r.isPrivate, r.updatedDate) }
.update (description, isPrivate, currentDate)
def saveRepositoryDefaultBranch(userName: String, repositoryName: String,
defaultBranch: String)(implicit s: Session): Unit =
Repositories.filter(_.byRepository(userName, repositoryName))
.map { r => r.defaultBranch }
.update (defaultBranch)
/**
* Add collaborator to the repository.

View File

@@ -93,7 +93,7 @@ trait WikiService {
def getWikiPageList(owner: String, repository: String): List[String] = {
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
JGitUtil.getFileList(git, "master", ".")
.filter(_.name.endsWith(".md"))
.filter(_.name.endsWith(".md")).filterNot(_.name.startsWith("_"))
.map(_.name.stripSuffix(".md"))
.sortBy(x => x)
}

View File

@@ -18,9 +18,10 @@ import gitbucket.core.util.Directory
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 GitBucket version.
*/
val versions = Seq(
new Version(3, 11),
new Version(3, 10),
new Version(3, 9),
new Version(3, 8),
@@ -155,7 +156,7 @@ object AutoUpdate {
)
/**
* The head version of BitBucket.
* The head version of GitBucket.
*/
val headVersion = versions.head

View File

@@ -111,13 +111,21 @@ import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: Session)
extends PostReceiveHook with PreReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService
with WebHookPullRequestService {
with WebHookPullRequestService with ProtectedBranchService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
private var existIds: Seq[String] = Nil
def onPreReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
try {
commands.asScala.foreach { command =>
// call pre-commit hook
PluginRegistry().getReceiveHooks
.flatMap(_.preReceive(owner, repository, receivePack, command, pusher))
.headOption.foreach { error =>
command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, error)
}
}
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
existIds = JGitUtil.getAllCommitIds(git)
}
@@ -207,6 +215,9 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
newId = command.getNewId(), oldId = command.getOldId())
}
}
// call post-commit hook
PluginRegistry().getReceiveHooks.foreach(_.postReceive(owner, repository, receivePack, command, pusher))
}
}
// update repository last modified time.

View File

@@ -23,10 +23,10 @@ object GitCommand {
abstract class GitCommand() extends Command {
private val logger = LoggerFactory.getLogger(classOf[GitCommand])
protected var err: OutputStream = null
protected var in: InputStream = null
protected var out: OutputStream = null
protected var callback: ExitCallback = null
@volatile protected var err: OutputStream = null
@volatile protected var in: InputStream = null
@volatile protected var out: OutputStream = null
@volatile protected var callback: ExitCallback = null
protected def runTask(user: String)(implicit session: Session): Unit

View File

@@ -1,8 +1,6 @@
package gitbucket.core.util
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk
import scala.util.control.Exception._
import scala.language.reflectiveCalls
@@ -31,12 +29,6 @@ object ControlUtil {
git2.getRepository.close()
}
def using[T](revWalk: RevWalk)(f: RevWalk => T): T =
try f(revWalk) finally revWalk.release()
def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T =
try f(treeWalk) finally treeWalk.release()
def ignore[T](f: => Unit): Unit = try {
f
} catch {

View File

@@ -110,6 +110,8 @@ object JGitUtil {
newIsImage: Boolean,
oldObjectId: Option[String],
newObjectId: Option[String],
oldMode: String,
newMode: String,
tooLarge: Boolean
)
@@ -515,6 +517,8 @@ object JGitUtil {
newIsImage = newIsImage,
oldObjectId = None,
newObjectId = Option(treeWalk.getObjectId(0)).map(_.name),
oldMode = treeWalk.getFileMode(0).toString,
newMode = treeWalk.getFileMode(0).toString,
tooLarge = false
)
} else {
@@ -528,6 +532,8 @@ object JGitUtil {
newIsImage = newIsImage,
oldObjectId = None,
newObjectId = Option(treeWalk.getObjectId(0)).map(_.name),
oldMode = treeWalk.getFileMode(0).toString,
newMode = treeWalk.getFileMode(0).toString,
tooLarge = false
)
}))
@@ -562,6 +568,8 @@ object JGitUtil {
newIsImage = false,
oldObjectId = Option(diff.getOldId).map(_.name),
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = true
)
} else {
@@ -578,6 +586,8 @@ object JGitUtil {
newIsImage = newIsImage,
oldObjectId = Option(diff.getOldId).map(_.name),
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = false
)
} else {
@@ -591,6 +601,8 @@ object JGitUtil {
newIsImage = newIsImage,
oldObjectId = Option(diff.getOldId).map(_.name),
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = false
)
}
@@ -629,7 +641,7 @@ object JGitUtil {
def initRepository(dir: java.io.File): Unit =
using(new RepositoryBuilder().setGitDir(dir).setBare.build){ repository =>
repository.create
repository.create(true)
setReceivePack(repository)
}
@@ -688,7 +700,7 @@ object JGitUtil {
val newHeadId = inserter.insert(newCommit)
inserter.flush()
inserter.release()
inserter.close()
val refUpdate = git.getRepository.updateRef(ref)
refUpdate.setNewObjectId(newHeadId)

View File

@@ -1,6 +1,6 @@
package gitbucket.core.util
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
import org.scalatra.i18n.Messages
trait Validations {

View File

@@ -33,28 +33,30 @@ object Markdown {
enableTaskList: Boolean = false,
hasWritePermission: Boolean = false,
pages: List[String] = Nil)(implicit context: Context): String = {
// escape issue id
val s = if(enableRefsLink){
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
} else markdown
// escape task list
val source = if(enableTaskList){
escapeTaskList(s)
} else s
val source = if(enableTaskList) escapeTaskList(markdown) else markdown
val options = new Options()
options.setSanitize(true)
options.setBreaks(enableLineBreaks)
val renderer = new GitBucketMarkedRenderer(options, repository, enableWikiLink, enableRefsLink, enableAnchor, enableTaskList, hasWritePermission, pages)
val renderer = new GitBucketMarkedRenderer(options, repository,
enableWikiLink, enableRefsLink, enableAnchor, enableTaskList, hasWritePermission, pages)
Marked.marked(source, options, renderer)
}
/**
* Extends markedj Renderer for GitBucket
*/
class GitBucketMarkedRenderer(options: Options, repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean, enableAnchor: Boolean, enableTaskList: Boolean, hasWritePermission: Boolean,
class GitBucketMarkedRenderer(options: Options,
repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
enableRefsLink: Boolean,
enableAnchor: Boolean,
enableTaskList: Boolean,
hasWritePermission: Boolean,
pages: List[String])
(implicit val context: Context) extends Renderer(options) with LinkConverter with RequestCache {
@@ -62,11 +64,14 @@ object Markdown {
val id = generateAnchorName(text)
val out = new StringBuilder()
out.append("<h" + level + " id=\"" + options.getHeaderPrefix + id + "\" class=\"markdown-head\">")
out.append("<h" + level + " id=\"" + options.getHeaderPrefix + id + "\"")
if(enableAnchor){
out.append("<a class=\"markdown-anchor-link\" href=\"#" + id + "\"></a>")
out.append(" class=\"markdown-head\">")
out.append("<a class=\"markdown-anchor-link\" href=\"#" + id + "\"><span class=\"octicon octicon-link\"></span></a>")
out.append("<a class=\"markdown-anchor\" name=\"" + id + "\"></a>")
} else {
out.append(">")
}
out.append(text)
@@ -83,28 +88,27 @@ object Markdown {
var listType: String = null
if (ordered) {
listType = "ol"
}
else {
} else {
listType = "ul"
}
if(body.contains("""class="task-list-item-checkbox"""")){
return "<" + listType + " class=\"task-list\">\n" + body + "</" + listType + ">\n"
"<" + listType + " class=\"task-list\">\n" + body + "</" + listType + ">\n"
} else {
return "<" + listType + ">\n" + body + "</" + listType + ">\n"
"<" + listType + ">\n" + body + "</" + listType + ">\n"
}
}
override def listitem(text: String): String = {
if(text.contains("""class="task-list-item-checkbox" """)){
return "<li class=\"task-list-item\">" + text + "</li>\n"
"<li class=\"task-list-item\">" + text + "</li>\n"
} else {
return "<li>" + text + "</li>\n"
"<li>" + text + "</li>\n"
}
}
override def text(text: String): String = {
// convert commit id and username to link.
val t1 = if(enableRefsLink) convertRefsLinks(text, repository, "issue:", false) else text
val t1 = if(enableRefsLink) convertRefsLinks(text, repository, "#", false) else text
// convert task list to checkbox.
val t2 = if(enableTaskList) convertCheckBox(t1, hasWritePermission) else t1

View File

@@ -89,6 +89,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
enableWikiLink: Boolean,
enableRefsLink: Boolean,
enableLineBreaks: Boolean,
enableAnchor: Boolean = true,
enableTaskList: Boolean = false,
hasWritePermission: Boolean = false,
pages: List[String] = Nil)(implicit context: Context): Html =
@@ -97,7 +98,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
repository = repository,
enableWikiLink = enableWikiLink,
enableRefsLink = enableRefsLink,
enableAnchor = true,
enableAnchor = enableAnchor,
enableLineBreaks = enableLineBreaks,
enableTaskList = enableTaskList,
hasWritePermission = hasWritePermission,
@@ -288,10 +289,10 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
}
def commitStateIcon(state: CommitState) = Html(state match {
case CommitState.PENDING => ""
case CommitState.SUCCESS => "&#x2714;"
case CommitState.ERROR => "×"
case CommitState.FAILURE => "×"
case CommitState.PENDING => """<i style="color:inherit;width:inherit;height:inherit" class="octicon octicon-primitive-dot"></i>"""
case CommitState.SUCCESS => """<i style="color:inherit;width:inherit;height:inherit" class="octicon octicon-check"></i>"""
case CommitState.ERROR => """<i style="color:inherit;width:inherit;height:inherit" class="octicon octicon-x"></i>"""
case CommitState.FAILURE => """<i style="color:inherit;width:inherit;height:inherit" class="octicon octicon-x"></i>"""
})
def commitStateText(state: CommitState, commitId:String) = state match {

View File

@@ -21,7 +21,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
<ul class="dropdown-menu">
<li><a href="javascript:void(0);" data-name="@loginAccount.get.userName"><i class="octicon octicon-check"></i> <span>@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</span></a></li>
@groupNames.map { groupName =>
<li><a href="javascript:void(0);" data-name="@groupName"><i class="icon-white"></i> <span>@avatar(groupName, 20) @groupName</span></a></li>
<li><a href="javascript:void(0);" data-name="@groupName"><i class="octicon"></i> <span>@avatar(groupName, 20) @groupName</span></a></li>
}
</ul>
<input type="hidden" name="owner" id="owner" value="@loginAccount.get.userName"/>
@@ -77,8 +77,8 @@ $('#owner-dropdown a').click(function(){
var userName = $(this).data('name');
$('#owner').val(userName);
$('#owner-dropdown i').attr('class', 'icon-white');
$(this).find('i').attr('class', 'icon-ok');
$('#owner-dropdown i').attr('class', 'octicon');
$(this).find('i').attr('class', 'octicon octicon-check');
$('#owner-dropdown span.strong').html($(this).find('span').html());
});

View File

@@ -2,5 +2,5 @@
@if(condition){
<i class="octicon octicon-check"></i>
} else {
<i class="icon-white"></i>
<i class="octicon"></i>
}

View File

@@ -54,9 +54,8 @@
<a href="@url(repository)/blob/@newCommitId.get/@diff.newPath" class="btn btn-default btn-sm" title="View the whole file at version @newCommitId.get.substring(0, 10)" data-toggle="tooltip">View</a>
</div>
}
<span class="diffstat">
<i class="octicon octicon-diff-renamed"></i>
</span> @diff.oldPath -> @diff.newPath
<span class="diffstat"><i class="octicon octicon-diff-renamed"></i></span>
<span class="monospace">@diff.oldPath → @diff.newPath</span>
}
@if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){
@if(newCommitId.isDefined){
@@ -73,7 +72,7 @@
<i class="octicon octicon-diff-modified"></i>
}
</span>
@diff.newPath
<span class="monospace">@diff.newPath</span>
}
@if(diff.changeType == ChangeType.DELETE){
@if(oldCommitId.isDefined){
@@ -82,16 +81,22 @@
<a href="@url(repository)/blob/@oldCommitId.get/@diff.oldPath" class="btn btn-default btn-sm" title="View the whole file at version @oldCommitId.get.substring(0, 10)" data-toggle="tooltip">View</a>
</div>
}
<span class="diffstat">
<i class="octicon octicon-diff-removed"></i>
</span> @diff.oldPath
<span class="diffstat"><i class="octicon octicon-diff-removed"></i></span>
<span class="monospace">@diff.oldPath</span>
}
@if(diff.oldMode != diff.newMode){
<span class="monospace">@diff.oldMode → @diff.newMode</span>
}
</th>
</tr>
<tr>
<td style="padding: 0;">
@if(diff.oldObjectId == diff.newObjectId){
<div class="diff-same">File renamed without changes</div>
@if(diff.oldPath != diff.newPath){
<div class="diff-same">File renamed without changes</div>
} else {
<div class="diff-same">File mode changed</div>
}
} else {
@if(diff.newContent != None || diff.oldContent != None){
<div id="diffText-@i" class="diffText"></div>
@@ -99,7 +104,7 @@
<textarea id="oldText-@i" style="display: none;" data-file-name="@diff.newPath">@diff.oldContent.getOrElse("")</textarea>
} else {
@if(diff.newIsImage || diff.oldIsImage){
<div class="diff-image-render diff2up">@diff.oldIsImage @diff.newIsImage
<div class="diff-image-render diff2up">
@if(oldCommitId.isDefined && diff.oldIsImage){
<div class="diff-image-frame diff-old"><img src="@url(repository)/raw/@oldCommitId.get/@diff.oldPath" class="diff-image" onload="onLoadedDiffImages(this)" style="display:none" /></div>
} else {

View File

@@ -1,8 +1,8 @@
@(page: Int, count: Int, limit: Int, width: Int, baseURL: String)
@defining(gitbucket.core.view.Pagination(page, count, limit, width)){ p =>
@if(p.count > p.limit){
<div class="pagination">
<ul>
<div>
<ul class="pagination">
@if(page == 1){
<li class="disabled"><span>&#9664;</span></li>
} else {

View File

@@ -15,7 +15,7 @@
@import gitbucket.core._
@import gitbucket.core.view.helpers._
<div class="tabbable">
<ul class="nav nav-tabs fill-width pull-left" style="@*margin-top: 12px; *@margin-bottom: 10px;">
<ul class="nav nav-tabs fill-width" style="margin-top: 12px; margin-bottom: 10px;">
<li class="active"><a href="#tab@uid" data-toggle="tab">Write</a></li>
<li><a href="#tab@(uid+1)" data-toggle="tab" id="preview@uid">Preview</a></li>
</ul>

View File

@@ -48,7 +48,7 @@
}
@if(userRepositories.size > max){
<li class="list-group-item show-more">
<a href="javascript:void(0);" id="show-more-repos">Show more @{userRepositories.size - max} pages...</a>
<a href="javascript:void(0);" id="show-more-repos">Show @{userRepositories.size - max} more repositories...</a>
</li>
}
}
@@ -62,12 +62,19 @@
@if(recentRepositories.isEmpty){
<li class="list-group-item">No repositories</li>
} else {
@recentRepositories.map { repository =>
<li class="list-group-item repo-link">
@defining(20){ max =>
@recentRepositories.zipWithIndex.map { case (repository, i) =>
<li class="list-group-item repo-link" style="@if(i > max - 1){display:none;}">
@helper.html.repositoryicon(repository, false)
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
</li>
}
@if(recentRepositories.size > max){
<li class="list-group-item show-more">
<a href="javascript:void(0);" id="show-more-recent-repos">Show @{recentRepositories.size - max} more repositories...</a>
</li>
}
}
}
</ul>
</div>
@@ -77,7 +84,7 @@
}
<script>
$(function(){
$('#show-more-repos').click(function(e){
$('#show-more-repos, #show-more-recent-repos').click(function(e){
$(e.target).parents('ul.list-group').find('li.repo-link').show();
$(e.target).parents('li.show-more').remove();
});

View File

@@ -30,10 +30,12 @@
</div>
</div>
}
@issueOrPullRequest()={ @if(issue.isDefined && issue.get.isPullRequest)( "pull request" )else( "issue" ) }
@comments.map {
case comment: gitbucket.core.model.IssueComment => {
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"
&& comment.action != "commit" && comment.action != "refer"){
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
<div class="panel panel-default issue-comment-box" id="comment-@comment.commentId">
<div class="panel-heading">
@@ -42,7 +44,7 @@
@if(comment.action == "comment"){
commented
} else {
@if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request }
referenced the @issueOrPullRequest()
}
@helper.html.datetimeago(comment.registeredDate)
</span>
@@ -53,79 +55,114 @@
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-x" aria-label="Remove"></i></a>
</span>
}
@if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){
@defining(comment.content.substring(comment.content.length - 40)){ id =>
<span class="pull-right"><a href="@path/@repository.owner/@repository.name/commit/@id" class="monospace">@id.substring(0, 7)</a></span>
}
}
</div>
<div class="panel-body issue-content markdown-body" id="commentContent-@comment.commentId">
@if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){
@markdown(
markdown = comment.content.substring(0, comment.content.length - 41),
repository = repository,
enableWikiLink = false,
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = hasWritePermission
)
} else {
@if(comment.action == "refer"){
@defining(comment.content.split(":")){ case Array(issueId, rest @ _*) =>
<strong>@issueLink(repository, issueId.toInt): @rest.mkString(":")</strong>
}
} else {
@markdown(
markdown = comment.content,
repository = repository,
enableWikiLink = false,
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = hasWritePermission
)
@markdown(
markdown = comment.content,
repository = repository,
enableWikiLink = false,
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = hasWritePermission
)
</div>
</div>
}
@if(comment.action == "commit"){
@defining({
val (content, id) = " ([a-f0-9]{40})$".r.findFirstMatchIn(comment.content)
.map(m => (m.before.toString -> Some(m.group(1))))
.getOrElse(comment.content -> None)
val head = content.take(100).takeWhile(_ != '\n')
(id, head, if(head == content){ None }else{ Some(content.drop(head.length).dropWhile(_ == '\n')) }.filter(_.nonEmpty))
}){ case (commitId, head, rest) =>
<div class="discussion-item discussion-item-commit">
<div class="discussion-item-header">
<span class="discussion-item-icon"><i class="octicon octicon-bookmark"></i></span>
@avatar(comment.commentedUserName, 16)
@user(comment.commentedUserName, styleClass="username strong")
added a commit that referenced this @issueOrPullRequest()
@helper.html.datetimeago(comment.registeredDate)
</div>
<div style="discussion-item-content">
@commitId.map{ id =>
<span class="pull-right"><a href="@path/@repository.owner/@repository.name/commit/@id" class="monospace">@id.substring(0, 7)</a></span>
}
<span class="discussion-item-content-head"><i class="octicon octicon-git-commit"></i></span>
@link(head, repository)
@rest.map{ content =>
<a href="javascript:void(0)" class="omit" onclick="$(this.parentNode).find('.discussion-item-content-text').toggle()">...</a>
<pre class="reset discussion-item-content-text" style="display:none">@link(content, repository)</pre>
}
</div>
</div>
}
}
@if(comment.action == "refer"){
<div class="discussion-item discussion-item-refer">
<div class="discussion-item-header">
<span class="discussion-item-icon"><i class="octicon octicon-bookmark"></i></span>
@avatar(comment.commentedUserName, 16)
@user(comment.commentedUserName, styleClass="username strong")
referenced the @issueOrPullRequest()
@helper.html.datetimeago(comment.registeredDate)
</div>
<div style="discussion-item-content">
@defining(comment.content.split(":")){ case Array(issueId, rest @ _*) =>
<strong>@issueLink(repository, issueId.toInt): @rest.mkString(":")</strong>
}
</div>
</div>
}
@if(comment.action == "merge"){
<div class="small" style="margin-top: 10px; margin-bottom: 10px;">
<span class="label label-info">Merged</span>
@avatar(comment.commentedUserName, 20)
@user(comment.commentedUserName, styleClass="username strong") merged commit <code>@pullreq.map(_.commitIdTo.substring(0, 7))</code> into
@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>
} else {
<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)
<div class="discussion-item discussion-item-merge">
<div class="discussion-item-header">
<span class="discussion-item-icon"><i class="octicon octicon-git-merge"></i></span>
@avatar(comment.commentedUserName, 16)
@user(comment.commentedUserName, styleClass="username strong")
merged commit
<code>@pullreq.map(_.commitIdTo.substring(0, 7))</code> into
@if(pullreq.get.requestUserName == repository.owner){
<code>@pullreq.map(_.branch)</code> from <code>@pullreq.map(_.requestBranch)</code>
} else {
<code>@pullreq.map(_.userName):@pullreq.map(_.branch)</code> from <code>@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</code>
}
@helper.html.datetimeago(comment.registeredDate)
</div>
</div>
}
@if(comment.action == "close" || comment.action == "close_comment"){
<div class="issue-comment-action">
<i class="octicon octicon-circle-slash danger"></i>
@avatar(comment.commentedUserName, 20)
@if(issue.isDefined && issue.get.isPullRequest){
@user(comment.commentedUserName, styleClass="username strong") closed the pull request @helper.html.datetimeago(comment.registeredDate)
} else {
@user(comment.commentedUserName, styleClass="username strong") closed the issue @helper.html.datetimeago(comment.registeredDate)
}
<div class="discussion-item discussion-item-close">
<div class="discussion-item-header">
<span class="discussion-item-icon"><i class="octicon octicon-circle-slash"></i></span>
@avatar(comment.commentedUserName, 16)
@user(comment.commentedUserName, styleClass="username strong")
close @issueOrPullRequest()
@helper.html.datetimeago(comment.registeredDate)
</div>
</div>
}
@if(comment.action == "reopen" || comment.action == "reopen_comment"){
<div class="issue-comment-action issue-reopened">
<i class="octicon octicon-primitive-dot"></i>
@avatar(comment.commentedUserName, 20)
@user(comment.commentedUserName, styleClass="username strong") reopened the issue @helper.html.datetimeago(comment.registeredDate)
<div class="discussion-item discussion-item-reopen">
<div class="discussion-item-header">
<span class="discussion-item-icon"><i class="octicon octicon-primitive-dot"></i></span>
@avatar(comment.commentedUserName, 16)
@user(comment.commentedUserName, styleClass="username strong")
reopened the @issueOrPullRequest()
@helper.html.datetimeago(comment.registeredDate)
</div>
</div>
}
@if(comment.action == "delete_branch"){
<div class="issue-comment-action">
<span class="label">Deleted</span>
@avatar(comment.commentedUserName, 20)
@user(comment.commentedUserName, styleClass="username strong") deleted the <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> branch @helper.html.datetimeago(comment.registeredDate)
<div class="discussion-item discussion-item-delete_branch">
<div class="discussion-item-header">
<span class="discussion-item-icon"><i class="octicon octicon-git-branch"></i></span>
@avatar(comment.commentedUserName, 16)
@user(comment.commentedUserName, styleClass="username strong")
deleted the <code>@pullreq.map(_.requestBranch)</code> branch
@helper.html.datetimeago(comment.registeredDate)
</div>
</div>
}
}

View File

@@ -93,7 +93,7 @@
@collaborators.map { collaborator =>
<li>
<a href="javascript:void(0);" class="assign" data-name="@collaborator">
@helper.html.checkicon(issue.exists(_.assignedUserName == collaborator))@avatar(collaborator, 20) @collaborator
@helper.html.checkicon(issue.exists(_.assignedUserName.exists(_ == collaborator)))@avatar(collaborator, 20) @collaborator
</a>
</li>
}
@@ -164,7 +164,7 @@ $(function(){
switchLabel($(this));
var labelNames = Array();
$('a.toggle-label').each(function(i, e){
if($(e).children('i').hasClass('icon-ok') == true){
if($(e).children('i').hasClass('octicon-check') == true){
labelNames.push($(e).text().trim());
}
});
@@ -195,17 +195,17 @@ $(function(){
function switchLabel($this){
var i = $this.children('i');
if(i.hasClass('icon-ok')){
i.removeClass().addClass('icon-white');
if(i.hasClass('octicon-check')){
i.removeClass('octicon-check');
return 'delete';
} else {
i.removeClass().addClass('icon-ok');
i.addClass('octicon-check');
return 'new';
}
}
function displayMilestone(title, milestoneId, progress){
$('a.milestone i.icon-ok').attr('class', 'icon-white');
$('a.milestone i.octicon-check').removeClass('octicon-check');
if(milestoneId == ''){
$('#label-milestone').html($('<span class="muted small">').text('No milestone'));
$('#milestone-progress-area').empty();
@@ -214,19 +214,19 @@ $(function(){
if(progress){
$('#milestone-progress-area').html(progress);
}
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
$('a.milestone[data-id=' + milestoneId + '] i').addClass('octicon-check');
}
}
function displayAssignee($this, userName){
$('a.assign i.icon-ok').attr('class', 'icon-white');
$('a.assign i.octicon-check').removeClass('octicon-check');
if(userName == ''){
$('#label-assigned').html($('<span class="muted small">').text('No one'));
} else {
$('#label-assigned').empty()
.append($this.find('img.avatar-mini').clone(false)).append(' ')
.append($('<a class="username strong small">').attr('href', '@context.path/' + userName).text(userName));
$('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').attr('class', 'icon-ok');
$('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').addClass('octicon-check');
}
}
});

View File

@@ -28,7 +28,7 @@
<td style="padding: 20px; background-color: #eee; text-align: center;">
No labels to show.
@if(hasWritePermission){
<a href="@url(repository)/issues/labels/new">Create a new label.</a>
Click on the "New Label" button above to create one.
}
</td>
</tr>

View File

@@ -124,7 +124,7 @@
@labels.map { label =>
<li>
<a href="javascript:void(0);" class="toggle-label" data-id="@label.labelId">
<i class="icon-white"></i>
<i class="octicon"></i>
<span class="label" style="background-color: #@label.color;">&nbsp;</span>
@label.labelName
</a>
@@ -140,7 +140,7 @@
@helper.html.dropdown("Assignee", flat = true) {
<li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="octicon octicon-x"></i> Clear assignee</a></li>
@collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
<li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="octicon"></i>@avatar(collaborator, 20) @collaborator</a></li>
}
}
</div>

View File

@@ -78,8 +78,7 @@
<div class="pull-right" style="margin-top: 6px;">
<div class="btn-group" style="margin-right: 8px;">
<a class="dropdown-toggle menu" data-toggle="dropdown" href="#">
<i class="octicon octicon-plus" style="color: black; font-size: 20px; vertical-align: middle;height:20px !important;"></i>
<span class="caret" style="color: black; vertical-align: middle;"></span>
<i class="octicon octicon-plus" style="color: black; font-size: 20px; vertical-align: middle;height:20px !important;"></i><span class="caret" style="color: black; vertical-align: middle;"></span>
</a>
<ul class="dropdown-menu pull-right">
<li><a href="@path/new">New repository</a></li>
@@ -87,8 +86,8 @@
</ul>
</div>
<div class="btn-group">
<a class="dropdown-toggle menu" data-toggle="dropdown" href="#" data-toggle="tooltip" data-placement="bottom" title="Signed is as @loginAccount.get.userName">@avatar(loginAccount.get.userName, 16)
<span class="caret" style="color: black; vertical-align: middle;"></span>
<a class="dropdown-toggle menu" data-toggle="dropdown" href="#" data-toggle="tooltip" data-placement="bottom" title="Signed is as @loginAccount.get.userName">
@avatar(loginAccount.get.userName, 16)<span class="caret" style="color: black; vertical-align: middle;"></span>
</a>
<ul class="dropdown-menu pull-right">
<li><a href="@url(loginAccount.get.userName)">Your profile</a></li>

View File

@@ -15,7 +15,7 @@
<i class="menu-icon @if(active == name){menu-icon-active} octicon octicon-@{icon} "></i>
@if(expand){ @label}
@if(expand && count > 0){
<div class="pull-right"><span class="label">@count</span></div>
<div class="pull-right"><span class="badge">@count</span></div>
}
</a>
</li>
@@ -105,7 +105,7 @@
}
}
</div>
<div style="margin-right: @if(expand){180px} else {55px};">
<div class="pull-left" style="width: @if(expand){770px} else {895px};">
@if(expand){
@repository.repository.description.map { description =>
<p class="description">@detectAndRenderLinks(description)</p>
@@ -116,7 +116,11 @@
<td style="width: 33%; text-align: center;">
<a href="@url(repository)/commits/@encodeRefName(id.getOrElse(""))" class="header-link">
<i class="octicon octicon-history"></i>
<strong>@repository.commitCount</strong> commits
@if(repository.commitCount > 10000){
<strong>10000+</strong> commits
} else {
<strong>@repository.commitCount</strong> commits
}
</a>
</td>
<td style="width: 33%; text-align: center;">

View File

@@ -174,8 +174,8 @@
$(function(){
$('a.origin-owner, a.forked-owner, a.origin-branch, a.forked-branch').click(function(){
var e = $(this);
e.parents('ul').find('i').attr('class', 'icon-white');
e.find('i').attr('class', 'octicon-check');
e.parents('ul').find('i').attr('class', 'octicon');
e.find('i').addClass('octicon-check');
e.parents('div.btn-group').find('button span.strong').text(e.text());
@if(members.isEmpty){

View File

@@ -20,7 +20,7 @@
case comment: gitbucket.core.model.IssueComment => Some(comment)
case other => None
}.exists(_.action == "merge")){ merged =>
@if(hasWritePermission && !issue.closed){
@if(!issue.closed){
<div class="check-conflict" style="display: none;">
<div class="box issue-comment-box" style="background-color: #fbeed5">
<div class="box-content"class="issue-content" style="border: 1px solid #c09853; padding: 10px;">
@@ -40,9 +40,9 @@
<span class="small muted">You're all set-the <span class="label label-info monospace">@pullreq.requestBranch</span> branch can be safely deleted.</span>
</div>
</div>
}
@issues.html.commentform(issue, !merged, hasWritePermission, repository)
}
@issues.html.commentform(issue, !merged, hasWritePermission, repository)
}
</div>
<div class="col-md-2">
@issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
@@ -55,10 +55,12 @@ $(function(){
$('#merge-pull-request').show();
});
@if(hasWritePermission){
$('.check-conflict').show();
var checkConflict = $('.check-conflict').show();
if(checkConflict.length){
$.get('@url(repository)/pull/@issue.issueId/mergeguide', function(data){ $('.check-conflict').html(data); });
}
@if(hasWritePermission){
$('.delete-branch').click(function(e){
var branchName = $(e.target).data('name');
return confirm('Are you sure you want to remove the ' + branchName + ' branch?');

View File

@@ -1,120 +1,139 @@
@(hasConflict: Boolean,
hasProblem: Boolean,
@(status: gitbucket.core.service.PullRequestService.MergeStatus,
issue: gitbucket.core.model.Issue,
pullreq: gitbucket.core.model.PullRequest,
statuses: List[model.CommitStatus],
originRepository: gitbucket.core.service.RepositoryService.RepositoryInfo,
forkedRepository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.service.SystemSettingsService
@import context._
@import gitbucket.core.view.helpers._
@import model.CommitState
<div class="box issue-comment-box" style="background-color: @if(hasProblem){ #fbeed5 }else{ #d8f5cd };">
<div class="box-content issue-content" style="border: 1px solid @if(hasProblem){ #c09853 }else{ #95c97e }; padding: 10px;">
<div class="box issue-comment-box" style="background-color: @if(status.hasProblem){ #fbeed5 }else{ #d8f5cd };">
<div class="box-content issue-content" style="border: 1px solid @if(status.hasProblem){ #c09853 }else{ #95c97e };padding:0">
<div id="merge-pull-request">
@if(!statuses.isEmpty){
@if(!status.statuses.isEmpty){
<div class="build-statuses">
@if(statuses.size==1){
@defining(statuses.head){ status =>
@defining(status.commitStateSummary){ case (summaryState, summary) =>
<div class="build-status-item-header">
<a class="pull-right" id="toggle-all-checks"></a>
<span class="build-status-icon text-@{summaryState.name}">@commitStateIcon(summaryState)</span>
<strong class="text-@{summaryState.name}">@commitStateText(summaryState, pullreq.commitIdTo)</strong>
<span class="text-@{summaryState.name}">— @summary checks</span>
</div>
}
<div class="build-statuses-list" style="@if(status.isAllSuccess){ display:none; }else{ }">
@status.statusesAndRequired.map{ case (status, required) =>
<div class="build-status-item">
@status.targetUrl.map{ url => <a class="pull-right" href="@url">Details</a> }
<div class="pull-right">
@if(required){ <span class="label">Required</span> }
@status.targetUrl.map{ url => <a href="@url">Details</a> }
</div>
<span class="build-status-icon text-@{status.state.name}">@commitStateIcon(status.state)</span>
<strong class="text-@{status.state.name}">@commitStateText(status.state, pullreq.commitIdTo)</strong>
<strong>@status.context</strong>
@status.description.map{ desc => <span class="muted">— @desc</span> }
</div>
}
} else {
@defining(statuses.groupBy(_.state)){ stateMap =>
@defining(CommitState.combine(stateMap.keySet)){ state =>
<div class="build-status-item">
<a class="pull-right" id="toggle-all-checks"></a>
<span class="build-status-icon text-@{state.name}">@commitStateIcon(state)</span>
<strong class="text-@{state.name}">@commitStateText(state, pullreq.commitIdTo)</strong>
<span class="text-@{state.name}">— @{stateMap.map{ case (keyState, states) => states.size+" "+keyState.name }.mkString(", ")} checks</span>
</div>
<div class="build-statuses-list" style="@if(state==CommitState.SUCCESS){ display:none; }else{ }">
@statuses.map{ status =>
<div class="build-status-item">
@status.targetUrl.map{ url => <a class="pull-right" href="@url">Details</a> }
<span class="build-status-icon text-@{status.state.name}">@commitStateIcon(status.state)</span>
<span class="text-@{status.state.name}">@status.context</span>
@status.description.map{ desc => <span class="muted">— @desc</span> }
</div>
}
</div>
}
}
}
</div>
</div>
}
<div class="pull-right">
<input type="button" class="btn @if(!hasProblem){ btn-success }else{ btn-default }" id="merge-pull-request-button" value="Merge pull request"@if(hasConflict){ disabled="true"}/>
</div>
<div>
@if(hasConflict){
<span class="strong">We cant automatically merge this pull request.</span>
} else {
@if(hasProblem){
<span class="strong">Merge with caution!</span>
} else {
<span class="strong">This pull request can be automatically merged.</span>
}
}
</div>
<div class="small">
@if(hasConflict){
<a href="#" id="show-command-line">Use the command line</a> to resolve conflicts before continuing.
} else {
You can also merge branches on the <a href="#" id="show-command-line">command line</a>.
}
</div>
<div id="command-line" style="display: none;">
<hr>
@if(hasConflict){
<span class="strong">Checkout via command line</span>
<p>
If you cannot merge a pull request automatically here, you have the option of checking
it out via command line to resolve conflicts and perform a manual merge.
</p>
} else {
<span class="strong">Merging via command line</span>
<p>
If you do not want to use the merge button or an automatic merge cannot be performed,
you can perform a manual merge on the command line.
</p>
}
@helper.html.copy("repository-url-copy", forkedRepository.httpUrl, true){
<div class="btn-group" data-toggle="buttons-radio">
<button class="btn btn-small active" type="button" id="repository-url-http">HTTP</button>
@if(settings.ssh && loginAccount.isDefined){
<button class="btn btn-small" type="button" id="repository-url-ssh" style="border-radius: 0px;">SSH</button>
<div style="padding:15px">
@if(status.hasConflict){
<div class="merge-indicator merge-indicator-alert"><span class="octicon octicon-alert"></span></div>
<span class="strong">This branch has conflicts that must be resolved</span>
<div class="small">
@if(status.hasMergePermission){
<a href="#" class="show-command-line">Use the command line</a> to resolve conflicts before continuing.
} else {
Only those with write access to this repository can merge pull requests.
}
</div>
<input type="text" style="width: 500px;" value="@forkedRepository.httpUrl" id="repository-url" readonly>
}
<div>
<p>
<span class="strong">Step 1:</span> From your project repository, check out a new branch and test the changes.
</p>
@defining(s"git checkout -b ${pullreq.requestUserName}-${pullreq.requestBranch} ${pullreq.branch}\n" +
s"git pull ${forkedRepository.httpUrl} ${pullreq.requestBranch}"){ command =>
@helper.html.copy("merge-command-copy-1", command){
<pre style="width: 600px; float: left; font-size: 12px; border-radius: 3px 0px 3px 3px;" id="merge-command">@Html(command)</pre>
}
} else { @if(status.branchIsOutOfDate){
@if(status.hasUpdatePermission){
<div class="pull-right">
<form method="POST" action="@url(originRepository)/pull/@pullreq.issueId/update_branch">
<input type="hidden" name="expected_head_oid" value="@pullreq.commitIdFrom">
<button class="btn"@if(!status.canUpdate){ disabled="true"} id="update-branch-button">Update branch</button>
</form>
</div>
}
</div>
<div>
<p>
<span class="strong">Step 2:</span> Merge the changes and update on the server.
</p>
@defining(s"git checkout ${pullreq.branch}\ngit merge --no-ff ${pullreq.requestUserName}-${pullreq.requestBranch}\n" +
s"git push origin ${pullreq.branch}"){ command =>
@helper.html.copy("merge-command-copy-2", command){
<pre style="width: 600px; float: left; font-size: 12px; border-radius: 3px 0px 3px 3px;">@command</pre>
}
<div class="merge-indicator merge-indicator-alert"><span class="octicon octicon-alert"></span></div>
<span class="strong">This branch is out-of-date with the base branch</span>
<div class="small">
Merge the latest changes from <code>@pullreq.branch</code> into this branch.
</div>
} else { @if(status.hasRequiredStatusProblem) {
<div class="merge-indicator merge-indicator-warning"><span class="octicon octicon-primitive-dot"></span></div>
<span class="strong">Required statuses must pass before merging.</span>
<div class="small">
All required status checks on this pull request must run successfully to enable automatic merging.
</div>
} else {
<div class="merge-indicator merge-indicator-success"><span class="octicon octicon-check"></span></div>
@if(status.hasMergePermission){
<span class="strong">Merging can be performed automatically.</span>
<div class="small">
Merging can be performed automatically.
</div>
} else {
<span class="strong">This branch has no conflicts with the base branch.</span>
<div class="small">
Only those with write access to this repository can merge pull requests.
</div>
}
</div>
} } }
</div>
@if(status.hasMergePermission){
<div style="padding:15px;border-top:solid 1px #e5e5e5;background:#fafafa">
<input type="button" class="btn @if(!status.hasProblem){ btn-success }" id="merge-pull-request-button" value="Merge pull request"@if(!status.canMerge){ disabled="true"}/>
You can also merge branches on the <a href="#" class="show-command-line">command line</a>.
<div id="command-line" style="display: none;margin-top: 15px;">
<hr />
@if(status.hasConflict){
<span class="strong">Checkout via command line</span>
<p>
If you cannot merge a pull request automatically here, you have the option of checking
it out via command line to resolve conflicts and perform a manual merge.
</p>
} else {
<span class="strong">Merging via command line</span>
<p>
If you do not want to use the merge button or an automatic merge cannot be performed,
you can perform a manual merge on the command line.
</p>
}
@helper.html.copy("repository-url-copy", forkedRepository.httpUrl, true){
<div class="btn-group" data-toggle="buttons-radio">
<button class="btn btn-small active" type="button" id="repository-url-http">HTTP</button>
@if(settings.ssh && loginAccount.isDefined){
<button class="btn btn-small" type="button" id="repository-url-ssh" style="border-radius: 0px;">SSH</button>
}
</div>
<input type="text" style="width: 500px;" value="@forkedRepository.httpUrl" id="repository-url" readonly />
}
<div>
<p>
<span class="strong">Step 1:</span> From your project repository, check out a new branch and test the changes.
</p>
@defining(s"git checkout -b ${pullreq.requestUserName}-${pullreq.requestBranch} ${pullreq.branch}\n" +
s"git pull ${forkedRepository.httpUrl} ${pullreq.requestBranch}"){ command =>
@helper.html.copy("merge-command-copy-1", command){
<pre style="width: 600px; float: left; font-size: 12px; border-radius: 3px 0px 3px 3px;" id="merge-command">@Html(command)</pre>
}
}
</div>
<div>
<p>
<span class="strong">Step 2:</span> Merge the changes and update on the server.
</p>
@defining(s"git checkout ${pullreq.branch}\ngit merge --no-ff ${pullreq.requestUserName}-${pullreq.requestBranch}\n" +
s"git push origin ${pullreq.branch}"){ command =>
@helper.html.copy("merge-command-copy-2", command){
<pre style="width: 600px; float: left; font-size: 12px; border-radius: 3px 0px 3px 3px;">@command</pre>
}
}
</div>
</div>
</div>
}
</div>
<div id="confirm-merge-form" style="display: none;">
<form method="POST" action="@url(originRepository)/pull/@pullreq.issueId/merge">
@@ -134,8 +153,8 @@
<script>
$(function(){
$('#show-command-line').click(function(){
$('#command-line').show();
$('.show-command-line').click(function(){
$('#command-line').toggle();
return false;
});
function setToggleAllChecksLabel(){

View File

@@ -8,7 +8,8 @@
dayByDayCommits: Seq[Seq[gitbucket.core.util.JGitUtil.CommitInfo]],
diffs: Seq[gitbucket.core.util.JGitUtil.DiffInfo],
hasWritePermission: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
flash: Map[String, String])(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.view.helpers._
@import gitbucket.core.model._
@@ -73,6 +74,12 @@
</ul>
<div class="tab-content fill-width pull-left">
<div class="tab-pane active" id="conversation">
@flash.get("error").map{ error =>
<div class="alert alert-error">@error</div>
}
@flash.get("info").map{ info =>
<div class="alert alert-info">@info</div>
}
@pulls.html.conversation(issue, pullreq, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
</div>
<div class="tab-pane" id="commits">

View File

@@ -7,7 +7,7 @@
isBlame: Boolean)(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.view.helpers._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.main(s"${(repository.name :: pathList).mkString("/")} at ${encodeRefName(branch)} - ${repository.owner}/${repository.name}", Some(repository)) {
@html.menu("code", repository){
<div class="head">
<div class="pull-right hide-if-blame"><div class="btn-group">

View File

@@ -1,4 +1,4 @@
@(branchInfo: Seq[(gitbucket.core.util.JGitUtil.BranchInfo, Option[(gitbucket.core.model.PullRequest, gitbucket.core.model.Issue)])],
@(branchInfo: Seq[(gitbucket.core.util.JGitUtil.BranchInfo, Option[(gitbucket.core.model.PullRequest, gitbucket.core.model.Issue)], Boolean)],
hasWritePermission: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import context._
@@ -13,7 +13,7 @@
</tr>
</thead>
<tbody>
@branchInfo.map { case (branch, prs) =>
@branchInfo.map { case (branch, prs, isProtected) =>
<tr><td style="padding: 12px;">
<div class="branch-action">
@branch.mergeInfo.map{ info =>
@@ -48,13 +48,18 @@
@if(prs.map(!_._2.closed).getOrElse(false)){
<a class="disabled" data-toggle="tooltip" title="You cant delete this branch because it has an open pull request"><i class="octicon octicon-trashcan"></i></a>
} else {
<a href="@url(repository)/delete/@encodeRefName(branch.name)" class="delete-branch" data-name="@branch.name" @if(info.isMerged){ data-toggle="tooltip" title="this branch is merged" }><i class="octicon octicon-trashcan @if(info.isMerged){warning} else {danger}"></i></a>
@if(isProtected){
<a class="disabled" data-toggle="tooltip" title="You cant delete a protected branch."><i class="octicon octicon-trashcan"></i></a>
} else {
<a href="@url(repository)/delete/@encodeRefName(branch.name)" class="delete-branch" data-name="@branch.name" @if(info.isMerged){ data-toggle="tooltip" title="this branch is merged" }><i class="octicon octicon-trashcan @if(info.isMerged){warning} else {danger}"></i></a>
}
}
</span>
}
}
</div>
<div class="branch-details">
@if(isProtected){ <span class="octicon octicon-shield" title="This branch is protected"></span> }
<a href="@url(repository)/tree/@encodeRefName(branch.name)" class="branch-name">@branch.name</a>
<span class="branch-meta">
<span>Updated @helper.html.datetimeago(branch.commitTime, false)

View File

@@ -2,11 +2,15 @@
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
pathList: List[String],
fileName: Option[String],
content: gitbucket.core.util.JGitUtil.ContentInfo)(implicit context: gitbucket.core.controller.Context)
content: gitbucket.core.util.JGitUtil.ContentInfo,
protectedBranch: Boolean)(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.view.helpers._
@html.main(if(fileName.isEmpty) "New File" else s"Editing ${fileName.get} at ${branch} - ${repository.owner}/${repository.name}", Some(repository)) {
@html.menu("code", repository){
@if(protectedBranch){
<div class="alert alert-danger">branch @branch is protected.</div>
}
<form method="POST" action="@url(repository)/@if(fileName.isEmpty){create}else{update}" validate="true">
<span class="error" id="error-newFileName"></span>
<div class="head">
@@ -82,6 +86,9 @@ $(function(){
@if(fileName.isDefined){
editor.getSession().setMode("ace/mode/@editorType(fileName.get)");
}
@if(protectedBranch){
editor.setReadOnly(true);
}
editor.on('change', function(){
updateCommitButtonStatus();
@@ -122,16 +129,17 @@ $(function(){
$('#editor').hide();
$('#preview').show();
$('#btn-code').removeClass('active');
$('#btn-preview').appendClass('active');
$('#btn-preview').addClass('active');
@if(fileName.map(isRenderable _).getOrElse(false)) {
// update preview
$('#preview').html('<img src="@assets/common/images/indicator.gif"> Previewing...');
$.post('@url(repository)/_preview', {
content : editor.getValue(),
enableWikiLink : false,
enableRefsLink : false,
enableTaskList : false
content : editor.getValue(),
enableWikiLink : false,
enableRefsLink : false,
enableLineBreaks : false,
enableTaskList : false
}, function(data){
$('#preview').empty().append(
$('<div class="markdown-body" style="padding-left: 20px; padding-right: 20px;">').html(data));

View File

@@ -11,7 +11,16 @@
error: Option[Any] = None)(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.view.helpers._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.main(
if(pathList.isEmpty){
if(branch == repository.repository.defaultBranch){
s"${repository.owner}/${repository.name}"
} else {
s"${repository.owner}/${repository.name} at ${encodeRefName(branch)}"
}
} else {
s"${(repository.name :: pathList).mkString("/")} at ${encodeRefName(branch)} - ${repository.owner}/${repository.name}"
}, Some(repository)) {
@html.menu("code", repository, Some(branch), pathList.isEmpty, groupNames.isEmpty, info, error){
<div class="head">
<div class="pull-right"><div class="btn-group">
@@ -97,25 +106,25 @@
}
</td>
<td class="content-name">
@if(file.isDirectory){
@if(file.linkUrl.isDefined){
<a href="@file.linkUrl">
<span class="simplified-path">@file.name.split("/").toList.init match {
case Nil => {}
case list => {@list.mkString("", "/", "/")}
}</span>@file.name.split("/").toList.last
</a>
@if(file.isDirectory){
@if(file.linkUrl.isDefined){
<a href="@file.linkUrl">
<span class="simplified-path">@file.name.split("/").toList.init match {
case Nil => {}
case list => {@list.mkString("", "/", "/")}
}</span>@file.name.split("/").toList.last
</a>
} else {
<a href="@url(repository)/tree@{(branch :: pathList).map(encodeRefName).mkString("/", "/", "/")}@{encodeRefName(file.name)}">
<span class="simplified-path">@file.name.split("/").toList.init match {
case Nil => {}
case list => {@list.mkString("", "/", "/")}
}</span>@file.name.split("/").toList.last
</a>
}
} else {
<a href="@url(repository)/tree@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">
<span class="simplified-path">@file.name.split("/").toList.init match {
case Nil => {}
case list => {@list.mkString("", "/", "/")}
}</span>@file.name.split("/").toList.last
</a>
<a href="@url(repository)/blob@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>
}
} else {
<a href="@url(repository)/blob@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>
}
</td>
<td class="mute">
<a href="@url(repository)/commit/@file.commitId" class="commit-message shorten-text" title="@file.message">@link(file.message, repository)</a>

View File

@@ -0,0 +1,69 @@
@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
protectedBranchList: Seq[String],
info: Option[Any])(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.view.helpers._
@import gitbucket.core.model.WebHook._
@html.main("Branches", Some(repository)){
@html.menu("settings", repository){
@menu("branches", repository){
@if(repository.branchList.isEmpty){
<div class="well">
<center>
<p><i class="octicon octicon-git-branch" style="font-size:300%"></i></p>
<p>You dont have any branches</p>
<p>Before you can edit branch settings, you need to add a branch.</p>
</center>
</div>
}else{
@helper.html.information(info)
<div class="panel panel-default">
<div class="panel-heading strong">Default branch</div>
<div class="panel-body">
<p>The default branch is considered the “base” branch in your repository, against which all pull requests and code commits are automatically made, unless you specify a different branch.</p>
<form id="form" method="post" action="@url(repository)/settings/update_default_branch" validate="true" class="form-inline">
<span class="error" id="error-defaultBranch"></span>
<select name="defaultBranch" id="defaultBranch" class="form-control">
@repository.branchList.map { branch =>
<option @if(branch==repository.repository.defaultBranch){ selected}>@branch</option>
}
</select>
<input type="submit" class="btn btn-default" value="Update" />
</form>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading strong">Protected branches</div>
<div class="panel-body">
<p>Protect branches to disable force pushing, prevent branches from being deleted, and optionally require status checks before merging. New to protected branches?
<form class="form-inline">
<select name="protectBranch" id="protectBranch" onchange="location=$(this).val()" class="form-control">
<option>Choose a branch...</option>
@repository.branchList.map { branch =>
<option value="@url(repository)/settings/branches/@encodeRefName(branch)">@branch</option>
}
</select>
<span class="error" id="error-protectBranch"></span>
</form>
</p>
<table class="table table-bordered table-hover branches">
@protectedBranchList.map { branch =>
<tr>
<td>
<span class="branch-name">@branch</span>
<span class="branch-action">
<a href="@url(repository)/settings/branches/@encodeRefName(branch)" class="btn btn-small btn-default">Edit</a>
</span>
</td>
</tr>
}
</table>
</div>
</div>
}
}
}
}

View File

@@ -0,0 +1,123 @@
@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
branch: String,
protection: gitbucket.core.api.ApiBranchProtection,
knownContexts: Seq[String],
info: Option[Any])(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.view.helpers._
@import gitbucket.core.model.WebHook._
@check(bool:Boolean)={@if(bool){ checked}}
@html.main(s"Branch protection for ${branch}", Some(repository)){
@html.menu("settings", repository){
@menu("branches", repository){
@helper.html.information(info)
<div class="alert alert-info" style="display:none" id="saved-info">Branch protection options saved</div>
<form name="branchProtection" onsubmit="submitForm(event)">
<div class="panel panel-default">
<div class="panel-heading">Branch protection for <b>@branch</b></div>
<div class="panel-body">
<div class="checkbox">
<label>
<input type="checkbox" name="enabled" onclick="update()" @check(protection.enabled)>
<span class="strong">Protect this branch</span>
</label>
<p class="help-block">Disables force-pushes to this branch and prevents it from being deleted.</p>
</div>
<div class="checkbox js-enabled" style="display:none">
<label>
<input type="checkbox" name="has_required_statuses" onclick="update()" @check(protection.status.enforcement_level.name!="off")>
<span class="strong">Require status checks to pass before merging</span>
</label>
<p class="help-block">Choose which status checks must pass before branches can be merged into test.
When enabled, commits must first be pushed to another branch, then merged or pushed directly to test after status checks have passed.</p>
<div class="js-has_required_statuses" style="display:none">
<div class="checkbox">
<label>
<input type="checkbox" name="enforce_for_admins" onclick="update()" @check(protection.status.enforcement_level.name=="everyone")>
<span class="strong">Include administrators</span>
</label>
<p class="help-block">Enforce required status checks for repository administrators.</p>
</div>
<div class="panel panel-default">
<div class="panel-heading">Status checks found in the last week for this repository</div>
<div class="panel-body">
@knownContexts.map { context =>
<div class="checkbox">
<label>
<input type="checkbox" name="contexts" value="@context" onclick="update()" @check(protection.status.contexts.find(_ == context))>
<span>@context</span>
</label>
</div>
}
</div>
</div>
</div>
</div>
<input class="btn btn-success btn-lg" type="submit" value="Save changes" />
</div>
</div>
</form>
}
}
}
<script>
function getValue(){
var v = {}, contexts=[];
$("input[type=checkbox]:checked").each(function(){
if(this.name === 'contexts'){
contexts.push(this.value);
} else {
v[this.name] = true;
}
});
if(v.enabled){
return {
enabled: true,
required_status_checks: {
enforcement_level: v.has_required_statuses ? ((v.enforce_for_admins ? 'everyone' : 'non_admins')) : 'off',
contexts: v.has_required_statuses ? contexts : []
}
};
} else {
return {
enabled: false,
required_status_checks: {
enforcement_level: "off",
contexts: []
}
};
}
}
function updateView(protection){
$('.js-enabled').toggle(protection.enabled);
$('.js-has_required_statuses').toggle(protection.required_status_checks.enforcement_level != 'off');
}
function update(){
var protection = getValue();
updateView(protection);
}
$(update);
function submitForm(e){
e.stopPropagation();
e.preventDefault();
var protection = getValue();
$.ajax({
method:'PATCH',
url:'/api/v3/repos/@repository.owner/@repository.name/branches/@encodeRefName(branch)',
contentType: 'application/json',
dataType: 'json',
data:JSON.stringify({protection:protection}),
success:function(r){
$('#saved-info').show();
},
error:function(err){
console.log(err);
alert('update error');
}
});
}
</script>

View File

@@ -11,6 +11,11 @@
<li@if(active=="collaborators"){ class="active"}>
<a href="@url(repository)/settings/collaborators">Collaborators</a>
</li>
@if(!repository.branchList.isEmpty){
<li@if(active=="branches"){ class="active"}>
<a href="@url(repository)/settings/branches">Branches</a>
</li>
}
<li@if(active=="hooks"){ class="active"}>
<a href="@url(repository)/settings/hooks">Service Hooks</a>
</li>

View File

@@ -18,22 +18,6 @@
<label for="description" class="strong">Description:</label>
<input type="text" name="description" id="description" class="form-control" value="@repository.repository.description"/>
</fieldset>
<fieldset class="margin form-group">
<label for="defaultBranch" class="strong">Default Branch:</label>
<select name="defaultBranch" id="defaultBranch"@if(repository.branchList.isEmpty){ disabled} class="form-control">
@if(repository.branchList.isEmpty){
<option value="none" selected>No Branch</option>
} else {
@repository.branchList.map { branch =>
<option@if(branch==repository.repository.defaultBranch){ selected}>@branch</option>
}
}
</select>
@if(repository.branchList.isEmpty){
<input type="hidden" name="defaultBranch" value="none"/>
}
<span class="error" id="error-defaultBranch"></span>
</fieldset>
<fieldset class="margin">
<label class="radio">
<input type="radio" name="isPrivate" value="false"

View File

@@ -10,7 +10,7 @@
@html.main(s"Compare Revisions - ${repository.owner}/${repository.name}", Some(repository)){
@helper.html.information(info)
@html.menu("wiki", repository){
<ul class="nav nav-tabs fill-width pull-left">
<ul class="nav nav-tabs fill-width">
<li>
<h1 class="wiki-title"><span class="muted">Compare Revisions</span></h1>
</li>

View File

@@ -5,7 +5,7 @@
@import gitbucket.core.view.helpers._
@html.main(s"${if(pageName.isEmpty) "New Page" else pageName} - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("wiki", repository){
<ul class="nav nav-tabs fill-width pull-left">
<ul class="nav nav-tabs fill-width">
<li>
<h1 class="wiki-title"><span class="muted">Editing</span> @if(pageName.isEmpty){New Page} else {@pageName}</h1>
</li>

View File

@@ -5,7 +5,7 @@
@import gitbucket.core.view.helpers._
@html.main(s"History - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("wiki", repository){
<ul class="nav nav-tabs fill-width pull-left">
<ul class="nav nav-tabs fill-width">
<li>
<h1 class="wiki-title">
@if(pageName.isEmpty){

View File

@@ -2,13 +2,15 @@
page: gitbucket.core.service.WikiService.WikiPageInfo,
pages: List[String],
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context)
hasWritePermission: Boolean,
sidebar: Option[gitbucket.core.service.WikiService.WikiPageInfo],
footer: Option[gitbucket.core.service.WikiService.WikiPageInfo])(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.view.helpers._
@import gitbucket.core.service.WikiService._
@html.main(s"${pageName} - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("wiki", repository){
<ul class="nav nav-tabs fill-width pull-left">
<ul class="nav nav-tabs fill-width">
<li>
<h1 class="wiki-title">@pageName</h1>
<div class="small">
@@ -28,9 +30,13 @@
@defining(15){ max =>
<div class="panel panel-default">
<div class="panel-heading strong">
Pages <span class="badge">@pages.length</span>
<a id="show-pages-index" href="javascript:void(0);">
<span id="triangle-down" class="octicon octicon-triangle-down"></span>
<span id="triangle-right" class="octicon octicon-triangle-right" style="display: none;"></span>
<span class="strong">Pages</span> <span class="badge">@pages.length</span>
</a>
</div>
<ul class="list-group list-group-flush">
<ul id="pages-index" class="list-group list-group-flush">
@pages.zipWithIndex.map { case (page, i) =>
<li class="list-group-item page-link" style="border: none; @if(i > max - 1){display:none;}">
<a href="@url(repository)/wiki/@urlEncode(page)" class="strong">@page</a>
@@ -44,6 +50,20 @@
</ul>
</div>
}
@sidebar.map { sidebarPage =>
<div class="wiki-sidebar">
@if(hasWritePermission){
<a href="@url(repository)/wiki/_Sidebar/_edit" style="text-decoration: none;"><span class="octicon octicon-pencil pull-right"></span></a>
}
@markdown(sidebarPage.content, repository, true, false, false, false, pages)
</div>
}.getOrElse{
@if(hasWritePermission){
<a class="button-link" href="@url(repository)/wiki/_Sidebar/_edit" style="text-decoration: none;">
<div class="wiki-sidebar-dotted text-center"><i class="octicon octicon-plus"></i> Add a custom sidebar</div>
</a>
}
}
<div class="small">
<strong>Clone this wiki locally</strong>
</div>
@@ -69,6 +89,20 @@
pages = pages
)
</div>
@footer.map { footerPage =>
<div class="wiki-sidebar wiki-footer">
@if(hasWritePermission){
<a href="@url(repository)/wiki/_Footer/_edit" style="text-decoration: none;"><span class="octicon octicon-pencil pull-right"></span></a>
}
@markdown(footerPage.content, repository, true, false, false, false, pages)
</div>
}.getOrElse{
@if(hasWritePermission){
<a class="button-link" href="@url(repository)/wiki/_Footer/_edit" style="text-decoration: none;">
<div class="wiki-sidebar-dotted text-center"><i class="octicon octicon-plus"></i> Add a custom footer</div>
</a>
}
}
</div>
}
}
@@ -79,6 +113,24 @@ $(function(){
$(e.target).parents('div.show-more').remove();
});
$('#show-pages-index').click(function(e){
if($('#pages-index').is(":visible")){
$('#triangle-down').hide();
$('#triangle-right').show();
$('#pages-index').hide();
} else {
$('#triangle-right').hide();
$('#triangle-down').show();
$('#pages-index').show();
}
});
@sidebar.map { sidebarPage =>
$('#pages-index').hide();
$('#triangle-down').hide();
$('#triangle-right').show();
}
@if(settings.ssh && loginAccount.isDefined){
$('#repository-url-http').click(function(){
$('#repository-url').val('@httpUrl(repository)');

View File

@@ -5,7 +5,7 @@
@import gitbucket.core.view.helpers._
@html.main(s"Pages - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("wiki", repository){
<ul class="nav nav-tabs fill-width pull-left">
<ul class="nav nav-tabs fill-width">
<li>
<h1 class="wiki-title"><span class="muted">Pages</span></h1>
</li>

View File

@@ -146,6 +146,24 @@ hr {
margin-bottom: 4px;
}
pre.reset {
/* defaults */
display: block;
font-family: monospace;
white-space: pre;
margin: 1em 0px;
/* bootstrap overrides */
padding: initial;
line-height: initial;
color: initial;
background-color: initial;
border: initial;
overflow: initial;
word-break: initial;
word-wrap: initial;
border-radius: initial;
}
/* ======================================================================== */
/* Global Header */
/* ======================================================================== */
@@ -564,7 +582,7 @@ ul.sidemenu li {
}
ul.sidemenu span.badge {
padding-right: 4px;
}
ul.sidemenu a:hover i.menu-icon, ul.sidemenu i.menu-icon-active {
@@ -613,6 +631,11 @@ ul.nav-stacked.side-menu li {
margin-bottom: -3px;
}
a#show-pages-index {
color: #333;
text-decoration: none;
}
/*
ul.nav-stacked.side-menu li span.header {
border-top-right-radius: 3px;
@@ -824,6 +847,9 @@ table.table-file-list .file-icon {
table.table-file-list .content-name {
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
table.table-file-list .commit-message {
@@ -1310,6 +1336,74 @@ li.task-list-item input.task-list-item-checkbox {
vertical-align: middle;
}
.discussion-item {
position: relative;
margin: 15px 0 15px 79px;
padding-left: 25px;
}
.discussion-item-header {
min-height: 30px;
padding-top: 5px;
padding-bottom: 5px;
color: #767676;
line-height: 20px;
word-wrap: break-word;
}
.discussion-item-icon {
float: left;
width: 32px;
height: 32px;
margin-top: -7px;
margin-left: -40px;
line-height: 28px;
color: #767676;
text-align: center;
background-color: #f3f3f3;
border: 2px solid #fff;
border-radius: 50%;
}
.discussion-item-content {
margin-left:36px;
}
.discussion-item-content-head{
padding-right:20px;
background-color: white;
float:left
}
pre.reset.discussion-item-content-text{
border-left: 1px solid rgb(238, 238, 238);
margin: 0 0 0 5px;
padding-left: 25px;
}
.discussion-item-commit .discussion-item-content {
font-size:12px;
}
.discussion-item-icon .octicon {
color: inherit;
}
.discussion-item-merge .discussion-item-icon {
background-color: #6e5494;
color: white;
padding-top: 1px;
padding-left: 3px;
}
.discussion-item-close .discussion-item-icon {
background-color: #bd2c00;
color: white;
padding-top: 1px;
}
.discussion-item-delete_branch .discussion-item-icon {
padding-left: 2px;
padding-top: 1px;
background-color: #767676;
color: white;
}
.discussion-item-reopen .discussion-item-icon {
background-color: #6cc644;
padding-top: 1px;
color: white;
}
/****************************************************************************/
/* Pull Request */
/****************************************************************************/
@@ -1331,6 +1425,59 @@ a.absent {
color: #c00;
}
/*
div.wiki-index-header {
background-color: #f5f5f5;
color: #333333;
margin: 0;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
border: 1px solid #d8d8d8;
padding: 8px 8px 8px 8px;
}
*/
div.wiki-sidebar {
background-color: white;
border: 1px solid #d8d8d8;
padding: 8px 10px 0px 10px;
border-radius: 3px;
margin-bottom: 20px;
margin-top: 20px;
}
div.wiki-sidebar img {
max-width: 100%;
}
div.wiki-sidebar-dotted {
background-color: white;
border: 1px dashed #ddd;
padding: 10px 15px;
margin-bottom: 20px;
margin-top: 20px;
border-radius: 3px;
font-size: 16px;
}
div.wiki-footer {
margin-top: 50px;
background-color: #f5f5f5;
color: gray;
}
/*
div.wiki-index-content {
background-color: white;
border: 1px solid #d8d8d8;
padding: 0px;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
margin-bottom: 20px;
border-top: none;
}
*/
/****************************************************************************/
/* Commit */
/****************************************************************************/
@@ -1351,13 +1498,43 @@ div.author-info div.committer {
margin: -10px -10px 10px -10px;
}
.build-statuses .build-status-item{
padding: 10px 15px 10px 12px;
padding: 10px 15px 10px 64px;
border-bottom: 1px solid #eee;
}
.build-statuses-list .build-status-item{
background-color: #fafafa;
}
.merge-indicator{
float:left;
border-radius: 50%;
width: 30px;
height: 30px;
line-height: 30px;
text-align: center;
vertical-align: middle;
margin-right: 10px;
}
.merge-indicator-success{
background-color: #6cc644;
}
.merge-indicator-warning{
background-color: #cea61b;
}
.merge-indicator-alert{
background-color: #888;
}
.merge-indicator .octicon{
color: white;
font-size: 16px;
width: 15px;
height: 15px;
}
.merge-indicator-warning .octicon{
color: white;
font-size: 30px;
}
/****************************************************************************/
/* Diff */
/****************************************************************************/
@@ -1661,28 +1838,39 @@ div.markdown-body h1 {
border-bottom: 1px solid #ddd;
font-size: 2.5em;
font-weight: bold;
line-height: 1.7;
}
div.markdown-body h2 {
border-bottom: 1px solid #eee;
font-size: 2em;
font-weight: bold;
line-height: 1.7;
}
div.markdown-body h3 {
font-size: 1.5em;
font-weight: bold;
line-height: 1.7;
}
div.markdown-body h4 {
font-size: 1.2em;
font-weight: bold;
line-height: 1.7;
}
div.markdown-body h5 {
font-size: 1em;
font-weight: bold;
line-height: 1.7;
}
div.markdown-body h6 {
color:#777;
font-size: 1em;
font-weight: bold;
line-height: 1.7;
}
div.markdown-body li {
@@ -1694,6 +1882,10 @@ div.markdown-body p {
line-height: 1.5;
}
div.markdown-body img {
max-width: 100%;
}
div.markdown-body pre {
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 12px;
@@ -1848,43 +2040,19 @@ div.markdown-body table colgroup + tbody tr:first-child td:last-child {
.markdown-head {
position: relative;
line-height: 1.7;
}
a.markdown-anchor-link {
position: absolute;
left: -18px;
display: none;
margin-left: -16px;
margin-right: 2px;
line-height: 1;
color: #999;
/* From octicon style */
font: normal normal normal 16px/1 octicons;
text-decoration: none;
text-rendering: auto;
}
a.markdown-anchor-link:before { content: '\f05c'} /*  */
h1 a.markdown-anchor-link {
top: 24px;
cursor: pointer;
}
h2 a.markdown-anchor-link {
top: 20px;
}
h3 a.markdown-anchor-link {
top: 12px;
}
h4 a.markdown-anchor-link {
top: 8px;
}
h5 a.markdown-anchor-link {
top: 6px;
}
h6 a.markdown-anchor-link {
top: 6px;
a.markdown-anchor-link span.octicon {
visibility: hidden;
vertical-align: middle;
}
/****************************************************************************/

Some files were not shown because too many files have changed in this diff Show More