Bug 384955 - Show content on git repository(ries) in a more dynamic way
diff --git a/bundles/org.eclipse.orion.client.core/web/orion/dynamicContent.js b/bundles/org.eclipse.orion.client.core/web/orion/dynamicContent.js
new file mode 100644
index 0000000..5a0506c
--- /dev/null
+++ b/bundles/org.eclipse.orion.client.core/web/orion/dynamicContent.js
@@ -0,0 +1,226 @@
+/*******************************************************************************

+ * @license

+ * Copyright (c) 2012 IBM Corporation and others.

+ * All rights reserved. This program and the accompanying materials are made 

+ * available under the terms of the Eclipse Public License v1.0 

+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 

+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). 

+ * 

+ * Contributors: IBM Corporation - initial API and implementation

+ ******************************************************************************/

+ define(['dojo', 'dijit'], function(dojo, dijit){

+ 

+	/**

+	* Default progress indicator in form of a simple spinner.

+	*

+	* @param id [required] unique identifier, e.g. the row number in which the spinner is created

+	* @param anchor [required] father DOM node for the created spinner

+	*

+	* @returns ProgressSpinner object

+	*/

+	function ProgressSpinner(id, anchor){

+		if(id === undefined){ throw new Error("Missing reqired argument: id"); }

+		if(anchor === undefined){ throw new Error("Missing reqired argument: anchor"); }

+		

+		this._id = id;

+		this._anchor = anchor;

+		

+		// we add a prefix for the id label

+		this._prefix = "progressSpinner:";

+	}

+	

+	ProgressSpinner.prototype = {

+		

+		/**

+		* [interface] starts the progress indicator

+		*/

+		start: function(){

+			dojo.create("img", {"id":this._prefix+this._id, "src":"/images/none.png", "class":"progressPane_running"}, this._anchor);	

+		},

+		

+		/**

+		* [interface] stops the progress indicator

+		*/

+		stop: function(){

+			dojo.destroy(dojo.byId(this._prefix+this._id));

+		},

+		

+		/**

+		* [interface] renders the progress indicator after an population error

+		*/

+		error: function(err){

+			var indicator = dojo.byId(this._prefix+this._id);

+			indicator.src = "/images/problem.gif";

+			dojo.removeAttr(indicator, "class");

+			

+			new dijit.Tooltip({

+				connectId: [this._prefix+this._id],

+				label: err

+			});

+		}

+	};

+	

+	// add constructor

+	ProgressSpinner.prototype.constructor = ProgressSpinner;

+ 

+	/**

+	* Dynamic content model which handles the population logic

+	*

+	* @param objects [required] collection of objects to be populated

+	* @param populate [required] population function (i), which populates the i-th object in the collection

+	*

+	* @returns DynamicContentModel object

+	*/

+	function DynamicContentModel(objects, populate){

+		if(!objects) { throw new Error("Missing reqired argument: objects"); }

+		if(!populate) { throw new Error("Missing reqired argument: populate"); }

+	

+		this._objects = objects;

+		this._populate = populate;

+	}

+	

+	DynamicContentModel.prototype = {

+	

+		/**

+		* [interface] returns the object collection

+		*/

+		getObjects : function(){

+			return this._objects;

+		},

+		

+		/**

+		* [interface] returns the deferred for i-th element population

+		*/

+		getDetails : function(i){

+			return this._populate(i);

+		}

+	};

+	

+	// add constructor

+	DynamicContentModel.prototype.constructor = DynamicContentModel;

+ 

+	/**

+	* Dynamic content renderer which provides render methods.

+	* After being used in an explorer, the renderer gains access

+	* to the explorer through the explorer field

+	*

+	* @returns DynamicContentRenderer object

+	*/

+	function DynamicContentRenderer(){ }

+	

+	DynamicContentRenderer.prototype = {

+		// default progress indicator

+		progressIndicator : ProgressSpinner,

+		

+		// default setup properties

+		setupProperties : {

+			//progress indicator list

+			progressIndicators : []

+		},

+		

+		// default error handler

+		errorHandler : function(i, err){

+			this.explorer.progressIndicators[i].error(err);

+		}

+	};

+	

+	// add constructor

+	DynamicContentRenderer.prototype.constructor = DynamicContentRenderer;

+ 

+	/**

+	* Dynamic content explorer which allows dynamic population and rendering of the given model

+	* @param model [required] dynamic content model to be explored

+	*

+	* @returns DynamicContentExplorer object

+	*/

+	function DynamicContentExplorer(model){

+		if(!model) { throw new Error("Missing required argument: model"); }	

+		this._model = model;

+		

+		//default renderer

+		this.use(new DynamicContentRenderer());

+	}

+	

+	DynamicContentExplorer.prototype = {		

+		/**

+		* [interface] dynamically populates and renders the model.

+		*/

+		render : function(){

+			var that = this;

+		

+			//render initial data

+			if(this._initialRender){ this._initialRender(); }

+			

+			//called if the i-th object is successfully populated

+			var populationSuccess = function(i){

+				return function(){

+					//after population work

+					that._renderAfterItemPopulation(i);

+					

+					//stop indicator

+					that.progressIndicators[i].stop();

+				};

+			};

+			

+			//called if the i-th object could not be successfully populated

+			var populationFailure = function(i){

+				return function(resp){

+					if(that._errorHandler) { that._errorHandler(i, resp); }

+					else { throw new Error(resp); }

+				};

+			};

+			

+			for(var i=0; i<this._model.getObjects().length; i++){

+				//before population render work

+				this._renderBeforeItemPopulation(i);

+				

+				//start indicator

+				this.progressIndicators[i].start();

+				

+				//population

+				this._model.getDetails(i).then(

+					populationSuccess(i), populationFailure(i)

+				);

+			}

+			

+			//cleanup render work

+			if(this._cleanupRender) { this._cleanupRender(); }

+		},

+		

+		/**

+		* [interface] extends explorer functionality using the render functions in obj.

+		*/

+		use : function(obj){

+			for(var field in obj){

+				if(field === "populateItem"){ this._populateItem = obj.populateItem.bind(obj); obj.explorer = this; }

+				else if(field === "renderBeforeItemPopulation"){ this._renderBeforeItemPopulation = obj.renderBeforeItemPopulation.bind(obj); obj.explorer = this; }

+				else if(field === "renderAfterItemPopulation"){ this._renderAfterItemPopulation = obj.renderAfterItemPopulation.bind(obj); obj.explorer = this; }

+				else if(field === "errorHandler"){ this._errorHandler = obj.errorHandler.bind(obj); obj.explorer = this; }

+				else if(field === "initialRender"){ this._initialRender = obj.initialRender.bind(obj); obj.explorer = this; }

+				else if(field === "cleanupRender"){ this._cleanupRender = obj.cleanupRender.bind(obj); obj.explorer = this; }

+				

+				// inherit the progress indicator

+				else if(field === "progressIndicator"){ this.progressIndicator = obj.progressIndicator; obj.explorer = this; }

+				

+				// inherit some setup properties

+				else if(field === "setupProperties"){

+					for(var property in obj.setupProperties){

+						this[property] = obj.setupProperties[property];

+					}

+					

+					obj.explorer = this;

+				}

+			}

+		}

+	};

+	

+	// add constructor

+	DynamicContentExplorer.prototype.constructor = DynamicContentExplorer;

+	

+	return {

+		DynamicContentModel : DynamicContentModel,

+		DynamicContentExplorer : DynamicContentExplorer,

+		DynamicContentRenderer : DynamicContentRenderer,

+		ProgressSpinner : ProgressSpinner

+	};

+ });
\ No newline at end of file
diff --git a/bundles/org.eclipse.orion.client.git/web/orion/git/gitRepositoryExplorer.js b/bundles/org.eclipse.orion.client.git/web/orion/git/gitRepositoryExplorer.js
index ed2e487..37f7e7a 100644
--- a/bundles/org.eclipse.orion.client.git/web/orion/git/gitRepositoryExplorer.js
+++ b/bundles/org.eclipse.orion.client.git/web/orion/git/gitRepositoryExplorer.js
@@ -11,8 +11,8 @@
 
 /*global define dijit console document Image */
 
