From 5210a143fd782f356fb7d802fbf43f21319aa358 Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Sat, 30 Nov 2013 20:22:00 +0900 Subject: [PATCH 01/77] Update README.md --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index c6311fd0a..f56e0da98 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,14 @@ For Installation on Windows Server with IIS see [this wiki page](https://github. Release Notes -------- +### 1.8 - COMMING SOON! +- Add user and group deletion +- Improve pull request performance +- LDAP StartTLS support +- Hard wrap for Markdown +- Add new some options to specify the data directory +- Fix some bugs + ### 1.7 - 26 Oct 2013 - Support working on Java6 in embedded Jetty mode - Add ```--host``` option to bind specified host name in embedded Jetty mode From b60fe338869f8e11fc8375d088efe3eb29cbb529 Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Sat, 30 Nov 2013 20:26:31 +0900 Subject: [PATCH 02/77] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f56e0da98..4ec51b15c 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ or you can start GitBucket by ```java -jar gitbucket.war``` without servlet cont - --prefix=[CONTEXTPATH] - --host=[HOSTNAME] - --https=true +- --gitbucket.home=[DATA_DIR] To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk. @@ -53,6 +54,7 @@ Release Notes ### 1.8 - COMMING SOON! - Add user and group deletion - Improve pull request performance +- Pull request synchronization (when source repository is updated after pull request, it's applied to the pull request) - LDAP StartTLS support - Hard wrap for Markdown - Add new some options to specify the data directory From f86e50c7235099c1a8710e687f72277a42dc620c Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Sat, 30 Nov 2013 21:04:21 +0900 Subject: [PATCH 03/77] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4ec51b15c..c8f06d9cb 100644 --- a/README.md +++ b/README.md @@ -51,13 +51,13 @@ For Installation on Windows Server with IIS see [this wiki page](https://github. Release Notes -------- -### 1.8 - COMMING SOON! +### 1.8 - 30 Nov 2013 - Add user and group deletion - Improve pull request performance - Pull request synchronization (when source repository is updated after pull request, it's applied to the pull request) - LDAP StartTLS support - Hard wrap for Markdown -- Add new some options to specify the data directory +- Add new some options to specify the data directory. See details in [Wiki](https://github.com/takezoe/gitbucket/wiki/DirectoryStructure). - Fix some bugs ### 1.7 - 26 Oct 2013 From 608dce2205bd0c1d39d089b201d91a87ebf3e96c Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Sat, 30 Nov 2013 21:20:12 +0900 Subject: [PATCH 04/77] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c8f06d9cb..46f1a831c 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Release Notes - Improve pull request performance - Pull request synchronization (when source repository is updated after pull request, it's applied to the pull request) - LDAP StartTLS support -- Hard wrap for Markdown +- Enable hard wrapping in Markdown - Add new some options to specify the data directory. See details in [Wiki](https://github.com/takezoe/gitbucket/wiki/DirectoryStructure). - Fix some bugs From 6556d267420fc4dddcf746256c52f2fc48a291ce Mon Sep 17 00:00:00 2001 From: Ali Ayas Date: Sun, 1 Dec 2013 23:55:34 +0200 Subject: [PATCH 05/77] Turn off autocomplete on "Add collaborator" form You have already created js autocomplete for that input, so it is good to turn off the browser autocomplete. If there are more forms that have custom autocomplete, this change should be applied to them, too. --- src/main/twirl/settings/collaborators.scala.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/twirl/settings/collaborators.scala.html b/src/main/twirl/settings/collaborators.scala.html index 24b630cdc..8264764e6 100644 --- a/src/main/twirl/settings/collaborators.scala.html +++ b/src/main/twirl/settings/collaborators.scala.html @@ -18,7 +18,7 @@ } @if(!isGroupRepository){ -
+
From 26d579f13f154f23f931bd419996e3c876b63ee8 Mon Sep 17 00:00:00 2001 From: Drew Larson Date: Sun, 1 Dec 2013 19:56:21 -0600 Subject: [PATCH 06/77] Fixes issue #216 Added a div element to wrap the buttons so they are vertically aligned with each other. Also converted input and a elements to button elements as Bootstrap recommends: http://getbootstrap.com/css/#buttons-tags --- src/main/twirl/issues/editissue.scala.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/twirl/issues/editissue.scala.html b/src/main/twirl/issues/editissue.scala.html index 9c7bd733a..1fbf96a88 100644 --- a/src/main/twirl/issues/editissue.scala.html +++ b/src/main/twirl/issues/editissue.scala.html @@ -3,8 +3,10 @@ - -Cancel +
+ + +
\ No newline at end of file From 89bfcdc44e484938a734774fcd99dfd7b9ca6dc0 Mon Sep 17 00:00:00 2001 From: takezoe Date: Sat, 18 Jan 2014 06:44:39 +0900 Subject: [PATCH 37/77] (refs #102)Add validation and auto completion to the transfer user name field --- .../app/RepositorySettingsController.scala | 18 +++++++++++++++++- src/main/twirl/settings/transfer.scala.html | 19 +++++++------------ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/main/scala/app/RepositorySettingsController.scala b/src/main/scala/app/RepositorySettingsController.scala index 26a35fd8e..f47119d0f 100644 --- a/src/main/scala/app/RepositorySettingsController.scala +++ b/src/main/scala/app/RepositorySettingsController.scala @@ -48,7 +48,7 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo case class TransferOwnerShipForm(newOwner: String) val transferForm = mapping( - "newOwner" -> trim(label("New owner", text(required))) // TODO user and repository existence check + "newOwner" -> trim(label("New owner", text(required, transferUser))) )(TransferOwnerShipForm.apply) /** @@ -256,4 +256,20 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo } } + /** + * Provides Constraint to validate the repository transfer user. + */ + private def transferUser: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = + getAccountByUserName(value) match { + case None => Some("User does not exist.") + case Some(x) => if(x.userName == params("owner")){ + Some("This is current repository owner.") + } else { + params.get("repository").flatMap { repositoryName => + getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map{ _ => "User already has same repository." } + } + } + } + } } \ No newline at end of file diff --git a/src/main/twirl/settings/transfer.scala.html b/src/main/twirl/settings/transfer.scala.html index c6c8af861..4bf2ff33b 100644 --- a/src/main/twirl/settings/transfer.scala.html +++ b/src/main/twirl/settings/transfer.scala.html @@ -1,24 +1,19 @@ @(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) @import context._ @import view.helpers._ -@html.main("Delete Repository", Some(repository)){ +@html.main("Transfer Ownership", Some(repository)){ @html.header("settings", repository) @menu("transfer", repository){ - +

Transfer Ownership

- - + @helper.html.account("newOwner", 300) + +
+ +
- } } - \ No newline at end of file From 5b1aef5e52e7e370bbb9bf47b4fccb170722c1ce Mon Sep 17 00:00:00 2001 From: takezoe Date: Sat, 18 Jan 2014 07:06:48 +0900 Subject: [PATCH 38/77] (refs #101, #102)Put Repository deletion and transfer ownership together to Danger Zone. --- .../app/RepositorySettingsController.scala | 18 +++----- src/main/twirl/settings/danger.scala.html | 44 +++++++++++++++++++ src/main/twirl/settings/delete.scala.html | 22 ---------- src/main/twirl/settings/menu.scala.html | 7 +-- src/main/twirl/settings/transfer.scala.html | 19 -------- 5 files changed, 51 insertions(+), 59 deletions(-) create mode 100644 src/main/twirl/settings/danger.scala.html delete mode 100644 src/main/twirl/settings/delete.scala.html delete mode 100644 src/main/twirl/settings/transfer.scala.html diff --git a/src/main/scala/app/RepositorySettingsController.scala b/src/main/scala/app/RepositorySettingsController.scala index f47119d0f..d745623eb 100644 --- a/src/main/scala/app/RepositorySettingsController.scala +++ b/src/main/scala/app/RepositorySettingsController.scala @@ -174,14 +174,14 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo }) /** - * Display the transfer ownership page. + * Display the danger zone. */ - get("/:owner/:repository/settings/transfer")(ownerOnly { - settings.html.transfer(_) + get("/:owner/:repository/settings/danger")(ownerOnly { + settings.html.danger(_) }) /** - * Save the repository options. + * Transfer repository ownership. */ post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) => // Change repository owner @@ -197,15 +197,7 @@ trait RepositorySettingsControllerBase extends ControllerBase with FlashMapSuppo FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name)) } } -// flash += "info" -> "Repository ownership is transferred." - redirect(s"/${form.newOwner}/${repository.name}/settings/transfer") - }) - - /** - * Display the delete repository page. - */ - get("/:owner/:repository/settings/delete")(ownerOnly { - settings.html.delete(_) + redirect(s"/${form.newOwner}/${repository.name}") }) /** diff --git a/src/main/twirl/settings/danger.scala.html b/src/main/twirl/settings/danger.scala.html new file mode 100644 index 000000000..cdb77b138 --- /dev/null +++ b/src/main/twirl/settings/danger.scala.html @@ -0,0 +1,44 @@ +@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) +@import context._ +@import view.helpers._ +@html.main("Danger Zone", Some(repository)){ + @html.header("settings", repository) + @menu("danger", repository){ +
+
Danger Zone
+
+
+
+ +
+ Transfer this repo to another user or to group. +
+ @helper.html.account("newOwner", 150) + +
+ +
+
+
+
+
+
+
+ +
+ Once you delete a repository, there is no going back. + +
+
+
+
+
+ } +} + \ No newline at end of file diff --git a/src/main/twirl/settings/delete.scala.html b/src/main/twirl/settings/delete.scala.html deleted file mode 100644 index b78e071c0..000000000 --- a/src/main/twirl/settings/delete.scala.html +++ /dev/null @@ -1,22 +0,0 @@ -@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main("Delete Repository", Some(repository)){ - @html.header("settings", repository) - @menu("delete", repository){ -
-

Delete repository

-

- Once you delete a repository, there is no going back. -

- -
- } -} - \ No newline at end of file diff --git a/src/main/twirl/settings/menu.scala.html b/src/main/twirl/settings/menu.scala.html index f08ba41fe..616f4cf72 100644 --- a/src/main/twirl/settings/menu.scala.html +++ b/src/main/twirl/settings/menu.scala.html @@ -14,11 +14,8 @@ Service Hooks - - Transfer Ownership - - - Delete Repository + + Danger Zone diff --git a/src/main/twirl/settings/transfer.scala.html b/src/main/twirl/settings/transfer.scala.html deleted file mode 100644 index 4bf2ff33b..000000000 --- a/src/main/twirl/settings/transfer.scala.html +++ /dev/null @@ -1,19 +0,0 @@ -@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main("Transfer Ownership", Some(repository)){ - @html.header("settings", repository) - @menu("transfer", repository){ -
-

Transfer Ownership

-
- - @helper.html.account("newOwner", 300) - -
- -
-
-
- } -} From 5a1f541e13aca1a1a8b8f22d1a2698c8bcf992cf Mon Sep 17 00:00:00 2001 From: takezoe Date: Sat, 25 Jan 2014 05:07:32 +0900 Subject: [PATCH 39/77] (refs #245)Add full name attribute for LDAP authentication. --- .../scala/app/SystemSettingsController.scala | 1 + src/main/scala/service/AccountService.scala | 6 +- .../scala/service/SystemSettingsService.scala | 4 + src/main/scala/util/LDAPUtil.scala | 95 ++++++++++--------- src/main/twirl/admin/system.scala.html | 7 ++ 5 files changed, 65 insertions(+), 48 deletions(-) diff --git a/src/main/scala/app/SystemSettingsController.scala b/src/main/scala/app/SystemSettingsController.scala index 68efda53b..d6dd7875e 100644 --- a/src/main/scala/app/SystemSettingsController.scala +++ b/src/main/scala/app/SystemSettingsController.scala @@ -33,6 +33,7 @@ trait SystemSettingsControllerBase extends ControllerBase with FlashMapSupport { "bindPassword" -> trim(label("Bind Password", optional(text()))), "baseDN" -> trim(label("Base DN", text(required))), "userNameAttribute" -> trim(label("User name attribute", text(required))), + "fullNameAttribute" -> trim(label("Full name attribute", optional(text()))), "mailAttribute" -> trim(label("Mail address attribute", text(required))), "tls" -> trim(label("Enable TLS", optional(boolean()))), "keystore" -> trim(label("Keystore", optional(text()))) diff --git a/src/main/scala/service/AccountService.scala b/src/main/scala/service/AccountService.scala index 905736bd5..006a9be53 100644 --- a/src/main/scala/service/AccountService.scala +++ b/src/main/scala/service/AccountService.scala @@ -36,11 +36,11 @@ trait AccountService { */ private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) = { LDAPUtil.authenticate(settings.ldap.get, userName, password) match { - case Right(mailAddress) => { + case Right(ldapUserInfo) => { // Create or update account by LDAP information getAccountByUserName(userName) match { - case Some(x) => updateAccount(x.copy(mailAddress = mailAddress)) - case None => createAccount(userName, "", userName, mailAddress, false, None) + case Some(x) => updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName)) + case None => createAccount(userName, "", ldapUserInfo.fullName, ldapUserInfo.mailAddress, false, None) } getAccountByUserName(userName) } diff --git a/src/main/scala/service/SystemSettingsService.scala b/src/main/scala/service/SystemSettingsService.scala index 85e94f797..5514cbead 100644 --- a/src/main/scala/service/SystemSettingsService.scala +++ b/src/main/scala/service/SystemSettingsService.scala @@ -31,6 +31,7 @@ trait SystemSettingsService { ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x)) props.setProperty(LdapBaseDN, ldap.baseDN) props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute) + ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x)) props.setProperty(LdapMailAddressAttribute, ldap.mailAttribute) ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString)) ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x)) @@ -71,6 +72,7 @@ trait SystemSettingsService { getOptionValue(props, LdapBindPassword, None), getValue(props, LdapBaseDN, ""), getValue(props, LdapUserNameAttribute, ""), + getOptionValue(props, LdapFullNameAttribute, None), getValue(props, LdapMailAddressAttribute, ""), getOptionValue[Boolean](props, LdapTls, None), getOptionValue(props, LdapKeystore, None))) @@ -101,6 +103,7 @@ object SystemSettingsService { bindPassword: Option[String], baseDN: String, userNameAttribute: String, + fullNameAttribute: Option[String], mailAttribute: String, tls: Option[Boolean], keystore: Option[String]) @@ -134,6 +137,7 @@ object SystemSettingsService { private val LdapBindPassword = "ldap.bind_password" private val LdapBaseDN = "ldap.baseDN" private val LdapUserNameAttribute = "ldap.username_attribute" + private val LdapFullNameAttribute = "ldap.fullname_attribute" private val LdapMailAddressAttribute = "ldap.mail_attribute" private val LdapTls = "ldap.tls" private val LdapKeystore = "ldap.keystore" diff --git a/src/main/scala/util/LDAPUtil.scala b/src/main/scala/util/LDAPUtil.scala index b6859d2fb..05a717167 100644 --- a/src/main/scala/util/LDAPUtil.scala +++ b/src/main/scala/util/LDAPUtil.scala @@ -18,51 +18,49 @@ object LDAPUtil { /** * Try authentication by LDAP using given configuration. - * Returns Right(mailAddress) if authentication is successful, otherwise Left(errorMessage). + * Returns Right(LDAPUserInfo) if authentication is successful, otherwise Left(errorMessage). */ - def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, String] = { + def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, LDAPUserInfo] = { bind( - ldapSettings.host, - ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort), - ldapSettings.bindDN.getOrElse(""), - ldapSettings.bindPassword.getOrElse(""), - ldapSettings.tls.getOrElse(false), - ldapSettings.keystore.getOrElse("") - ) match { - case Some(conn) => { - withConnection(conn) { conn => - findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match { - case Some(userDN) => userAuthentication(ldapSettings, userDN, password) - case None => Left("User does not exist.") - } - } + host = ldapSettings.host, + port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort), + dn = ldapSettings.bindDN.getOrElse(""), + password = ldapSettings.bindPassword.getOrElse(""), + tls = ldapSettings.tls.getOrElse(false), + keystore = ldapSettings.keystore.getOrElse(""), + error = "System LDAP authentication failed." + ){ conn => + findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute) match { + case Some(userDN) => userAuthentication(ldapSettings, userDN, userName, password) + case None => Left("User does not exist.") } - case None => Left("System LDAP authentication failed.") } } - private def userAuthentication(ldapSettings: Ldap, userDN: String, password: String): Either[String, String] = { + private def userAuthentication(ldapSettings: Ldap, userDN: String, userName: String, password: String): Either[String, LDAPUserInfo] = { bind( - ldapSettings.host, - ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort), - userDN, - password, - ldapSettings.tls.getOrElse(false), - ldapSettings.keystore.getOrElse("") - ) match { - case Some(conn) => { - withConnection(conn) { conn => - findMailAddress(conn, userDN, ldapSettings.mailAttribute) match { - case Some(mailAddress) => Right(mailAddress) - case None => Left("Can't find mail address.") - } - } + host = ldapSettings.host, + port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort), + dn = userDN, + password = password, + tls = ldapSettings.tls.getOrElse(false), + keystore = ldapSettings.keystore.getOrElse(""), + error = "User LDAP Authentication Failed." + ){ conn => + findMailAddress(conn, userDN, ldapSettings.mailAttribute) match { + case Some(mailAddress) => Right(LDAPUserInfo( + userName = userName, + fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute => + findFullName(conn, userDN, fullNameAttribute) + }.getOrElse(userName), + mailAddress = mailAddress)) + case None => Left("Can't find mail address.") } - case None => Left("User LDAP Authentication Failed.") } } - private def bind(host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String): Option[LDAPConnection] = { + private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, keystore: String, error: String) + (f: LDAPConnection => Either[String, A]): Either[String, A] = { if (tls) { // Dynamically set Sun as the security provider Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider()) @@ -87,7 +85,9 @@ object LDAPUtil { // Bind to the server conn.bind(LDAP_VERSION, dn, password.getBytes) - Some(conn) + // Execute a given function and returns a its result + f(conn) + } catch { case e: Exception => { // Provide more information if something goes wrong @@ -96,20 +96,15 @@ object LDAPUtil { if (conn.isConnected) { conn.disconnect() } - - None + // Returns an error message + Left(error) } } } - private def withConnection[T](conn: LDAPConnection)(f: LDAPConnection => T): T = { - try { - f(conn) - } finally { - conn.disconnect() - } - } - + /** + * Search a specified user and returns userDN if exists. + */ private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String): Option[String] = { @tailrec def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = { @@ -134,4 +129,14 @@ object LDAPUtil { Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue) } else None } + + private def findFullName(conn: LDAPConnection, userDN: String, nameAttribute: String): Option[String] = + defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, null, Array[String](nameAttribute), false)){ results => + if(results.hasMore) { + Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue) + } else None + } + + case class LDAPUserInfo(userName: String, fullName: String, mailAddress: String) + } diff --git a/src/main/twirl/admin/system.scala.html b/src/main/twirl/admin/system.scala.html index 17e806299..1ccab70b5 100644 --- a/src/main/twirl/admin/system.scala.html +++ b/src/main/twirl/admin/system.scala.html @@ -94,6 +94,13 @@ +
+ +
+ + +
+
From 59a096bfd633c1abf01c0baecccc890d086c7f83 Mon Sep 17 00:00:00 2001 From: takezoe Date: Sat, 25 Jan 2014 05:25:17 +0900 Subject: [PATCH 40/77] (refs #250)Include repository name in download zip filename. --- src/main/scala/app/RepositoryViewerController.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala index 5fbfa7557..71e39e76d 100644 --- a/src/main/scala/app/RepositoryViewerController.scala +++ b/src/main/scala/app/RepositoryViewerController.scala @@ -175,7 +175,8 @@ trait RepositoryViewerControllerBase extends ControllerBase { } workDir.mkdirs - val zipFile = new File(workDir, (if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip") + val zipFile = new File(workDir, repository.name + "-" + + (if(revision.length == 40) revision.substring(0, 10) else revision) + ".zip") using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision)) @@ -204,6 +205,7 @@ trait RepositoryViewerControllerBase extends ControllerBase { } contentType = "application/octet-stream" + response.setHeader("Content-Disposition", s"attachment; filename=${zipFile.getName}") zipFile } else { BadRequest From 52fcc4ad1ecf846b5e42eb1c9e1310b3e46a5df7 Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Sun, 26 Jan 2014 08:43:34 +0900 Subject: [PATCH 41/77] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4983cd6c8..0e087bf90 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Release Notes ### 1.7 - 26 Oct 2013 - Support working on Java6 in embedded Jetty mode - Add ```--host``` option to bind specified host name in embedded Jetty mode -- Add ```--https=true``` option to use https in embedded Jetty mode +- Add ```--https=true``` option to force https scheme when using embedded Jetty mode at the back of https proxy - Add full name as user property - Change link color for absent Wiki pages - Add ZIP download button to the repository viewer tab From 05a91565dc162508b25b024ad435905545b565a3 Mon Sep 17 00:00:00 2001 From: shootaroo Date: Thu, 30 Jan 2014 15:16:29 +0900 Subject: [PATCH 42/77] Add id for line number --- src/main/webapp/assets/google-code-prettify/prettify.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/assets/google-code-prettify/prettify.js b/src/main/webapp/assets/google-code-prettify/prettify.js index 7b990496d..8f2eccfe6 100644 --- a/src/main/webapp/assets/google-code-prettify/prettify.js +++ b/src/main/webapp/assets/google-code-prettify/prettify.js @@ -12,7 +12,7 @@ q,"'\"`"]):d.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(? s+")*(?:\\x5D|$))+/")+")")])}(b=a.types)&&g.push(["typ",b]);b=(""+a.keywords).replace(/^ | $/g,"");b.length&&g.push(["kwd",RegExp("^(?:"+b.replace(/[\s,]+/g,"|")+")\\b"),q]);d.push(["pln",/^\s+/,q," \r\n\t\u00a0"]);b="^.[^\\s\\w.$@'\"`/\\\\]*";a.regexLiterals&&(b+="(?!s*/)");g.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/, q],["pun",RegExp(b),q]);return C(d,g)}function J(a,d,g){function b(a){var c=a.nodeType;if(c==1&&!x.test(a.className))if("br"===a.nodeName)s(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)b(a);else if((c==3||c==4)&&g){var d=a.nodeValue,i=d.match(m);if(i)c=d.substring(0,i.index),a.nodeValue=c,(d=d.substring(i.index+i[0].length))&&a.parentNode.insertBefore(j.createTextNode(d),a.nextSibling),s(a),c||a.parentNode.removeChild(a)}}function s(a){function b(a,c){var d= c?a.cloneNode(!1):a,e=a.parentNode;if(e){var e=b(e,1),g=a.nextSibling;e.appendChild(d);for(var i=g;i;i=g)g=i.nextSibling,e.appendChild(i)}return d}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),d;(d=a.parentNode)&&d.nodeType===1;)a=d;c.push(a)}for(var x=/(?:^|\s)nocode(?:\s|$)/,m=/\r\n?|\n/,j=a.ownerDocument,k=j.createElement("li");a.firstChild;)k.appendChild(a.firstChild);for(var c=[k],i=0;i=0;){var b=d[g];F.hasOwnProperty(b)?D.console&&console.warn("cannot override language handler %s",b):F[b]=a}}function I(a,d){if(!a||!F.hasOwnProperty(a))a=/^\s*=0;){var b=d[g];F.hasOwnProperty(b)?D.console&&console.warn("cannot override language handler %s",b):F[b]=a}}function I(a,d){if(!a||!F.hasOwnProperty(a))a=/^\s*=l&&(b+=2);g>=B&&(r+=2)}}finally{if(f)f.style.display=h}}catch(u){D.console&&console.log(u&&u.stack||u)}}var D=window,y=["break,continue,do,else,for,if,return,while"],E=[[y,"auto,case,char,const,default,double,enum,extern,float,goto,inline,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],M=[E,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,delegate,dynamic_cast,explicit,export,friend,generic,late_check,mutable,namespace,nullptr,property,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],N=[E,"abstract,assert,boolean,byte,extends,final,finally,implements,import,instanceof,interface,null,native,package,strictfp,super,synchronized,throws,transient"], From 913561cb2ad3cbda8ca502ed8a26edff681ed2fc Mon Sep 17 00:00:00 2001 From: takezoe Date: Thu, 30 Jan 2014 20:42:53 +0900 Subject: [PATCH 43/77] (refs #254)Remove AUTO_SERVER=TRUE for performance issue. --- src/main/scala/servlet/AutoUpdateListener.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala index 8a1170b2c..4383ff854 100644 --- a/src/main/scala/servlet/AutoUpdateListener.scala +++ b/src/main/scala/servlet/AutoUpdateListener.scala @@ -121,7 +121,7 @@ class AutoUpdateListener extends ServletContextListener { System.setProperty("gitbucket.home", datadir) } org.h2.Driver.load() - event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};AUTO_SERVER=TRUE") + event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome}") logger.debug("Start schema update") defining(getConnection(event.getServletContext)){ conn => From 738b0cfe9a59455e632e4866fbf2567a83f5bb92 Mon Sep 17 00:00:00 2001 From: takezoe Date: Sat, 1 Feb 2014 06:11:18 +0900 Subject: [PATCH 44/77] Add version 1.10. --- src/main/scala/servlet/AutoUpdateListener.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala index 8a1170b2c..b2ee771d2 100644 --- a/src/main/scala/servlet/AutoUpdateListener.scala +++ b/src/main/scala/servlet/AutoUpdateListener.scala @@ -50,6 +50,7 @@ object AutoUpdate { * The history of versions. A head of this sequence is the current BitBucket version. */ val versions = Seq( + Version(1, 10), Version(1, 9), Version(1, 8), Version(1, 7), From 1c529eea3dc6bb0b9af6b16bc681997258de2695 Mon Sep 17 00:00:00 2001 From: takezoe Date: Sat, 1 Feb 2014 07:06:17 +0900 Subject: [PATCH 45/77] Disable the post commit hook for Wiki repository. --- .../scala/servlet/GitRepositoryServlet.scala | 143 ++++++++++-------- 1 file changed, 76 insertions(+), 67 deletions(-) diff --git a/src/main/scala/servlet/GitRepositoryServlet.scala b/src/main/scala/servlet/GitRepositoryServlet.scala index 7fc940353..712cd8121 100644 --- a/src/main/scala/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/servlet/GitRepositoryServlet.scala @@ -69,7 +69,9 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] logger.debug("repository:" + owner + "/" + repository) logger.debug("baseURL:" + baseURL) - receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseURL)) + if(!repository.endsWith(".wiki")){ + receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseURL)) + } receivePack } } @@ -83,78 +85,85 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseURL: private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = { - using(Git.open(Directory.getRepositoryDir(owner, repository))) { git => - commands.asScala.foreach { command => - logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}") - val commits = command.getType match { - case ReceiveCommand.Type.DELETE => Nil - case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name) - } - val refName = command.getRefName.split("/") - val branchName = refName.drop(2).mkString("/") - - // Extract new commit and apply issue comment - val newCommits = if(commits.size > 1000){ - val existIds = getAllCommitIds(owner, repository) - commits.flatMap { commit => - if(!existIds.contains(commit.id)){ - createIssueComment(commit) - Some(commit) - } else None + try { + using(Git.open(Directory.getRepositoryDir(owner, repository))) { git => + commands.asScala.foreach { command => + logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}") + val commits = command.getType match { + case ReceiveCommand.Type.DELETE => Nil + case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name) } - } else { - commits.flatMap { commit => - if(!existsCommitId(owner, repository, commit.id)){ - createIssueComment(commit) - Some(commit) - } else None - } - } + val refName = command.getRefName.split("/") + val branchName = refName.drop(2).mkString("/") - // batch insert all new commit id - insertAllCommitIds(owner, repository, newCommits.map(_.id)) - - // record activity - if(refName(1) == "heads"){ - command.getType match { - case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, pusher, branchName) - case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, pusher, branchName, newCommits) - case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, pusher, branchName) - case _ => - } - } else if(refName(1) == "tags"){ - command.getType match { - case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, pusher, branchName, newCommits) - case ReceiveCommand.Type.DELETE => recordDeleteTagActivity(owner, repository, pusher, branchName, newCommits) - case _ => - } - } - - if(refName(1) == "heads"){ - command.getType match { - case ReceiveCommand.Type.CREATE | - ReceiveCommand.Type.UPDATE | - ReceiveCommand.Type.UPDATE_NONFASTFORWARD => - updatePullRequests(branchName) - case _ => - } - } - - // call web hook - getWebHookURLs(owner, repository) match { - case webHookURLs if(webHookURLs.nonEmpty) => - for(pusherAccount <- getAccountByUserName(pusher); - ownerAccount <- getAccountByUserName(owner); - repositoryInfo <- getRepository(owner, repository, baseURL)){ - callWebHook(owner, repository, webHookURLs, - WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount)) + // Extract new commit and apply issue comment + val newCommits = if(commits.size > 1000){ + val existIds = getAllCommitIds(owner, repository) + commits.flatMap { commit => + if(!existIds.contains(commit.id)){ + createIssueComment(commit) + Some(commit) + } else None } - case _ => + } else { + commits.flatMap { commit => + if(!existsCommitId(owner, repository, commit.id)){ + createIssueComment(commit) + Some(commit) + } else None + } + } + + // batch insert all new commit id + insertAllCommitIds(owner, repository, newCommits.map(_.id)) + + // record activity + if(refName(1) == "heads"){ + command.getType match { + case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, pusher, branchName) + case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, pusher, branchName, newCommits) + case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, pusher, branchName) + case _ => + } + } else if(refName(1) == "tags"){ + command.getType match { + case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, pusher, branchName, newCommits) + case ReceiveCommand.Type.DELETE => recordDeleteTagActivity(owner, repository, pusher, branchName, newCommits) + case _ => + } + } + + if(refName(1) == "heads"){ + command.getType match { + case ReceiveCommand.Type.CREATE | + ReceiveCommand.Type.UPDATE | + ReceiveCommand.Type.UPDATE_NONFASTFORWARD => + updatePullRequests(branchName) + case _ => + } + } + + // call web hook + getWebHookURLs(owner, repository) match { + case webHookURLs if(webHookURLs.nonEmpty) => + for(pusherAccount <- getAccountByUserName(pusher); + ownerAccount <- getAccountByUserName(owner); + repositoryInfo <- getRepository(owner, repository, baseURL)){ + callWebHook(owner, repository, webHookURLs, + WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount)) + } + case _ => + } } } + // update repository last modified time. + updateLastActivityDate(owner, repository) + } catch { + case ex: Exception => { + logger.error(ex.toString, ex) + throw ex + } } - // update repository last modified time. - updateLastActivityDate(owner, repository) } private def createIssueComment(commit: CommitInfo) = { From e87c69f989013982c9078e3f7aa1de48dcdce333 Mon Sep 17 00:00:00 2001 From: takezoe Date: Sat, 1 Feb 2014 07:08:03 +0900 Subject: [PATCH 46/77] (refs #251)Remove BOM from UTF-8 string. --- src/main/scala/service/WikiService.scala | 6 ++++-- src/main/scala/util/FileUtil.scala | 4 ++-- src/main/scala/util/StringUtil.scala | 9 ++++++++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/scala/service/WikiService.scala b/src/main/scala/service/WikiService.scala index 0988ad2df..00fbabd99 100644 --- a/src/main/scala/service/WikiService.scala +++ b/src/main/scala/service/WikiService.scala @@ -3,7 +3,7 @@ package service import java.util.Date import org.eclipse.jgit.api.Git import org.apache.commons.io.FileUtils -import util.{PatchUtil, Directory, JGitUtil, LockUtil} +import util._ import _root_.util.ControlUtil._ import org.eclipse.jgit.treewalk.{TreeWalk, CanonicalTreeParser} import org.eclipse.jgit.lib._ @@ -14,6 +14,7 @@ import java.io.ByteArrayInputStream import org.eclipse.jgit.patch._ import org.eclipse.jgit.api.errors.PatchFormatException import scala.collection.JavaConverters._ +import scala.Some object WikiService { @@ -61,7 +62,8 @@ trait WikiService { using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => if(!JGitUtil.isEmpty(git)){ JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file => - WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time, file.commitId) + WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes), + file.committer, file.time, file.commitId) } } else None } diff --git a/src/main/scala/util/FileUtil.scala b/src/main/scala/util/FileUtil.scala index 0b1a98b56..e4c052f93 100644 --- a/src/main/scala/util/FileUtil.scala +++ b/src/main/scala/util/FileUtil.scala @@ -63,9 +63,9 @@ object FileUtil { if(dir.exists()){ FileUtils.deleteDirectory(dir) } - try{ + try { action(dir) - }finally{ + } finally { FileUtils.deleteDirectory(dir) } } diff --git a/src/main/scala/util/StringUtil.scala b/src/main/scala/util/StringUtil.scala index 61c5c1855..55c923a88 100644 --- a/src/main/scala/util/StringUtil.scala +++ b/src/main/scala/util/StringUtil.scala @@ -3,6 +3,8 @@ package util import java.net.{URLDecoder, URLEncoder} import org.mozilla.universalchardet.UniversalDetector import util.ControlUtil._ +import org.apache.commons.io.input.BOMInputStream +import org.apache.commons.io.IOUtils object StringUtil { @@ -27,7 +29,12 @@ object StringUtil { def escapeHtml(value: String): String = value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """) - def convertFromByteArray(content: Array[Byte]): String = new String(content, detectEncoding(content)) + /** + * Make string from byte array. Character encoding is detected automatically by [[util.StringUtil.detectEncoding]]. + * And if given bytes contains UTF-8 BOM, it's removed from returned string.. + */ + def convertFromByteArray(content: Array[Byte]): String = + IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content)) def detectEncoding(content: Array[Byte]): String = defining(new UniversalDetector(null)){ detector => From b885a1a0d4daf10697e377f6222df78f4ebfed7a Mon Sep 17 00:00:00 2001 From: takezoe Date: Sat, 1 Feb 2014 17:05:33 +0900 Subject: [PATCH 47/77] (refs #256)If account is already registered but disabled, authentication fails. --- src/main/scala/service/AccountService.scala | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/scala/service/AccountService.scala b/src/main/scala/service/AccountService.scala index 006a9be53..84e78a1a4 100644 --- a/src/main/scala/service/AccountService.scala +++ b/src/main/scala/service/AccountService.scala @@ -38,9 +38,13 @@ trait AccountService { LDAPUtil.authenticate(settings.ldap.get, userName, password) match { case Right(ldapUserInfo) => { // Create or update account by LDAP information - getAccountByUserName(userName) match { - case Some(x) => updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName)) - case None => createAccount(userName, "", ldapUserInfo.fullName, ldapUserInfo.mailAddress, false, None) + getAccountByUserName(userName, true) match { + case Some(x) if(!x.isRemoved) => updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName)) + case Some(x) if(x.isRemoved) => { + logger.info(s"LDAP Authentication Failed: Account is already registered but disabled..") + defaultAuthentication(userName, password) + } + case None => createAccount(userName, "", ldapUserInfo.fullName, ldapUserInfo.mailAddress, false, None) } getAccountByUserName(userName) } From 5d5a4cacb1a32afc41d24750ed781aaf08747aa2 Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Sat, 1 Feb 2014 17:13:18 +0900 Subject: [PATCH 48/77] Update README.md --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 0e087bf90..03b482c98 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,13 @@ Run the following commands in `Terminal` to Release Notes -------- +### 1.10 - 01 Feb 2014 +- Rename repository +- Transfer repository owner +- Change default data directory to ```HOME/.gitbucket``` from ```HOME/gitbucket`` to avoid problem like #243, but if data directory already exist at HOME/gitbucket, it continues being used. +- Response improvement +- Fix some bugs + ### 1.9 - 28 Dec 2013 - Display GITBUCKET_HOME on the system settings page - Fix some bugs From 520e5ebb7a56a268473d40d8806f375e8e229055 Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Sat, 1 Feb 2014 17:14:41 +0900 Subject: [PATCH 49/77] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 03b482c98..2c8b6c2aa 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Installation The default administrator account is **root** and password is **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. +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. - --port=[NUMBER] - --prefix=[CONTEXTPATH] @@ -62,7 +62,7 @@ Release Notes ### 1.10 - 01 Feb 2014 - Rename repository - Transfer repository owner -- Change default data directory to ```HOME/.gitbucket``` from ```HOME/gitbucket`` to avoid problem like #243, but if data directory already exist at HOME/gitbucket, it continues being used. +- Change default data directory to `HOME/.gitbucket` from `HOME/gitbucket` to avoid problem like #243, but if data directory already exist at HOME/gitbucket, it continues being used. - Response improvement - Fix some bugs @@ -81,8 +81,8 @@ Release Notes ### 1.7 - 26 Oct 2013 - Support working on Java6 in embedded Jetty mode -- Add ```--host``` option to bind specified host name in embedded Jetty mode -- Add ```--https=true``` option to force https scheme when using embedded Jetty mode at the back of https proxy +- Add `--host` option to bind specified host name in embedded Jetty mode +- Add `--https=true` option to force https scheme when using embedded Jetty mode at the back of https proxy - Add full name as user property - Change link color for absent Wiki pages - Add ZIP download button to the repository viewer tab From a08316bba06f9620ebc72e5f8d789bf177be5ca0 Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Sat, 1 Feb 2014 17:15:50 +0900 Subject: [PATCH 50/77] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2c8b6c2aa..8e4975685 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ Release Notes - Rename repository - Transfer repository owner - Change default data directory to `HOME/.gitbucket` from `HOME/gitbucket` to avoid problem like #243, but if data directory already exist at HOME/gitbucket, it continues being used. +- Add LDAP display name attribute - Response improvement - Fix some bugs From 202c920064b3df9502848d9c914b26517e857f40 Mon Sep 17 00:00:00 2001 From: bati11 Date: Sun, 2 Feb 2014 02:47:09 +0900 Subject: [PATCH 51/77] Fix #257, "org.scalatra.ForceHttps" set to true, if --https=true ScalatraBase.redirect() use "org.scalatra.ForceHttps" in servlet context init parameter when choice 'http' or 'https'. --- src/main/java/JettyLauncher.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/JettyLauncher.java b/src/main/java/JettyLauncher.java index a644f1fa6..d7b137d6f 100644 --- a/src/main/java/JettyLauncher.java +++ b/src/main/java/JettyLauncher.java @@ -53,6 +53,9 @@ public class JettyLauncher { context.setDescriptor(location.toExternalForm() + "/WEB-INF/web.xml"); context.setServer(server); context.setWar(location.toExternalForm()); + if (forceHttps) { + context.setInitParameter("org.scalatra.ForceHttps", "true"); + } server.setHandler(context); server.start(); From 2764caae29bc32101866a76658e0bbaab594ea23 Mon Sep 17 00:00:00 2001 From: takezoe Date: Mon, 3 Feb 2014 08:00:43 +0900 Subject: [PATCH 52/77] (refs #224)Add delete branch button --- .../app/RepositoryViewerController.scala | 22 +++++++++++++++---- src/main/twirl/repo/branches.scala.html | 17 ++++++++++---- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala index 71e39e76d..b7cb49048 100644 --- a/src/main/scala/app/RepositoryViewerController.scala +++ b/src/main/scala/app/RepositoryViewerController.scala @@ -3,7 +3,7 @@ package app import util.Directory._ import util.Implicits._ import util.ControlUtil._ -import _root_.util.{ReferrerAuthenticator, JGitUtil, FileUtil, StringUtil} +import _root_.util._ import service._ import org.scalatra._ import java.io.File @@ -12,15 +12,16 @@ import org.eclipse.jgit.lib._ import org.apache.commons.io.FileUtils import org.eclipse.jgit.treewalk._ import java.util.zip.{ZipEntry, ZipOutputStream} +import scala.Some class RepositoryViewerController extends RepositoryViewerControllerBase - with RepositoryService with AccountService with ReferrerAuthenticator + with RepositoryService with AccountService with ReferrerAuthenticator with CollaboratorsAuthenticator /** * The repository viewer. */ trait RepositoryViewerControllerBase extends ControllerBase { - self: RepositoryService with AccountService with ReferrerAuthenticator => + self: RepositoryService with AccountService with ReferrerAuthenticator with CollaboratorsAuthenticator => /** * Returns converted HTML from Markdown for preview. @@ -150,10 +151,23 @@ trait RepositoryViewerControllerBase extends ControllerBase { val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next (branchName, revCommit.getCommitterIdent.getWhen) } - repo.html.branches(branchInfo, repository) + repo.html.branches(branchInfo, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository) } }) + /** + * Deletes branch. + */ + get("/:owner/:repository/delete/:branchName")(collaboratorsOnly { repository => + val branchName = params("branchName") + if(repository.repository.defaultBranch != branchName){ + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + git.branchDelete().setBranchNames(branchName).call() + } + } + redirect(s"/${repository.owner}/${repository.name}/branches") + }) + /** * Displays tags. */ diff --git a/src/main/twirl/repo/branches.scala.html b/src/main/twirl/repo/branches.scala.html index 5a2cfb07f..219c7c116 100644 --- a/src/main/twirl/repo/branches.scala.html +++ b/src/main/twirl/repo/branches.scala.html @@ -1,4 +1,5 @@ @(branchInfo: List[(String, java.util.Date)], + hasWritePermission: Boolean, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) @import context._ @import view.helpers._ @@ -17,9 +18,9 @@ @branchName - @* - Delete branch - *@ + @if(hasWritePermission && repository.repository.defaultBranch != branchName){ + Delete branch + } @datetime(latestUpdateDate) @@ -35,4 +36,12 @@ } -} \ No newline at end of file +} + \ No newline at end of file From 7629e347dfb0f15a4839e702de78aa43a544484b Mon Sep 17 00:00:00 2001 From: takezoe Date: Mon, 3 Feb 2014 08:06:04 +0900 Subject: [PATCH 53/77] (refs #224)Record delete branch activity --- src/main/scala/app/RepositoryViewerController.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala index b7cb49048..272613682 100644 --- a/src/main/scala/app/RepositoryViewerController.scala +++ b/src/main/scala/app/RepositoryViewerController.scala @@ -15,13 +15,13 @@ import java.util.zip.{ZipEntry, ZipOutputStream} import scala.Some class RepositoryViewerController extends RepositoryViewerControllerBase - with RepositoryService with AccountService with ReferrerAuthenticator with CollaboratorsAuthenticator + with RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator /** * The repository viewer. */ trait RepositoryViewerControllerBase extends ControllerBase { - self: RepositoryService with AccountService with ReferrerAuthenticator with CollaboratorsAuthenticator => + self: RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator => /** * Returns converted HTML from Markdown for preview. @@ -160,9 +160,11 @@ trait RepositoryViewerControllerBase extends ControllerBase { */ get("/:owner/:repository/delete/:branchName")(collaboratorsOnly { repository => val branchName = params("branchName") + val userName = context.loginAccount.get.userName if(repository.repository.defaultBranch != branchName){ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => git.branchDelete().setBranchNames(branchName).call() + recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName) } } redirect(s"/${repository.owner}/${repository.name}/branches") From c2eb53d1548309396059f3142c33bb98bcfc8f35 Mon Sep 17 00:00:00 2001 From: takezoe Date: Tue, 4 Feb 2014 09:04:25 +0900 Subject: [PATCH 54/77] (refs #224)Add delete branch button to pull request from same repository. --- .../scala/app/PullRequestsController.scala | 15 ++++++++++++++ src/main/twirl/issues/commentlist.scala.html | 9 ++++++++- src/main/twirl/pulls/discussion.scala.html | 20 +++++++++++++++++-- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/main/scala/app/PullRequestsController.scala b/src/main/scala/app/PullRequestsController.scala index a85d9907d..774977e88 100644 --- a/src/main/scala/app/PullRequestsController.scala +++ b/src/main/scala/app/PullRequestsController.scala @@ -103,6 +103,21 @@ trait PullRequestsControllerBase extends ControllerBase { } getOrElse NotFound }) + get("/:owner/:repository/pull/:id/delete/:branchName")(collaboratorsOnly { repository => + params("id").toIntOpt.map { issueId => + val branchName = params("branchName") + val userName = context.loginAccount.get.userName + if(repository.repository.defaultBranch != branchName){ + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + git.branchDelete().setBranchNames(branchName).call() + recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName) + } + } + createComment(repository.owner, repository.name, userName, issueId, "Delete branch", "delete") + 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 diff --git a/src/main/twirl/issues/commentlist.scala.html b/src/main/twirl/issues/commentlist.scala.html index edcd8a21c..92f604106 100644 --- a/src/main/twirl/issues/commentlist.scala.html +++ b/src/main/twirl/issues/commentlist.scala.html @@ -6,7 +6,7 @@ @import context._ @import view.helpers._ @comments.map { comment => - @if(comment.action != "close" && comment.action != "reopen"){ + @if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete"){
@avatar(comment.commentedUserName, 48)
@@ -63,6 +63,13 @@ @user(comment.commentedUserName, styleClass="username strong") reopened the issue @datetime(comment.registeredDate)
} + @if(comment.action == "delete"){ +
+ Deleted + @avatar(comment.commentedUserName, 20) + @user(comment.commentedUserName, styleClass="username strong") deleted the @pullreq.map(_.requestBranch) branch @datetime(comment.registeredDate) +
+ } } \ No newline at end of file From 9d7235af20a37ad6f8e77c2eabdbd35e97a0a204 Mon Sep 17 00:00:00 2001 From: takezoe Date: Tue, 4 Feb 2014 17:06:54 +0900 Subject: [PATCH 55/77] (refs #254)Store removed branch name into CONTENT column of COMMENT table. --- src/main/scala/app/PullRequestsController.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/app/PullRequestsController.scala b/src/main/scala/app/PullRequestsController.scala index 774977e88..806d8f864 100644 --- a/src/main/scala/app/PullRequestsController.scala +++ b/src/main/scala/app/PullRequestsController.scala @@ -113,7 +113,7 @@ trait PullRequestsControllerBase extends ControllerBase { recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName) } } - createComment(repository.owner, repository.name, userName, issueId, "Delete branch", "delete") + createComment(repository.owner, repository.name, userName, issueId, branchName, "delete") redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") } getOrElse NotFound }) From d882f20436198bd22b5eb4efb1c5664f187a2cec Mon Sep 17 00:00:00 2001 From: takezoe Date: Tue, 4 Feb 2014 17:20:48 +0900 Subject: [PATCH 56/77] (refs #254)Change comment action to "delete_branch" from "delete". --- src/main/scala/app/PullRequestsController.scala | 2 +- src/main/twirl/issues/commentlist.scala.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/app/PullRequestsController.scala b/src/main/scala/app/PullRequestsController.scala index 806d8f864..c3d110a45 100644 --- a/src/main/scala/app/PullRequestsController.scala +++ b/src/main/scala/app/PullRequestsController.scala @@ -113,7 +113,7 @@ trait PullRequestsControllerBase extends ControllerBase { recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName) } } - createComment(repository.owner, repository.name, userName, issueId, branchName, "delete") + createComment(repository.owner, repository.name, userName, issueId, branchName, "delete_branch") redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") } getOrElse NotFound }) diff --git a/src/main/twirl/issues/commentlist.scala.html b/src/main/twirl/issues/commentlist.scala.html index 92f604106..7bc168fb4 100644 --- a/src/main/twirl/issues/commentlist.scala.html +++ b/src/main/twirl/issues/commentlist.scala.html @@ -6,7 +6,7 @@ @import context._ @import view.helpers._ @comments.map { comment => - @if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete"){ + @if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){
@avatar(comment.commentedUserName, 48)
@@ -63,7 +63,7 @@ @user(comment.commentedUserName, styleClass="username strong") reopened the issue @datetime(comment.registeredDate)
} - @if(comment.action == "delete"){ + @if(comment.action == "delete_branch"){
Deleted @avatar(comment.commentedUserName, 20) From 4366c512fe827f49f5c6d316acea60073f396851 Mon Sep 17 00:00:00 2001 From: takezoe Date: Wed, 5 Feb 2014 03:07:37 +0900 Subject: [PATCH 57/77] (refs #265)Label editing for the pull request. --- .../scala/app/PullRequestsController.scala | 10 ++-- src/main/twirl/issues/issue.scala.html | 46 +---------------- src/main/twirl/issues/labels.scala.html | 51 +++++++++++++++++++ src/main/twirl/pulls/discussion.scala.html | 3 ++ src/main/twirl/pulls/pullreq.scala.html | 4 +- 5 files changed, 64 insertions(+), 50 deletions(-) create mode 100644 src/main/twirl/issues/labels.scala.html diff --git a/src/main/scala/app/PullRequestsController.scala b/src/main/scala/app/PullRequestsController.scala index c3d110a45..1f8453c52 100644 --- a/src/main/scala/app/PullRequestsController.scala +++ b/src/main/scala/app/PullRequestsController.scala @@ -21,12 +21,12 @@ import org.eclipse.jgit.errors.NoMergeBaseException import service.WebHookService.WebHookPayload class PullRequestsController extends PullRequestsControllerBase - with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with ActivityService with WebHookService - with ReferrerAuthenticator with CollaboratorsAuthenticator + with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService + with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator trait PullRequestsControllerBase extends ControllerBase { - self: RepositoryService with AccountService with IssuesService with MilestonesService with ActivityService with PullRequestService with WebHookService - with ReferrerAuthenticator with CollaboratorsAuthenticator => + self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService + with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator => private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase]) @@ -79,8 +79,10 @@ trait PullRequestsControllerBase extends ControllerBase { pulls.html.pullreq( issue, pullreq, getComments(owner, name, issueId), + getIssueLabels(owner, name, issueId.toInt), (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, getMilestonesWithIssueCount(owner, name), + getLabels(owner, name), commits, diffs, hasWritePermission(owner, name, context.loginAccount), diff --git a/src/main/twirl/issues/issue.scala.html b/src/main/twirl/issues/issue.scala.html index bb8c8dfea..fdeda57d8 100644 --- a/src/main/twirl/issues/issue.scala.html +++ b/src/main/twirl/issues/issue.scala.html @@ -33,51 +33,7 @@ }

-
- Labels - @if(hasWritePermission){ -
- @helper.html.dropdown() { - @labels.map { label => -
  • - - @helper.html.checkicon(issueLabels.exists(_.labelId == label.labelId)) -   - @label.labelName - -
  • - } - } -
    - } -
    - + @issues.html.labels(issue, issueLabels, labels, hasWritePermission, repository)
    } - \ No newline at end of file diff --git a/src/main/twirl/issues/labels.scala.html b/src/main/twirl/issues/labels.scala.html new file mode 100644 index 000000000..187e68e99 --- /dev/null +++ b/src/main/twirl/issues/labels.scala.html @@ -0,0 +1,51 @@ +@(issue: model.Issue, + issueLabels: List[model.Label], + labels: List[model.Label], + hasWritePermission: Boolean, + repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) +@import view.helpers._ +
    + Labels + @if(hasWritePermission){ +
    + @helper.html.dropdown() { + @labels.map { label => +
  • + + @helper.html.checkicon(issueLabels.exists(_.labelId == label.labelId)) +   + @label.labelName + +
  • + } + } +
    + } +
    + + diff --git a/src/main/twirl/pulls/discussion.scala.html b/src/main/twirl/pulls/discussion.scala.html index 85e904070..2927caa83 100644 --- a/src/main/twirl/pulls/discussion.scala.html +++ b/src/main/twirl/pulls/discussion.scala.html @@ -1,8 +1,10 @@ @(issue: model.Issue, pullreq: model.PullRequest, comments: List[model.IssueComment], + issueLabels: List[model.Label], collaborators: List[String], milestones: List[(model.Milestone, Int, Int)], + labels: List[model.Label], hasWritePermission: Boolean, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) @import context._ @@ -63,6 +65,7 @@ @comments.size @plural(comments.size, "comment")

    + @issues.html.labels(issue, issueLabels, labels, hasWritePermission, repository)
    + \ No newline at end of file diff --git a/src/main/webapp/assets/common/css/gitbucket.css b/src/main/webapp/assets/common/css/gitbucket.css index fc503a904..4c69e737f 100644 --- a/src/main/webapp/assets/common/css/gitbucket.css +++ b/src/main/webapp/assets/common/css/gitbucket.css @@ -480,6 +480,10 @@ pre.blob { padding: 30px; } +li.highlight { + background-color: #ffb; +} + /****************************************************************************/ /* Issues */ /****************************************************************************/ diff --git a/src/main/webapp/assets/common/js/jquery.ba-hashchange.js b/src/main/webapp/assets/common/js/jquery.ba-hashchange.js new file mode 100644 index 000000000..47105f4ab --- /dev/null +++ b/src/main/webapp/assets/common/js/jquery.ba-hashchange.js @@ -0,0 +1,390 @@ +/*! + * jQuery hashchange event - v1.3 - 7/21/2010 + * http://benalman.com/projects/jquery-hashchange-plugin/ + * + * Copyright (c) 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ + +// Script: jQuery hashchange event +// +// *Version: 1.3, Last updated: 7/21/2010* +// +// Project Home - http://benalman.com/projects/jquery-hashchange-plugin/ +// GitHub - http://github.com/cowboy/jquery-hashchange/ +// Source - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.js +// (Minified) - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.min.js (0.8kb gzipped) +// +// About: License +// +// Copyright (c) 2010 "Cowboy" Ben Alman, +// Dual licensed under the MIT and GPL licenses. +// http://benalman.com/about/license/ +// +// About: Examples +// +// These working examples, complete with fully commented code, illustrate a few +// ways in which this plugin can be used. +// +// hashchange event - http://benalman.com/code/projects/jquery-hashchange/examples/hashchange/ +// document.domain - http://benalman.com/code/projects/jquery-hashchange/examples/document_domain/ +// +// About: Support and Testing +// +// Information about what version or versions of jQuery this plugin has been +// tested with, what browsers it has been tested in, and where the unit tests +// reside (so you can test it yourself). +// +// jQuery Versions - 1.2.6, 1.3.2, 1.4.1, 1.4.2 +// Browsers Tested - Internet Explorer 6-8, Firefox 2-4, Chrome 5-6, Safari 3.2-5, +// Opera 9.6-10.60, iPhone 3.1, Android 1.6-2.2, BlackBerry 4.6-5. +// Unit Tests - http://benalman.com/code/projects/jquery-hashchange/unit/ +// +// About: Known issues +// +// While this jQuery hashchange event implementation is quite stable and +// robust, there are a few unfortunate browser bugs surrounding expected +// hashchange event-based behaviors, independent of any JavaScript +// window.onhashchange abstraction. See the following examples for more +// information: +// +// Chrome: Back Button - http://benalman.com/code/projects/jquery-hashchange/examples/bug-chrome-back-button/ +// Firefox: Remote XMLHttpRequest - http://benalman.com/code/projects/jquery-hashchange/examples/bug-firefox-remote-xhr/ +// WebKit: Back Button in an Iframe - http://benalman.com/code/projects/jquery-hashchange/examples/bug-webkit-hash-iframe/ +// Safari: Back Button from a different domain - http://benalman.com/code/projects/jquery-hashchange/examples/bug-safari-back-from-diff-domain/ +// +// Also note that should a browser natively support the window.onhashchange +// event, but not report that it does, the fallback polling loop will be used. +// +// About: Release History +// +// 1.3 - (7/21/2010) Reorganized IE6/7 Iframe code to make it more +// "removable" for mobile-only development. Added IE6/7 document.title +// support. Attempted to make Iframe as hidden as possible by using +// techniques from http://www.paciellogroup.com/blog/?p=604. Added +// support for the "shortcut" format $(window).hashchange( fn ) and +// $(window).hashchange() like jQuery provides for built-in events. +// Renamed jQuery.hashchangeDelay to and +// lowered its default value to 50. Added +// and properties plus document-domain.html +// file to address access denied issues when setting document.domain in +// IE6/7. +// 1.2 - (2/11/2010) Fixed a bug where coming back to a page using this plugin +// from a page on another domain would cause an error in Safari 4. Also, +// IE6/7 Iframe is now inserted after the body (this actually works), +// which prevents the page from scrolling when the event is first bound. +// Event can also now be bound before DOM ready, but it won't be usable +// before then in IE6/7. +// 1.1 - (1/21/2010) Incorporated document.documentMode test to fix IE8 bug +// where browser version is incorrectly reported as 8.0, despite +// inclusion of the X-UA-Compatible IE=EmulateIE7 meta tag. +// 1.0 - (1/9/2010) Initial Release. Broke out the jQuery BBQ event.special +// window.onhashchange functionality into a separate plugin for users +// who want just the basic event & back button support, without all the +// extra awesomeness that BBQ provides. This plugin will be included as +// part of jQuery BBQ, but also be available separately. + +(function($,window,undefined){ + '$:nomunge'; // Used by YUI compressor. + + // Reused string. + var str_hashchange = 'hashchange', + + // Method / object references. + doc = document, + fake_onhashchange, + special = $.event.special, + + // Does the browser support window.onhashchange? Note that IE8 running in + // IE7 compatibility mode reports true for 'onhashchange' in window, even + // though the event isn't supported, so also test document.documentMode. + doc_mode = doc.documentMode, + supports_onhashchange = 'on' + str_hashchange in window && ( doc_mode === undefined || doc_mode > 7 ); + + // Get location.hash (or what you'd expect location.hash to be) sans any + // leading #. Thanks for making this necessary, Firefox! + function get_fragment( url ) { + url = url || location.href; + return '#' + url.replace( /^[^#]*#?(.*)$/, '$1' ); + }; + + // Method: jQuery.fn.hashchange + // + // Bind a handler to the window.onhashchange event or trigger all bound + // window.onhashchange event handlers. This behavior is consistent with + // jQuery's built-in event handlers. + // + // Usage: + // + // > jQuery(window).hashchange( [ handler ] ); + // + // Arguments: + // + // handler - (Function) Optional handler to be bound to the hashchange + // event. This is a "shortcut" for the more verbose form: + // jQuery(window).bind( 'hashchange', handler ). If handler is omitted, + // all bound window.onhashchange event handlers will be triggered. This + // is a shortcut for the more verbose + // jQuery(window).trigger( 'hashchange' ). These forms are described in + // the section. + // + // Returns: + // + // (jQuery) The initial jQuery collection of elements. + + // Allow the "shortcut" format $(elem).hashchange( fn ) for binding and + // $(elem).hashchange() for triggering, like jQuery does for built-in events. + $.fn[ str_hashchange ] = function( fn ) { + return fn ? this.bind( str_hashchange, fn ) : this.trigger( str_hashchange ); + }; + + // Property: jQuery.fn.hashchange.delay + // + // The numeric interval (in milliseconds) at which the + // polling loop executes. Defaults to 50. + + // Property: jQuery.fn.hashchange.domain + // + // If you're setting document.domain in your JavaScript, and you want hash + // history to work in IE6/7, not only must this property be set, but you must + // also set document.domain BEFORE jQuery is loaded into the page. This + // property is only applicable if you are supporting IE6/7 (or IE8 operating + // in "IE7 compatibility" mode). + // + // In addition, the property must be set to the + // path of the included "document-domain.html" file, which can be renamed or + // modified if necessary (note that the document.domain specified must be the + // same in both your main JavaScript as well as in this file). + // + // Usage: + // + // jQuery.fn.hashchange.domain = document.domain; + + // Property: jQuery.fn.hashchange.src + // + // If, for some reason, you need to specify an Iframe src file (for example, + // when setting document.domain as in ), you can + // do so using this property. Note that when using this property, history + // won't be recorded in IE6/7 until the Iframe src file loads. This property + // is only applicable if you are supporting IE6/7 (or IE8 operating in "IE7 + // compatibility" mode). + // + // Usage: + // + // jQuery.fn.hashchange.src = 'path/to/file.html'; + + $.fn[ str_hashchange ].delay = 50; + /* + $.fn[ str_hashchange ].domain = null; + $.fn[ str_hashchange ].src = null; + */ + + // Event: hashchange event + // + // Fired when location.hash changes. In browsers that support it, the native + // HTML5 window.onhashchange event is used, otherwise a polling loop is + // initialized, running every milliseconds to + // see if the hash has changed. In IE6/7 (and IE8 operating in "IE7 + // compatibility" mode), a hidden Iframe is created to allow the back button + // and hash-based history to work. + // + // Usage as described in : + // + // > // Bind an event handler. + // > jQuery(window).hashchange( function(e) { + // > var hash = location.hash; + // > ... + // > }); + // > + // > // Manually trigger the event handler. + // > jQuery(window).hashchange(); + // + // A more verbose usage that allows for event namespacing: + // + // > // Bind an event handler. + // > jQuery(window).bind( 'hashchange', function(e) { + // > var hash = location.hash; + // > ... + // > }); + // > + // > // Manually trigger the event handler. + // > jQuery(window).trigger( 'hashchange' ); + // + // Additional Notes: + // + // * The polling loop and Iframe are not created until at least one handler + // is actually bound to the 'hashchange' event. + // * If you need the bound handler(s) to execute immediately, in cases where + // a location.hash exists on page load, via bookmark or page refresh for + // example, use jQuery(window).hashchange() or the more verbose + // jQuery(window).trigger( 'hashchange' ). + // * The event can be bound before DOM ready, but since it won't be usable + // before then in IE6/7 (due to the necessary Iframe), recommended usage is + // to bind it inside a DOM ready handler. + + // Override existing $.event.special.hashchange methods (allowing this plugin + // to be defined after jQuery BBQ in BBQ's source code). + special[ str_hashchange ] = $.extend( special[ str_hashchange ], { + + // Called only when the first 'hashchange' event is bound to window. + setup: function() { + // If window.onhashchange is supported natively, there's nothing to do.. + if ( supports_onhashchange ) { return false; } + + // Otherwise, we need to create our own. And we don't want to call this + // until the user binds to the event, just in case they never do, since it + // will create a polling loop and possibly even a hidden Iframe. + $( fake_onhashchange.start ); + }, + + // Called only when the last 'hashchange' event is unbound from window. + teardown: function() { + // If window.onhashchange is supported natively, there's nothing to do.. + if ( supports_onhashchange ) { return false; } + + // Otherwise, we need to stop ours (if possible). + $( fake_onhashchange.stop ); + } + + }); + + // fake_onhashchange does all the work of triggering the window.onhashchange + // event for browsers that don't natively support it, including creating a + // polling loop to watch for hash changes and in IE 6/7 creating a hidden + // Iframe to enable back and forward. + fake_onhashchange = (function(){ + var self = {}, + timeout_id, + + // Remember the initial hash so it doesn't get triggered immediately. + last_hash = get_fragment(), + + fn_retval = function(val){ return val; }, + history_set = fn_retval, + history_get = fn_retval; + + // Start the polling loop. + self.start = function() { + timeout_id || poll(); + }; + + // Stop the polling loop. + self.stop = function() { + timeout_id && clearTimeout( timeout_id ); + timeout_id = undefined; + }; + + // This polling loop checks every $.fn.hashchange.delay milliseconds to see + // if location.hash has changed, and triggers the 'hashchange' event on + // window when necessary. + function poll() { + var hash = get_fragment(), + history_hash = history_get( last_hash ); + + if ( hash !== last_hash ) { + history_set( last_hash = hash, history_hash ); + + $(window).trigger( str_hashchange ); + + } else if ( history_hash !== last_hash ) { + location.href = location.href.replace( /#.*/, '' ) + history_hash; + } + + timeout_id = setTimeout( poll, $.fn[ str_hashchange ].delay ); + }; + + // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + // vvvvvvvvvvvvvvvvvvv REMOVE IF NOT SUPPORTING IE6/7/8 vvvvvvvvvvvvvvvvvvv + // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + $.browser.msie && !supports_onhashchange && (function(){ + // Not only do IE6/7 need the "magical" Iframe treatment, but so does IE8 + // when running in "IE7 compatibility" mode. + + var iframe, + iframe_src; + + // When the event is bound and polling starts in IE 6/7, create a hidden + // Iframe for history handling. + self.start = function(){ + if ( !iframe ) { + iframe_src = $.fn[ str_hashchange ].src; + iframe_src = iframe_src && iframe_src + get_fragment(); + + // Create hidden Iframe. Attempt to make Iframe as hidden as possible + // by using techniques from http://www.paciellogroup.com/blog/?p=604. + iframe = $('