Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Add GitHub Pull Request Management stuff.

* pull-request/2: (22 commits)
  Fix WS.
  Do some nicer error handling.
  More cleanup for hide/show logic.
  Bind show/hide on hashchange event
  Fix event handling.
  Refresh correct thing after closing a pull request and fix event handling
  Use URL hash for history.
  Use config.php here, too.
  Move config in a file.
  Avoid errors if no action was provided.
  Fix HTML.
  Fid indention.
  Split JavaScript stuff in a extra file
  Ask for login when needed.
  Use a local icon.
  Use correct preposition
  Store password a bit safer.
  Add Pull Request UI to navigation.
  WS.
  Add login stuff and update routines.
  ...
  • Loading branch information...
commit afcb95867b06d3926a2e43cd1fd7f583b0191455 2 parents f86cd45 + 7070ea5
Johannes Schlüter johannes authored
1  .gitignore
View
@@ -1,2 +1,3 @@
reports/db/*.sqlite
reports/db/*.cache
+pulls/config.php
BIN  gfx/github.ico
View
Binary file not shown
3  include/functions.php
View
@@ -34,7 +34,8 @@ function common_header() {
| <a href="/rc.php" class="head_links">Release Candidates</a>
| <a href="/howtohelp.php" class="head_links">How to Help</a>
| <a href="/handling-bugs.php" class="head_links">Handling Bug Reports</a>
- | <a href="/reports/" class="head_links">Test reports</a>
+ | <a href="/reports/" class="head_links">Test Reports</a>
+ | <a href="/pulls/" class="head_links">Manage Pull Requests</a>
</td>
<td valign="bottom" align="right" class="head_links">&nbsp;</td>
</tr>
186 pulls/api.php
View
@@ -0,0 +1,186 @@
+<?php
+require('./config.php');
+
+if ($_SERVER['SERVER_NAME'] === 'schlueters.de') {
+ define('DEV', true);
+ error_reporting(-1);
+ ini_set('display_errors', 1);
+} else {
+ define('DEV', false);
+}
+
+function verify_password($user, $pass)
+{
+ global $errors;
+
+ $post = http_build_query(
+ array(
+ 'token' => getenv('AUTH_TOKEN'),
+ 'username' => $user,
+ 'password' => $pass,
+ )
+ );
+
+ $opts = array(
+ 'method' => 'POST',
+ 'header' => 'Content-type: application/x-www-form-urlencoded',
+ 'content' => $post,
+ );
+
+ $ctx = stream_context_create(array('http' => $opts));
+
+ $s = @file_get_contents('https://master.php.net/fetch/cvsauth.php', false, $ctx);
+
+ $a = @unserialize($s);
+ if (!is_array($a)) {
+ $errors[] = "Failed to get authentication information.\nMaybe master is down?\n";
+ return false;
+ }
+ if (isset($a['errno'])) {
+ $errors[] = "Authentication failed: {$a['errstr']}\n";
+ return false;
+ }
+
+ return true;
+}
+
+function verify_password_DEV($user, $pass)
+{
+ global $errors;
+ $errors[] = "Unknown user $user (DEV)";
+ return $user === 'johannes';
+}
+
+function ghpostcomment($pull, $comment)
+{
+ $post = json_encode(array("body" => "**Comment on behalf of $_SESSION[user] at php.net:**\n\n$comment"));
+
+
+ $opts = array(
+ 'method' => 'POST',
+ 'content' => $post,
+ );
+
+ $ctx = stream_context_create(array('http' => $opts));
+
+ $url = str_replace('https://', 'https://'.GITHUB_USER.':'.GITHUB_PASS.'@', $pull->_links->comments->href);
+ $s = @file_get_contents($url, false, $ctx);
+ return (bool)$s;
+}
+
+function ghchangestate($pull, $state)
+{
+ $content = json_encode(array("state" => $state));
+
+ $opts = array(
+ 'method' => 'PATCH',
+ 'content' => $content
+ );
+
+ $ctx = stream_context_create(array('http' => $opts));
+
+ $url = str_replace('https://', 'https://'.GITHUB_USER.':'.GITHUB_PASS.'@', $pull->_links->self->href);
+ $s = @file_get_contents($url, false, $ctx);
+ return (bool)$s;
+}
+
+function login()
+{
+ global $errors;
+
+ $func = DEV ? 'verify_password_DEV' : 'verify_password';
+ if ($func($_POST['user'], $_POST['pass'])) {
+ $_SESSION['user'] = $_POST['user'];
+ die(json_encode(array('success' => true, 'user' => $_POST['user'])));
+ } else {
+ header('HTTP/1.0 401 Unauthorized');
+ $_SESSION['user'] = false;
+ die(json_encode(array('success' => false, 'errors' => $errors)));
+ }
+}
+
+function logout()
+{
+ session_destroy();
+ die(json_encode(array('success' => true)));
+}
+
+function loggedin()
+{
+ $result = array(
+ 'success' => !empty($_SESSION['user'])
+ );
+ if (!empty($_SESSION['user'])) {
+ $result['user'] = $_SESSION['user'];
+ }
+ die(json_encode($result));
+}
+
+function ghupdate()
+{
+ if (empty($_SESSION['user'])) {
+ header('HTTP/1.0 401 Unauthorized');
+ die(json_encode(array('success' => false, 'errors' => array('Unauthorized'))));
+ }
+
+ if (empty($_POST['repo'])) {
+ header('HTTP/1.0 400 Bad Request');
+ die(json_encode(array('success' => false, 'errors' => array("No repo provided"))));
+ }
+
+ if (empty($_POST['id']) || !is_numeric($_POST['id'])) {
+ header('HTTP/1.0 400 Bad Request');
+ die(json_encode(array('success' => false, 'errors' => array("No or inalid id provided"))));
+ }
+
+ if (empty($_POST['comment']) || !($comment = trim($_POST['comment']))) {
+ header('HTTP/1.0 400 Bad Request');
+ die(json_encode(array('success' => false, 'errors' => array("No comment provided"))));
+ }
+
+ if (!empty($_POST['state']) && !in_array($_POST['state'], array('open', 'closed'))) {
+ header('HTTP/1.0 400 Bad Request');
+ die(json_encode(array('success' => false, 'errors' => array("Invalid state"))));
+ }
+
+ $pull_raw = @file_get_contents(GITHUB_BASEURL.'repos/'.GITHUB_ORG.'/'.urlencode($_POST['repo']).'/pulls/'.$_POST['id']);
+ $pull = $pull_raw ? json_decode($pull_raw) : false;
+ if (!$pull) {
+ header('HTTP/1.0 400 Bad Request');
+ die(json_encode(array('success' => false, 'errors' => array("Failed to get data from GitHub"))));
+ }
+
+ $comment = @get_magic_quotes_gpc() ? stripslashes($_POST['comment']) : $_POST['comment'];
+
+ if (!ghpostcomment($pull, $comment)) {
+ header('500 Internal Server Error');
+ die(json_encode(array('success' => false, 'errors' => array("Failed to add comment on GitHub"))));
+ }
+
+ if (!empty($_POST['state'])) {
+ if (!ghchangestate($pull, $_POST['state'])) {
+ header('500 Internal Server Error');
+ die(json_encode(array('success' => false, 'errors' => array("Failed to set new state"))));
+ }
+ }
+
+ die(json_encode(array('success' => true)));
+}
+
+header('Content-Type: application/json');
+session_start();
+
+$accepted_actions = array(
+ 'login',
+ 'logout',
+ 'loggedin',
+ 'ghupdate'
+);
+if (isset($_REQUEST['action']) && in_array($_REQUEST['action'], $accepted_actions)) {
+ $action = $_REQUEST['action'];
+ $action();
+} else {
+ header('HTTP/1.0 400 Bad Request');
+ die(json_encode(array('success' => false, 'errors' => array("Unknown method"))));
+}
+
5 pulls/config.php.in
View
@@ -0,0 +1,5 @@
+<?php
+const GITHUB_BASEURL = 'https://api.github.com/';
+const GITHUB_ORG = 'php';
+const GITHUB_USER = '....';
+const GITHUB_PASS = '....';
137 pulls/index.php
View
@@ -0,0 +1,137 @@
+<?php
+include("../include/functions.php");
+include("../include/release-qa.php");
+
+@include("./config.php");
+
+$TITLE = "PHP-QA: GitHub Pull Requests";
+$SITE_UPDATE = date("D M d H:i:s Y T", filectime(__FILE__));
+
+common_header();
+
+?>
+<table width="70%" border="0" cellspacing="0" cellpadding="0">
+ <tr>
+ <td width="10"><img src="../gfx/spacer.gif" width="10" height="1"></td>
+ <td width="100%">
+ <style>
+ #loading {
+ position:fixed;
+ top:50%;
+ padding-top:-20px;
+ height:40px;
+ left:50%;
+ padding-left:-50px;
+ width:100px;
+ border:1px solid black;
+ background-color: black;
+ color: white;
+ text-align: center;
+ vertical-align: center;
+ }
+
+ .ghuser {
+ border: 1px solid #ffcc66;
+ float: right;
+ margin-left: 5px;
+ margin-bottom: 5px;
+ }
+
+ .ghuser a {
+ color: #ffcc66;
+ text-decoration: none;
+ }
+
+ #loginstatus {
+ width: 100%;
+ text-align: right;
+ }
+ </style>
+ <link href="http://code.jquery.com/ui/1.8.18/themes/ui-lightness/jquery-ui.css" rel="stylesheet" type="text/css"/>
+ <script type="text/javascript" src="http://code.jquery.com/jquery-1.7.1.min.js"></script>
+ <script type="text/javascript" src="http://code.jquery.com/ui/1.8.18/jquery-ui.min.js"></script>
+ <script type="text/javascript" src="jsrender.js"></script>
+ <script type="text/javascript" src="jquery.ba-bbq.min.js"></script>
+ <script id="repoListItemTemplate" type="text/x-jquery-tmpl">
+ <li repo="{{=name}}"><b><a href="#">{{=name}}:</a></b> {{=description}} ({{=open_issues}})</li>
+ </script>
+ <script id="repoOverviewTemplate" type="text/x-jquery-tmpl">
+ <h2>{{=repoName}}</h2>
+ <div id="repoPullList">
+ {{=pullList!}}
+ </div>
+ <!-- iframe style='width:100%; height:300px;border-width:1px;' src='' id='ghframe' name='ghframe'></iframe -->
+ </script>
+ <script id="pullRequestListItem" type="text/x-jquery-tmpl">
+ <h3><a href='#'>{{=number}}: {{=title}} ({{=state}})</a></h3>
+ <div class="pullrequest">
+ <div class="ghuser"><a href="{{=user.url}}"><img src="{{=user.avatar_url}}"><br>{{=user.login}}</a></div>
+ <div>Created: {{=created_at}}, LastUpdated: {{=updated_at}}</div>
+ <div>{{=body}}</div>
+ <div><a href="{{=html_url}}"><img src="../gfx/github.ico"> On GitHub</a> |
+ <a href="{{=diff_url}}">Diff</a> |
+ <a href="#" number="{{=number}}" state="{{=state}}" title="{{=title}}" class="pullinstructions">Show Pull Instructions</a> |
+ <a href="#" number="{{=number}}" state="{{=state}}" title="{{=title}}" class="updatepullrequest">Update</a>
+ </div>
+ </div>
+ </script>
+ <script id="pullInstructionTemplate" type="text/x-jquery-tmpl">
+ <pre>
+$ git fetch git://github.com/php/{{=repo}} pull/{{=number}}/head:pull-request/{{=number}}
+$ git log -p pull-request/{{=number}} # REVIEW IT
+$ git merge pull-request/{{=number}} # Merge it, add a GOOD commit message
+$ make test # you better don't forget that
+$ git push origin master # everything okay? good, let's push it
+ </pre>
+ </script>
+ <script id="updatePullRequestTemplate" type="text/x-jquery-tmpl">
+ State: <select id="newState">
+ <option>open</option>
+ <option>closed</option>
+ </select><br>
+ Please provide a comment for your change:<br>
+ <textarea id="comment"></textarea><br>
+ <button>Go</button>
+ </script>
+ <script type="text/javascript">
+ var GITHUB_BASEURL = <?php echo json_encode(GITHUB_BASEURL); ?>;
+ var GITHUB_ORG = <?php echo json_encode(GITHUB_ORG); ?>;
+ var API_URL = "api.php";
+ </script>
+ <script src="pullrequests.js"></script>
+ <div id="loginstatus">
+ <span id="checkinglogin">(checking login state ...)</span>
+ <span id="loggedin"></span>
+ <span id="notloggedin"><a href="#">Login</a></span>
+ </div>
+ <h1>Github Pull Requests</h1>
+<?php
+if (empty($_ENV['AUTH_TOKEN'])) {
+ echo '<div style="width: 100%; border: 2px solid red; padding:10px;"><b>Error:</b> AUTH_TOKEN not set</div><br>';
+}
+
+if (!constant('GITHUB_PASS')) {
+ echo '<div style="width: 100%; border: 2px solid red; padding:10px;"><b>Error:</b> config.php not configured correctly.</div><br>';
+ common_footer();
+ exit;
+}
+
+?>
+ <div id="backToRepolist"><a href="#">&lt;&lt;&lt-- Repos</a></div>
+ <div id="mainContent"><ul id="repolist"></ul>The PHP project is using github to mirror its git repostories from <a href="http://git.php.net">git.php.net</a>.</div>
+ <div id="repoContent"></div>
+ <div id="loginDialog" title="Login">
+ Username: <br><input id="userField"><br>
+ Password: <br><input id="passField" type="password"><br>
+ <button id="loginBtn">Login</button>
+ </div>
+ <div id="loading">Loading</div>
+
+ </td>
+ <td width="10"><img src="http://qa.php.net/gfx/spacer.gif" width="10" height="1"></td>
+ </tr>
+ </table>
+<?php
+
+common_footer();
+
18 pulls/jquery.ba-bbq.min.js
View
@@ -0,0 +1,18 @@
+/*
+ * jQuery BBQ: Back Button & Query Library - v1.3pre - 8/26/2010
+ * http://benalman.com/projects/jquery-bbq-plugin/
+ *
+ * Copyright (c) 2010 "Cowboy" Ben Alman
+ * Dual licensed under the MIT and GPL licenses.
+ * http://benalman.com/about/license/
+ */
+(function($,r){var h,n=Array.prototype.slice,t=decodeURIComponent,a=$.param,j,c,m,y,b=$.bbq=$.bbq||{},s,x,k,e=$.event.special,d="hashchange",B="querystring",F="fragment",z="elemUrlAttr",l="href",w="src",p=/^.*\?|#.*$/g,u,H,g,i,C,E={};function G(I){return typeof I==="string"}function D(J){var I=n.call(arguments,1);return function(){return J.apply(this,I.concat(n.call(arguments)))}}function o(I){return I.replace(H,"$2")}function q(I){return I.replace(/(?:^[^?#]*\?([^#]*).*$)?.*/,"$1")}function f(K,P,I,L,J){var R,O,N,Q,M;if(L!==h){N=I.match(K?H:/^([^#?]*)\??([^#]*)(#?.*)/);M=N[3]||"";if(J===2&&G(L)){O=L.replace(K?u:p,"")}else{Q=m(N[2]);L=G(L)?m[K?F:B](L):L;O=J===2?L:J===1?$.extend({},L,Q):$.extend({},Q,L);O=j(O);if(K){O=O.replace(g,t)}}R=N[1]+(K?C:O||!N[1]?"?":"")+O+M}else{R=P(I!==h?I:location.href)}return R}a[B]=D(f,0,q);a[F]=c=D(f,1,o);a.sorted=j=function(J,K){var I=[],L={};$.each(a(J,K).split("&"),function(P,M){var O=M.replace(/(?:%5B|=).*$/,""),N=L[O];if(!N){N=L[O]=[];I.push(O)}N.push(M)});return $.map(I.sort(),function(M){return L[M]}).join("&")};c.noEscape=function(J){J=J||"";var I=$.map(J.split(""),encodeURIComponent);g=new RegExp(I.join("|"),"g")};c.noEscape(",/");c.ajaxCrawlable=function(I){if(I!==h){if(I){u=/^.*(?:#!|#)/;H=/^([^#]*)(?:#!|#)?(.*)$/;C="#!"}else{u=/^.*#/;H=/^([^#]*)#?(.*)$/;C="#"}i=!!I}return i};c.ajaxCrawlable(0);$.deparam=m=function(L,I){var K={},J={"true":!0,"false":!1,"null":null};$.each(L.replace(/\+/g," ").split("&"),function(O,T){var N=T.split("="),S=t(N[0]),M,R=K,P=0,U=S.split("]["),Q=U.length-1;if(/\[/.test(U[0])&&/\]$/.test(U[Q])){U[Q]=U[Q].replace(/\]$/,"");U=U.shift().split("[").concat(U);Q=U.length-1}else{Q=0}if(N.length===2){M=t(N[1]);if(I){M=M&&!isNaN(M)?+M:M==="undefined"?h:J[M]!==h?J[M]:M}if(Q){for(;P<=Q;P++){S=U[P]===""?R.length:U[P];R=R[S]=P<Q?R[S]||(U[P+1]&&isNaN(U[P+1])?{}:[]):M}}else{if($.isArray(K[S])){K[S].push(M)}else{if(K[S]!==h){K[S]=[K[S],M]}else{K[S]=M}}}}else{if(S){K[S]=I?h:""}}});return K};function A(K,I,J){if(I===h||typeof I==="boolean"){J=I;I=a[K?F:B]()}else{I=G(I)?I.replace(K?u:p,""):I}return m(I,J)}m[B]=D(A,0);m[F]=y=D(A,1);$[z]||($[z]=function(I){return $.extend(E,I)})({a:l,base:l,iframe:w,img:w,input:w,form:"action",link:l,script:w});k=$[z];function v(L,J,K,I){if(!G(K)&&typeof K!=="object"){I=K;K=J;J=h}return this.each(function(){var O=$(this),M=J||k()[(this.nodeName||"").toLowerCase()]||"",N=M&&O.attr(M)||"";O.attr(M,a[L](N,K,I))})}$.fn[B]=D(v,B);$.fn[F]=D(v,F);b.pushState=s=function(L,I){if(G(L)&&/^#/.test(L)&&I===h){I=2}var K=L!==h,J=c(location.href,K?L:{},K?I:2);location.href=J};b.getState=x=function(I,J){return I===h||typeof I==="boolean"?y(I):y(J)[I]};b.removeState=function(I){var J={};if(I!==h){J=x();$.each($.isArray(I)?I:arguments,function(L,K){delete J[K]})}s(J,2)};e[d]=$.extend(e[d],{add:function(I){var K;function J(M){var L=M[F]=c();M.getState=function(N,O){return N===h||typeof N==="boolean"?m(L,N):m(L,O)[N]};K.apply(this,arguments)}if($.isFunction(I)){K=I;return J}else{K=I.handler;I.handler=J}}})})(jQuery,this);
+/*
+ * 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/
+ */
+(function($,e,b){var c="hashchange",h=document,f,g=$.event.special,i=h.documentMode,d="on"+c in e&&(i===b||i>7);function a(j){j=j||location.href;return"#"+j.replace(/^[^#]*#?(.*)$/,"$1")}$.fn[c]=function(j){return j?this.bind(c,j):this.trigger(c)};$.fn[c].delay=50;g[c]=$.extend(g[c],{setup:function(){if(d){return false}$(f.start)},teardown:function(){if(d){return false}$(f.stop)}});f=(function(){var j={},p,m=a(),k=function(q){return q},l=k,o=k;j.start=function(){p||n()};j.stop=function(){p&&clearTimeout(p);p=b};function n(){var r=a(),q=o(m);if(r!==m){l(m=r,q);$(e).trigger(c)}else{if(q!==m){location.href=location.href.replace(/#.*/,"")+q}}p=setTimeout(n,$.fn[c].delay)}$.browser.msie&&!d&&(function(){var q,r;j.start=function(){if(!q){r=$.fn[c].src;r=r&&r+a();q=$('<iframe tabindex="-1" title="empty"/>').hide().one("load",function(){r||l(a());n()}).attr("src",r||"javascript:0").insertAfter("body")[0].contentWindow;h.onpropertychange=function(){try{if(event.propertyName==="title"){q.document.title=h.title}}catch(s){}}}};j.stop=k;o=function(){return a(q.location.href)};l=function(v,s){var u=q.document,t=$.fn[c].domain;if(v!==s){u.title=h.title;u.open();t&&u.write('<script>document.domain="'+t+'"<\/script>');u.close();q.location.hash=v}}})();return j})()})(jQuery,this);
573 pulls/jsrender.js
View
@@ -0,0 +1,573 @@
+/*! JsRender v1.0pre - (jsrender.js version: does not require jQuery): http://github.com/BorisMoore/jsrender */
+/*
+ * Optimized version of jQuery Templates, fosr rendering to string, using 'codeless' markup.
+ *
+ * Copyright 2011, Boris Moore
+ * Released under the MIT License.
+ */
+window.JsViews || window.jQuery && jQuery.views || (function( window, undefined ) {
+
+var $, _$, JsViews, viewsNs, tmplEncode, render, rTag, registerTags, registerHelpers, extend,
+ FALSE = false, TRUE = true,
+ jQuery = window.jQuery, document = window.document,
+ htmlExpr = /^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,
+ rPath = /^(true|false|null|[\d\.]+)|(\w+|\$(view|data|ctx|(\w+)))([\w\.]*)|((['"])(?:\\\1|.)*\7)$/g,
+ rParams = /(\$?[\w\.\[\]]+)(?:(\()|\s*(===|!==|==|!=|<|>|<=|>=)\s*|\s*(\=)\s*)?|(\,\s*)|\\?(\')|\\?(\")|(\))|(\s+)/g,
+ rNewLine = /\r?\n/g,
+ rUnescapeQuotes = /\\(['"])/g,
+ rEscapeQuotes = /\\?(['"])/g,
+ rBuildHash = /\x08([^\x08]+)\x08/g,
+ autoName = 0,
+ escapeMapForHtml = {
+ "&": "&amp;",
+ "<": "&lt;",
+ ">": "&gt;"
+ },
+ htmlSpecialChar = /[\x00"&'<>]/g,
+ slice = Array.prototype.slice;
+
+if ( jQuery ) {
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ // jQuery is loaded, so make $ the jQuery object
+ $ = jQuery;
+
+ $.fn.extend({
+ // Use first wrapped element as template markup.
+ // Return string obtained by rendering the template against data.
+ render: function( data, context, parentView, path ) {
+ return render( data, this[0], context, parentView, path );
+ },
+
+ // Consider the first wrapped element as a template declaration, and get the compiled template or store it as a named template.
+ template: function( name, context ) {
+ return $.template( name, this[0], context );
+ }
+ });
+
+} else {
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ // jQuery is not loaded. Make $ the JsViews object
+
+ // Map over the $ in case of overwrite
+ _$ = window.$;
+
+ window.JsViews = JsViews = window.$ = $ = {
+ extend: function( target, source ) {
+ var name;
+ for ( name in source ) {
+ target[ name ] = source[ name ];
+ }
+ return target;
+ },
+ isArray: Array.isArray || function( obj ) {
+ return Object.prototype.toString.call( obj ) === "[object Array]";
+ },
+ noConflict: function() {
+ if ( window.$ === JsViews ) {
+ window.$ = _$;
+ }
+ return JsViews;
+ }
+ };
+}
+
+extend = $.extend;
+
+//=================
+// View constructor
+//=================
+
+function View( context, path, parentView, data, template ) {
+ // Returns a view data structure for a new rendered instance of a template.
+ // The content field is a hierarchical array of strings and nested views.
+
+ parentView = parentView || { viewsCount:0, ctx: viewsNs.helpers };
+
+ var parentContext = parentView && parentView.ctx;
+
+ return {
+ jsViews: "v1.0pre",
+ path: path || "",
+ // inherit context from parentView, merged with new context.
+ itemNumber: ++parentView.viewsCount || 1,
+ viewsCount: 0,
+ tmpl: template,
+ data: data || parentView.data || {},
+ // Set additional context on this view (which will modify the context inherited from the parent, and be inherited by child views)
+ ctx : context && context === parentContext
+ ? parentContext
+ : (parentContext ? extend( extend( {}, parentContext ), context ) : context||{}),
+ // If no jQuery, extend does not support chained copies - so limit to two parameters
+ parent: parentView
+ };
+}
+extend( $, {
+ views: viewsNs = {
+ templates: {},
+ tags: {
+ "if": function() {
+ var ifTag = this,
+ view = ifTag._view;
+ view.onElse = function( presenter, args ) {
+ var i = 0,
+ l = args.length;
+ while ( l && !args[ i++ ]) {
+ // Only render content if args.length === 0 (i.e. this is an else with no condition) or if a condition argument is truey
+ if ( i === l ) {
+ return "";
+ }
+ }
+ view.onElse = undefined; // If condition satisfied, so won't run 'else'.
+ return render( view.data, presenter.tmpl, view.ctx, view);
+ };
+ return view.onElse( this, arguments );
+ },
+ "else": function() {
+ var view = this._view;
+ return view.onElse ? view.onElse( this, arguments ) : "";
+ },
+ each: function() {
+ var i,
+ self = this,
+ result = "",
+ args = arguments,
+ l = args.length,
+ content = self.tmpl,
+ view = self._view;
+ for ( i = 0; i < l; i++ ) {
+ result += args[ i ] ? render( args[ i ], content, self.ctx || view.ctx, view, self._path, self._ctor ) : "";
+ }
+ return l ? result
+ // If no data parameter, use the current $data from view, and render once
+ : result + render( view.data, content, view.ctx, view, self._path, self.tag );
+ },
+ "=": function( value ) {
+ return value;
+ },
+ "*": function( value ) {
+ return value;
+ }
+ },
+ helpers: {
+ not: function( value ) {
+ return !value;
+ }
+ },
+ allowCode: FALSE,
+ debugMode: TRUE,
+ err: function( e ) {
+ return viewsNs.debugMode ? ("<br/><b>Error:</b> <em> " + (e.message || e) + ". </em>"): '""';
+ },
+
+//===============
+// setDelimiters
+//===============
+
+ setDelimiters: function( openTag, closeTag ) {
+ // Set or modify the delimiter characters for tags: "{{" and "}}"
+ var firstCloseChar = closeTag.charAt( 0 ),
+ secondCloseChar = closeTag.charAt( 1 );
+ openTag = "\\" + openTag.charAt( 0 ) + "\\" + openTag.charAt( 1 );
+ closeTag = "\\" + firstCloseChar + "\\" + secondCloseChar;
+
+ // Build regex with new delimiters
+ // {{
+ rTag = openTag
+ // # tag (followed by space,! or }) or equals or code
+ + "(?:(?:(\\#)?(\\w+(?=[!\\s\\" + firstCloseChar + "]))" + "|(?:(\\=)|(\\*)))"
+ // params
+ + "\\s*((?:[^\\" + firstCloseChar + "]|\\" + firstCloseChar + "(?!\\" + secondCloseChar + "))*?)"
+ // encoding
+ + "(!(\\w*))?"
+ // closeBlock
+ + "|(?:\\/([\\w\\$\\.\\[\\]]+)))"
+ // }}
+ + closeTag;
+
+ // Default rTag: # tag equals code params encoding closeBlock
+ // /\{\{(?:(?:(\#)?(\w+(?=[\s\}!]))|(?:(\=)|(\*)))((?:[^\}]|\}(?!\}))*?)(!(\w*))?|(?:\/([\w\$\.\[\]]+)))\}\}/g;
+
+ rTag = new RegExp( rTag, "g" );
+ },
+
+
+//===============
+// registerTags
+//===============
+
+ // Register declarative tag.
+ registerTags: registerTags = function( name, tagFn ) {
+ var key;
+ if ( typeof name === "object" ) {
+ for ( key in name ) {
+ registerTags( key, name[ key ]);
+ }
+ } else {
+ // Simple single property case.
+ viewsNs.tags[ name ] = tagFn;
+ }
+ return this;
+ },
+
+//===============
+// registerHelpers
+//===============
+
+ // Register helper function for use in markup.
+ registerHelpers: registerHelpers = function( name, helper ) {
+ if ( typeof name === "object" ) {
+ // Object representation where property name is path and property value is value.
+ // TODO: We've discussed an "objectchange" event to capture all N property updates here. See TODO note above about propertyChanges.
+ var key;
+ for ( key in name ) {
+ registerHelpers( key, name[ key ]);
+ }
+ } else {
+ // Simple single property case.
+ viewsNs.helpers[ name ] = helper;
+ }
+ return this;
+ },
+
+//===============
+// tmpl.encode
+//===============
+
+ encode: function( encoding, text ) {
+ return text
+ ? ( tmplEncode[ encoding || "html" ] || tmplEncode.html)( text ) // HTML encoding is the default
+ : "";
+ },
+
+ encoders: tmplEncode = {
+ "none": function( text ) {
+ return text;
+ },
+ "html": function( text ) {
+ // HTML encoding helper: Replace < > & and ' and " by corresponding entities.
+ // Implementation, from Mike Samuel <msamuel@google.com>
+ return String( text ).replace( htmlSpecialChar, replacerForHtml );
+ }
+ //TODO add URL encoding, and perhaps other encoding helpers...
+ },
+
+//===============
+// renderTag
+//===============
+
+ renderTag: function( tag, view, encode, content, tagProperties ) {
+ // This is a tag call, with arguments: "tag", view, encode, content, presenter [, params...]
+ var ret, ctx, name,
+ args = arguments,
+ presenters = viewsNs.presenters;
+ hash = tagProperties._hash,
+ tagFn = viewsNs.tags[ tag ];
+
+ if ( !tagFn ) {
+ return "";
+ }
+
+ content = content && view.tmpl.nested[ content - 1 ];
+ tagProperties.tmpl = tagProperties.tmpl || content || undefined;
+ // Set the tmpl property to the content of the block tag, unless set as an override property on the tag
+
+ if ( presenters && presenters[ tag ]) {
+ ctx = extend( extend( {}, tagProperties.ctx ), tagProperties );
+ delete ctx.ctx;
+ delete ctx._path;
+ delete ctx.tmpl;
+ tagProperties.ctx = ctx;
+ tagProperties._ctor = tag + (hash ? "=" + hash.slice( 0, -1 ) : "");
+
+ tagProperties = extend( extend( {}, tagFn ), tagProperties );
+ tagFn = viewsNs.tags.each; // Use each to render the layout template against the data
+ }
+
+ tagProperties._encode = encode;
+ tagProperties._view = view;
+ ret = tagFn.apply( tagProperties, args.length > 5 ? slice.call( args, 5 ) : [view.data] );
+ return ret || (ret === undefined ? "" : ret.toString()); // (If ret is the value 0 or false or null, will render to string)
+ }
+ },
+
+//===============
+// render
+//===============
+
+ render: render = function( data, tmpl, context, parentView, path, tagName ) {
+ // Render template against data as a tree of subviews (nested template), or as a string (top-level template).
+ // tagName parameter for internal use only. Used for rendering templates registered as tags (which may have associated presenter objects)
+ var i, l, dataItem, arrayView, content, result = "";
+
+ if ( arguments.length === 2 && data.jsViews ) {
+ parentView = data;
+ context = parentView.ctx;
+ data = parentView.data;
+ }
+ tmpl = $.template( tmpl );
+ if ( !tmpl ) {
+ return ""; // Could throw...
+ }
+
+ if ( $.isArray( data )) {
+ // Create a view item for the array, whose child views correspond to each data item.
+ arrayView = new View( context, path, parentView, data);
+ l = data.length;
+ for ( i = 0, l = data.length; i < l; i++ ) {
+ dataItem = data[ i ];
+ content = dataItem ? tmpl( dataItem, new View( context, path, arrayView, dataItem, tmpl, this )) : "";
+ result += viewsNs.activeViews ? "<!--item-->" + content + "<!--/item-->" : content;
+ }
+ } else {
+ result += tmpl( data, new View( context, path, parentView, data, tmpl ));
+ }
+
+ return viewsNs.activeViews
+ // If in activeView mode, include annotations
+ ? "<!--tmpl(" + (path || "") + ") " + (tagName ? "tag=" + tagName : tmpl._name) + "-->" + result + "<!--/tmpl-->"
+ // else return just the string result
+ : result;
+ },
+
+//===============
+// template
+//===============
+
+ template: function( name, tmpl ) {
+ // Set:
+ // Use $.template( name, tmpl ) to cache a named template,
+ // where tmpl is a template string, a script element or a jQuery instance wrapping a script element, etc.
+ // Use $( "selector" ).template( name ) to provide access by name to a script block template declaration.
+
+ // Get:
+ // Use $.template( name ) to access a cached template.
+ // Also $( selectorToScriptBlock ).template(), or $.template( null, templateString )
+ // will return the compiled template, without adding a name reference.
+ // If templateString is not a selector, $.template( templateString ) is equivalent
+ // to $.template( null, templateString ). To ensure a string is treated as a template,
+ // include an HTML element, an HTML comment, or a template comment tag.
+
+ if (tmpl) {
+ // Compile template and associate with name
+ if ( "" + tmpl === tmpl ) { // type string
+ // This is an HTML string being passed directly in.
+ tmpl = compile( tmpl );
+ } else if ( jQuery && tmpl instanceof $ ) {
+ tmpl = tmpl[0];
+ }
+ if ( tmpl ) {
+ if ( jQuery && tmpl.nodeType ) {
+ // If this is a template block, use cached copy, or generate tmpl function and cache.
+ tmpl = $.data( tmpl, "tmpl" ) || $.data( tmpl, "tmpl", compile( tmpl.innerHTML ));
+ }
+ viewsNs.templates[ tmpl._name = tmpl._name || name || "_" + autoName++ ] = tmpl;
+ }
+ return tmpl;
+ }
+ // Return named compiled template
+ return name
+ ? "" + name !== name // not type string
+ ? (name._name
+ ? name // already compiled
+ : $.template( null, name ))
+ : viewsNs.templates[ name ] ||
+ // If not in map, treat as a selector. (If integrated with core, use quickExpr.exec)
+ $.template( null, htmlExpr.test( name ) ? name : try$( name ))
+ : null;
+ }
+});
+
+viewsNs.setDelimiters( "{{", "}}" );
+
+//=================
+// compile template
+//=================
+
+// Generate a reusable function that will serve to render a template against data
+// (Compile AST then build template function)
+
+function parsePath( all, comp, object, viewDataCtx, viewProperty, path, string, quot ) {
+ return object
+ ? ((viewDataCtx
+ ? viewProperty
+ ? ("$view." + viewProperty)
+ : object
+ :("$data." + object)
+ ) + ( path || "" ))
+ : string || (comp || "");
+}
+
+function compile( markup ) {
+ var newNode,
+ loc = 0,
+ stack = [],
+ topNode = [],
+ content = topNode,
+ current = [,,topNode];
+
+ function pushPreceedingContent( shift ) {
+ shift -= loc;
+ if ( shift ) {
+ content.push( markup.substr( loc, shift ).replace( rNewLine,"\\n"));
+ }
+ }
+
+ function parseTag( all, isBlock, tagName, equals, code, params, useEncode, encode, closeBlock, index ) {
+ // rTag : # tagName equals code params encode closeBlock
+ // /\{\{(?:(?:(\#)?(\w+(?=[\s\}!]))|(?:(\=)|(\*)))((?:[^\}]|\}(?!\}))*?)(!(\w*))?|(?:\/([\w\$\.\[\]]+)))\}\}/g;
+
+ // Build abstract syntax tree: [ tagName, params, content, encode ]
+ var named,
+ hash = "",
+ parenDepth = 0,
+ quoted = FALSE, // boolean for string content in double qoutes
+ aposed = FALSE; // or in single qoutes
+
+ function parseParams( all, path, paren, comp, eq, comma, apos, quot, rightParen, space, index ) {
+ // path paren eq comma apos quot rtPrn space
+ // /(\$?[\w\.\[\]]+)(?:(\()|(===)|(\=))?|(\,\s*)|\\?(\')|\\?(\")|(\))|(\s+)/g
+
+ return aposed
+ // within single-quoted string
+ ? ( aposed = !apos, (aposed ? all : '"'))
+ : quoted
+ // within double-quoted string
+ ? ( quoted = !quot, (quoted ? all : '"'))
+ : comp
+ // comparison
+ ? ( path.replace( rPath, parsePath ) + comp)
+ : eq
+ // named param
+ ? parenDepth ? "" :( named = TRUE, '\b' + path + ':')
+ : paren
+ // function
+ ? (parenDepth++, path.replace( rPath, parsePath ) + '(')
+ : rightParen
+ // function
+ ? (parenDepth--, ")")
+ : path
+ // path
+ ? path.replace( rPath, parsePath )
+ : comma
+ ? ","
+ : space
+ ? (parenDepth
+ ? ""
+ : named
+ ? ( named = FALSE, "\b")
+ : ","
+ )
+ : (aposed = apos, quoted = quot, '"');
+ }
+
+ tagName = tagName || equals;
+ pushPreceedingContent( index );
+ if ( code ) {
+ if ( viewsNs.allowCode ) {
+ content.push([ "*", params.replace( rUnescapeQuotes, "$1" )]);
+ }
+ } else if ( tagName ) {
+ if ( tagName === "else" ) {
+ current = stack.pop();
+ content = current[ 2 ];
+ isBlock = TRUE;
+ }
+ params = (params
+ ? (params + " ")
+ .replace( rParams, parseParams )
+ .replace( rBuildHash, function( all, keyValue, index ) {
+ hash += keyValue + ",";
+ return "";
+ })
+ : "");
+ params = params.slice( 0, -1 );
+ newNode = [
+ tagName,
+ useEncode ? encode || "none" : "",
+ isBlock && [],
+ "{" + hash + "_hash:'" + hash + "',_path:'" + params + "'}",
+ params
+ ];
+
+ if ( isBlock ) {
+ stack.push( current );
+ current = newNode;
+ }
+ content.push( newNode );
+ } else if ( closeBlock ) {
+ current = stack.pop();
+ }
+ loc = index + all.length; // location marker - parsed up to here
+ if ( !current ) {
+ throw "Expected block tag";
+ }
+ content = current[ 2 ];
+ }
+ markup = markup.replace( rEscapeQuotes, "\\$1" );
+ markup.replace( rTag, parseTag );
+ pushPreceedingContent( markup.length );
+ return buildTmplFunction( topNode );
+}
+
+// Build javascript compiled template function, from AST
+function buildTmplFunction( nodes ) {
+ var ret, node, i,
+ nested = [],
+ l = nodes.length,
+ code = "try{var views="
+ + (jQuery ? "jQuery" : "JsViews")
+ + '.views,tag=views.renderTag,enc=views.encode,html=views.encoders.html,$ctx=$view && $view.ctx,result=""+\n\n';
+
+ for ( i = 0; i < l; i++ ) {
+ node = nodes[ i ];
+ if ( node[ 0 ] === "*" ) {
+ code = code.slice( 0, i ? -1 : -3 ) + ";" + node[ 1 ] + ( i + 1 < l ? "result+=" : "" );
+ } else if ( "" + node === node ) { // type string
+ code += '"' + node + '"+';
+ } else {
+ var tag = node[ 0 ],
+ encode = node[ 1 ],
+ content = node[ 2 ],
+ obj = node[ 3 ],
+ params = node[ 4 ],
+ paramsOrEmptyString = params + '||"")+';
+
+ if( content ) {
+ nested.push( buildTmplFunction( content ));
+ }
+ code += tag === "="
+ ? (!encode || encode === "html"
+ ? "html(" + paramsOrEmptyString
+ : encode === "none"
+ ? ("(" + paramsOrEmptyString)
+ : ('enc("' + encode + '",' + paramsOrEmptyString)
+ )
+ : 'tag("' + tag + '",$view,"' + ( encode || "" ) + '",'
+ + (content ? nested.length : '""') // For block tags, pass in the key (nested.length) to the nested content template
+ + "," + obj + (params ? "," : "") + params + ")+";
+ }
+ }
+ ret = new Function( "$data, $view", code.slice( 0, -1) + ";return result;\n\n}catch(e){return views.err(e);}" );
+ ret.nested = nested;
+ return ret;
+}
+
+//========================== Private helper functions, used by code above ==========================
+
+function replacerForHtml( ch ) {
+ // Original code from Mike Samuel <msamuel@google.com>
+ return escapeMapForHtml[ ch ]
+ // Intentional assignment that caches the result of encoding ch.
+ || ( escapeMapForHtml[ ch ] = "&#" + ch.charCodeAt( 0 ) + ";" );
+}
+
+function try$( selector ) {
+ // If selector is valid, return jQuery object, otherwise return (invalid) selector string
+ try {
+ return $( selector );
+ } catch( e) {}
+ return selector;
+}
+})( window );
154 pulls/pullrequests.js
View
@@ -0,0 +1,154 @@
+function repoList(baseurl, org) {
+ this.url = baseurl+'orgs/'+org+'/repos';
+ this.data = {data:[]};
+ var t = this;
+ $("#backToRepolist").click(function (ev) { $.bbq.removeState('repo'); ev.preventDefault();});
+}
+
+repoList.prototype.showList = function() {
+ $("#repolist").fadeIn();
+ $("#backToRepolist").hide();
+ $("#repoContent").fadeOut();
+ $("#mainContent").show();
+ this.refreshView();
+}
+
+repoList.prototype.hideList = function() {
+ $("#repolist").fadeOut();
+ $("#mainContent").hide();
+ $("#backToRepolist").fadeIn();
+}
+
+repoList.prototype.refreshView = function() {
+ $("#repolist").html($("#repoListItemTemplate").render(this.data.data));
+ $("li", $("#repolist")).click(function(ev) { var reponame = loadRepo($(this).attr("repo")); $.bbq.pushState({ repo: reponame }); ev.preventDefault(); });
+}
+repoList.prototype.update = function() {
+ var t = this;
+ $("#loading").show();
+ $.ajax({ dataType: 'jsonp', url: this.url, success: function(d) { t.setData(d); $("#loading").hide(); } });
+}
+repoList.prototype.setData = function(data) {
+ this.data = data;
+ this.refreshView();
+}
+
+function loginHandler() {
+ var t = this;
+ this.user = false;
+ this.logindialog = $("#loginDialog").dialog({autoOpen: false});
+ this.checkLoggedIn();
+ $("#notloggedin").click(function(ev) {
+ t.showLoginForm();
+ ev.preventDefault();
+ } );
+ $("#loginBtn").click(function(ev) {
+ t.logindialog.dialog("close");
+ t.login();
+ ev.preventDefault();
+ } );
+}
+
+loginHandler.prototype.showLoginForm = function() {
+ this.logindialog.dialog("open");
+}
+
+loginHandler.prototype.login = function() {
+ var user = $("#userField").attr("value");
+ var pass = $("#passField").attr("value");
+ var t = this;
+
+ $.ajax({ url: API_URL, type: "POST", data: { action: 'login', user: user, pass: pass }, success: function(d) { t.updateLoginState(d); } });
+}
+
+loginHandler.prototype.checkLoggedIn = function() {
+ var t = this;
+ $("#checkinglogin").show();
+ $("#loggedin").hide();
+ $("#notloggedin").hide();
+ $.ajax({ url: API_URL+'?action=loggedin', success: function(d) { t.updateLoginState(d); } });
+}
+
+loginHandler.prototype.updateLoginState = function(d) {
+ var t = this;
+ if (d.success && d.user) {
+ $("#checkinglogin").hide();
+ $("#loggedin").html("Welcome "+d.user+" (<a href='#'>Logout</a>)").fadeIn();
+ $("#notloggedin").hide();
+ $("#loggedin a").click(function(ev) {
+ $.ajax({ url: API_URL+'?action=logout', success: function(d) { t.updateLoginState(d); } });
+ ev.preventDefault();
+ });
+ this.user = d.user;
+ } else {
+ $("#checkinglogin").hide();
+ $("#loggedin").hide();
+ $("#notloggedin").fadeIn();
+ this.user = false;
+ }
+}
+
+var repos;
+var login;
+
+$(document).ready(function() {
+ login = new loginHandler();
+ repos = new repoList(GITHUB_BASEURL, GITHUB_ORG);
+ repos.update();
+
+ $(window).bind( "hashchange", function(e) {
+ if ($.bbq.getState( "repo" )) {
+ loadRepo($.bbq.getState("repo"));
+ repos.hideList();
+ } else {
+ repos.showList();
+ }
+ });
+
+ $(window).trigger( "hashchange" );
+});
+
+function loadRepo(repo) {
+ $("#loading").show();
+ $.ajax({
+ dataType: 'jsonp',
+ url: GITHUB_BASEURL+'repos/'+GITHUB_ORG+"/"+repo+"/pulls",
+ success: function (data) {
+ $("#loading").hide();
+ $("#repoContent").html( $("#repoOverviewTemplate").render([{repoName: repo, pullList: $("#pullRequestListItem").render(data.data)}]));
+ $(".pullinstructions").click(function(ev) {
+ $('<div></div>').html($("#pullInstructionTemplate").render({ repo: repo, number: $(this).attr("number")}))
+ .dialog({title: $(this).attr("number")+': '+$(this).attr("title")+' ('+$(this).attr("state")+')', width: 800 });
+ ev.preventDefault();
+ });
+ $(".updatepullrequest").click(function(ev) {
+ var dia = $('<div></div>').html($("#updatePullRequestTemplate").render({}))
+ .dialog({title: $(this).attr("number")+': '+$(this).attr("title")+' ('+$(this).attr("state")+')' });
+ $("button", dia).click(function(r, n, dia) { return function(ev) { updateRepo(r, n, dia); ev.preventDefault();}}(repo, $(this).attr("number"), dia) );
+ ev.preventDefault();
+ });
+ $("#repoPullList").accordion({ autoHeight: false });
+ $("#repoContent").show();
+ $.bbq.pushState({ repo: repo });
+ }
+ });
+
+}
+
+function updateRepo(reponame, num, dia) {
+ var t = this;
+ if (!login.user) {
+ login.showLoginForm();
+ return;
+ }
+ $("#loading").show();
+ $.ajax({ url: API_URL, type: "POST", data: {
+ action: 'ghupdate',
+ repo: reponame,
+ id: num,
+ state: $("select", dia).attr("value"),
+ comment: $("textarea", dia).attr("value")
+ }, success: function(d) { loadRepo(reponame); } });
+ dia.dialog("destroy");
+}
+
Please sign in to comment.
Something went wrong with that request. Please try again.