-define(['i18n!git/nls/gitmessages', 'require', 'dojo', 'orion/commands', 'orion/section', 'orion/util', 'orion/PageUtil', 'orion/globalCommands', 'orion/git/gitCommands', 'orion/git/widgets/CommitTooltipDialog'], 
-		function(messages, require, dojo, mCommands, mSection, mUtil, PageUtil, mGlobalCommands, mGitCommands) {
+define(['i18n!git/nls/gitmessages', 'require', 'dojo', 'orion/commands', 'orion/section', 'orion/dynamicContent', 'orion/util', 'orion/PageUtil', 'orion/globalCommands', 'orion/git/gitCommands', 'orion/git/widgets/CommitTooltipDialog'], 
+		function(messages, require, dojo, mCommands, mSection, mDynamicContent, mUtil, PageUtil, mGlobalCommands, mGitCommands) {
 var exports = {};
 
 exports.GitRepositoryExplorer = (function() {
@@ -227,36 +227,40 @@
 	
 	// Git repo
 	
-	GitRepositoryExplorer.prototype.decorateRepositories = function(repositories, mode, deferred){
+	GitRepositoryExplorer.prototype.decorateRepository = function(repository, mode, deferred){
 		var that = this;
 		if (deferred == null){
 			deferred = new dojo.Deferred();
 		}
 		
-		if (repositories.length > 0) {
-			this.registry.getService("orion.core.file").loadWorkspace(repositories[0].ContentLocation + "?parts=meta").then( //$NON-NLS-1$ //$NON-NLS-0$
+		if(!mode){
+			mode = "full";
+		}
+		
+		
+		this.registry.getService("orion.core.file").loadWorkspace(repository.ContentLocation + "?parts=meta").then( //$NON-NLS-1$ //$NON-NLS-0$
 				function(resp){
-					repositories[0].Content = {};
+					repository.Content = {};
 					
 					var path = "root / "; //$NON-NLS-0$
-					if (resp.Parents != null)
+					if (resp.Parents !== null)
 						for (var i=resp.Parents.length; i>0; i--){
 							path += resp.Parents[i-1].Name + " / "; //$NON-NLS-0$
 						}
+						
 					path += resp.Name;
-					
-					repositories[0].Content.Path = path;
+					repository.Content.Path = path;
 					
 					if (mode !== "full"){ //$NON-NLS-0$
-						that.decorateRepositories(repositories.slice(1), mode, deferred);
+						deferred.callback();
 						return;
 					}
 					
-					that.registry.getService("orion.git.provider").getGitStatus(repositories[0].StatusLocation).then( //$NON-NLS-0$
+					that.registry.getService("orion.git.provider").getGitStatus(repository.StatusLocation).then( //$NON-NLS-0$
 						function(resp){
-							repositories[0].Status = resp;
+							repository.Status = resp;
 
-							that.registry.getService("orion.git.provider").getGitBranch(repositories[0].BranchLocation).then( //$NON-NLS-0$
+							that.registry.getService("orion.git.provider").getGitBranch(repository.BranchLocation).then( //$NON-NLS-0$
 								function(resp){
 									var branches = resp.Children;
 									var currentBranch;
@@ -267,99 +271,136 @@
 										}
 									}
 									
-									if (!currentBranch || currentBranch.RemoteLocation[0] == null){
-										that.decorateRepositories(repositories.slice(1), mode, deferred);
+									if (!currentBranch || currentBranch.RemoteLocation[0] === null){
+										deferred.callback();
 										return;
 									};
 									
-									var tracksRemoteBranch = (currentBranch.RemoteLocation.length == 1 && currentBranch.RemoteLocation[0].Children.length === 1);
+									var tracksRemoteBranch = (currentBranch.RemoteLocation.length === 1 && currentBranch.RemoteLocation[0].Children.length === 1);
 									
 									if (tracksRemoteBranch && currentBranch.RemoteLocation[0].Children[0].CommitLocation){
 										that.registry.getService("orion.git.provider").getLog(currentBranch.RemoteLocation[0].Children[0].CommitLocation + "?page=1&pageSize=20", "HEAD").then( //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-0$
 											function(resp){
-												repositories[0].CommitsToPush = resp.Children.length;
+												if(resp.Children === undefined) { repository.CommitsToPush = 0; }
+												else { repository.CommitsToPush = resp.Children.length; }
+												deferred.callback();
+												return;
+											}, function(resp){
+												deferred.errback();
+												return;
 											}
 										);
 									} else {
 										that.registry.getService("orion.git.provider").doGitLog(currentBranch.CommitLocation + "?page=1&pageSize=20").then(  //$NON-NLS-1$ //$NON-NLS-0$
 											function(resp){	
-												repositories[0].CommitsToPush = resp.Children.length;
+												if(resp.Children === undefined) { repository.CommitsToPush = 0; }
+												else { repository.CommitsToPush = resp.Children.length; }
+												deferred.callback();
+												return;
+											}, function(resp){
+												deferred.errback();
+												return;
 											}
 										);	
 									}
-									
-									that.decorateRepositories(repositories.slice(1), mode, deferred);
+								}, function(resp){
+									deferred.errback();
+									return;
 								}
 							);
+						}, function(resp){
+							deferred.errback();
+							return;
 						}	
 					);
-				}
+				}, function(resp){
+					deferred.errback();
+					return;
+				 }
 			);
-		} else {
-			deferred.callback();
-		}
 		
 		return deferred;
 	};
 	
 	GitRepositoryExplorer.prototype.displayRepositories = function(repositories, mode, links){
 		var that = this;
-		
-		var tableNode = dojo.byId( 'table' );	 //$NON-NLS-0$
-		dojo.empty( tableNode );
-	
 		var progressService = this.registry.getService("orion.page.message"); //$NON-NLS-0$
 		
-		if (!repositories || repositories.length === 0){
-			var titleWrapper = new mSection.Section(tableNode, {
-				id: "repositorySection", //$NON-NLS-0$
-				title: "Repository",
-				iconClass: "gitImageSprite git-sprite-repository" //$NON-NLS-0$
-			});
-			titleWrapper.setTitle(mode === "full" ? messages["No Repositories"] : messages["Repository Not Found"]); //$NON-NLS-0$
-			that.loadingDeferred.callback();
-			progressService.setProgressMessage("");
-			return;
-		}
-
-		var contentParent = dojo.create("div", {"role": "region", "class":"sectionTable"}, tableNode, "last");
-		contentParent.innerHTML = '<list id="repositoryNode" class="mainPadding"></list>'; //$NON-NLS-0$
-
-		this.decorateRepositories(repositories, mode).then(
-			function(){
-				for(var i=0; i<repositories.length;i++){
-					that.renderRepository(repositories[i], i, repositories.length, mode, links);
+		var dynamicContentModel = new mDynamicContent.DynamicContentModel(repositories,
+			function(i){
+				return that.decorateRepository.bind(that)(repositories[i]);
+			}
+		);
+		
+		var dcExplorer = new mDynamicContent.DynamicContentExplorer(dynamicContentModel);
+		var repositoryRenderer = {
+		
+			initialRender : function(){
+				var tableNode = dojo.byId('table');	 //$NON-NLS-0$
+				dojo.empty(tableNode);
+				
+				if(!repositories || repositories.length === 0){
+					var titleWrapper = new mSection.Section(tableNode, {
+						id: "repositorySection", //$NON-NLS-0$
+						title: "Repository",
+						iconClass: "gitImageSprite git-sprite-repository" //$NON-NLS-0$
+					});
+					titleWrapper.setTitle(mode === "full" ? messages["No Repositories"] : messages["Repository Not Found"]); //$NON-NLS-0$
+					that.loadingDeferred.callback();
+					progressService.setProgressMessage("");
+					return;
 				}
+				
+				var contentParent = dojo.create("div", {"role": "region", "class":"sectionTable"}, tableNode, "last");
+				contentParent.innerHTML = '<list id="repositoryNode" class="mainPadding"></list>'; //$NON-NLS-0$
+			},
+			
+			cleanupRender : function(){
 				that.loadingDeferred.callback();
 				progressService.setProgressMessage("");
 			},
-			function(){
-				that.loadingDeferred.errback();
-				progressService.setProgressMessage("");
+			
+			renderBeforeItemPopulation : function(i){
+				var extensionListItem = dojo.create( "div", { "class":"sectionTableItem " + ((repositories.length === 1) ? "" : ((i % 2) ? "darkTreeTableRow" : "lightTreeTableRow"))}, dojo.byId("repositoryNode") ); //$NON-NLS-5$ //$NON-NLS-4$ //$NON-NLS-3$ //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-0$
+				var horizontalBox = dojo.create( "div", null, extensionListItem ); //$NON-NLS-0$
+				
+				var detailsView = dojo.create( "div", { "class":"stretch" }, horizontalBox ); //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-0$
+				var title = dojo.create( "span", { "class":"gitMainDescription"}, detailsView ); //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-0$
+				
+				if (links){
+					var link = dojo.create("a", {"class": "navlinkonpage", href: "/git/git-repository.html#" + repositories[i].Location}, title); //$NON-NLS-3$ //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-0$
+					dojo.place(document.createTextNode(repositories[i].Name), link);
+				} else { dojo.place(document.createTextNode(repositories[i].Name), title); }
+				
+				//create indicator
+				this.explorer.progressIndicators[i] = new this.explorer.progressIndicator(i, title);
+				
+				dojo.create("div", null, detailsView);
+				dojo.create("span", {"class" : "gitSecondaryDescription", innerHTML : (repositories[i].GitUrl != null ? "git url: " + repositories[i].GitUrl : messages["(no remote)"]) }, detailsView);
+				dojo.create("div", null, detailsView);
+				dojo.create("span", { "id" : "location"+i, "class":"gitSecondaryDescription" }, detailsView);
+				
+				dojo.create("div", {"style" : "padding-top:10px"}, detailsView);
+				dojo.create("span", {"id":"workspaceState"+i, "class":"gitSecondaryDescription", "style" : "padding-left:10px"}, detailsView);
+				dojo.create("span", {"id":"commitsState"+i, "class":"gitSecondaryDescription", "style" : "padding-left:10px"}, detailsView);
+				
+				if (mode === "full"){
+					var actionsArea = dojo.create( "div", {"id":"repositoryActionsArea", "class":"sectionTableItemActions" }, horizontalBox ); //$NON-NLS-4$ //$NON-NLS-3$ //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-0$
+					that.commandService.renderCommands(that.actionScopeId, actionsArea, repositories[i], that, "tool"); //$NON-NLS-0$
+				}
+			},
+			
+			renderAfterItemPopulation : function(i){
+				that.renderRepository(repositories[i], i, repositories.length, mode, links);
 			}
-		);
+		};
+		
+		dcExplorer.use(repositoryRenderer);
+		dcExplorer.render();
 	};
 	
 	GitRepositoryExplorer.prototype.renderRepository = function(repository, index, length, mode, links){
-		var extensionListItem = dojo.create( "div", { "class":"sectionTableItem " + ((length == 1) ? "" : ((index % 2) ? "darkTreeTableRow" : "lightTreeTableRow"))}, dojo.byId("repositoryNode") ); //$NON-NLS-5$ //$NON-NLS-4$ //$NON-NLS-3$ //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-0$
-		var horizontalBox = dojo.create( "div", null, extensionListItem ); //$NON-NLS-0$
-		
-		var detailsView = dojo.create( "div", { "class":"stretch" }, horizontalBox ); //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-0$
-		var title = dojo.create( "span", { "class":"gitMainDescription"}, detailsView ); //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-0$
-		
-		if (links){
-			link = dojo.create("a", {"class": "navlinkonpage", href: "/git/git-repository.html#" + repository.Location}, title); //$NON-NLS-3$ //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-0$
-			dojo.place(document.createTextNode(repository.Name), link);
-		} else {
-			dojo.place(document.createTextNode(repository.Name), title);
-		}
-
-		dojo.create( "div", null, detailsView ); //$NON-NLS-0$
-		var description = dojo.create( "span", { "class":"gitSecondaryDescription",  //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-0$
-			innerHTML: (repository.GitUrl != null ? "git url: " + repository.GitUrl : messages["(no remote)"]) }, detailsView );
-		dojo.create( "div", null, detailsView ); //$NON-NLS-0$
-		var description = dojo.create( "span", { "class":"gitSecondaryDescription", innerHTML: messages["location: "] + repository.Content.Path }, detailsView ); //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-0$
-		
+		dojo.byId("location"+index).innerHTML = messages["location: "] + repository.Content.Path;
 		var status = repository.Status;
 		
 		if (mode === "full"){ //$NON-NLS-0$
@@ -369,17 +410,11 @@
 			var workspaceState = ((unstaged > 0 || staged > 0) 
 				? dojo.string.substitute(messages["${0} file(s) to stage and ${1} file(s) to commit."], [unstaged, staged])
 				: messages["Nothing to commit."]);
-			dojo.create( "div", {"style":"padding-top:10px"}, detailsView );		 //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-0$
-			dojo.create( "span", { "class":"gitSecondaryDescription", "style":"padding-left:10px", innerHTML: workspaceState}, detailsView ); //$NON-NLS-4$ //$NON-NLS-3$ //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-0$
+			
+			dojo.byId("workspaceState"+index).innerHTML = workspaceState;
 			
 			var commitsState = repository.CommitsToPush;
-			dojo.create( "span", { "class":"gitSecondaryDescription", "style":"padding-left:10px",  //$NON-NLS-4$ //$NON-NLS-3$ //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-0$
-				innerHTML: ((commitsState > 0 ) ? commitsState + messages[" commit(s) to push."] : messages["Nothing to push."])}, detailsView );
-		}
-		
-		if (mode === "full"){
-			var actionsArea = dojo.create( "div", {"id":"repositoryActionsArea", "class":"sectionTableItemActions" }, horizontalBox ); //$NON-NLS-4$ //$NON-NLS-3$ //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-0$
-			this.commandService.renderCommands(this.actionScopeId, actionsArea, repository, this, "tool"); //$NON-NLS-0$
+			dojo.byId("commitsState"+index).innerHTML = ((commitsState > 0) ? commitsState + messages[" commit(s) to push."] : messages["Nothing to push."]);	
 		}
 	};