[ARVADOS] updated: 983e7c81e1a185eda8f5c71ab78c4b207116edf3

git at public.curoverse.com git at public.curoverse.com
Mon Apr 20 18:23:12 EDT 2015


Summary of changes:
 .../app/assets/javascripts/application.js          |   6 +
 .../app/assets/javascripts/pipeline_instances.js   |  13 +-
 .../controllers/pipeline_instances_controller.rb   |  54 +++++
 apps/workbench/app/controllers/users_controller.rb |   6 +-
 apps/workbench/app/helpers/application_helper.rb   |  43 +++-
 .../app/helpers/pipeline_instances_helper.rb       |   7 +
 apps/workbench/app/models/repository.rb            |  12 +-
 .../pipeline_instances/_running_component.html.erb |   2 +-
 .../pipeline_instances/_show_components.html.erb   |   2 +
 .../_show_components_editable.html.erb             |  20 --
 .../views/pipeline_instances/_show_inputs.html.erb |   2 +
 .../views/projects/_show_contents_rows.html.erb    |   4 +-
 .../test/helpers/collections_helper_test.rb        |   2 +
 .../test/integration/anonymous_access_test.rb      |  21 +-
 apps/workbench/test/integration/errors_test.rb     |  65 +++---
 apps/workbench/test/integration/jobs_test.rb       |   4 +-
 apps/workbench/test/integration/projects_test.rb   |   2 +-
 apps/workbench/test/test_helper.rb                 |   2 +-
 doc/api/methods/jobs.html.textile.liquid           |   6 +-
 doc/api/schema/Job.html.textile.liquid             |   5 +-
 docker/keep/run-keep.in                            |   2 +-
 docker/sso/Dockerfile                              |   3 +-
 sdk/python/tests/run_test_server.py                |  18 +-
 services/api/Gemfile                               |   1 +
 services/api/Gemfile.lock                          |   4 +
 .../app/controllers/arvados/v1/jobs_controller.rb  |  13 +-
 services/api/app/models/commit.rb                  | 255 +++++++++++++--------
 services/api/app/models/job.rb                     |  43 +++-
 services/api/config/application.default.yml        |   1 +
 services/api/script/crunch-dispatch.rb             |  80 ++++---
 services/api/test/fixtures/pipeline_instances.yml  |  19 +-
 .../arvados/v1/commits_controller_test.rb          |  98 --------
 .../functional/arvados/v1/jobs_controller_test.rb  |  41 ++++
 services/api/test/helpers/git_test_helper.rb       |  32 ++-
 services/api/test/test_helper.rb                   |   1 +
 services/api/test/unit/commit_test.rb              | 156 ++++++++++++-
 services/api/test/unit/job_test.rb                 |  11 +
 services/keepstore/handler_test.go                 |  54 ++++-
 services/keepstore/handlers.go                     | 185 +++++++--------
 services/keepstore/keepstore.go                    | 164 ++++++++-----
 services/keepstore/keepstore_test.go               | 163 +++++++------
 services/keepstore/trash_worker.go                 |  15 +-
 services/keepstore/trash_worker_test.go            |   6 +-
 services/keepstore/volume.go                       | 185 ++++-----------
 services/keepstore/volume_test.go                  | 151 ++++++++++++
 services/keepstore/volume_unix.go                  |  31 ++-
 services/keepstore/volume_unix_test.go             |  57 ++++-
 47 files changed, 1294 insertions(+), 773 deletions(-)
 create mode 100644 services/keepstore/volume_test.go

  discards  d477846a052d7dbc2b4600996a1d991e874762de (commit)
  discards  d4deb0590e54eece238c1c5ad120e0daffd7806a (commit)
  discards  34abff5723aa938d03a1709e3c28899b2949b4d6 (commit)
  discards  0929ea3535871ac7f3d4a2d2f8c19d785565fef3 (commit)
  discards  1816e68eaedb6ca1d66a532a813ec9d09ffa845b (commit)
  discards  84166880a8b7a4401e5d7dd4aea4b6d97849b7fc (commit)
  discards  8634d02fc3887f21abc4d034ca3e03194f926427 (commit)
  discards  90b9ddc1b84d9a910cd5b46610b02c83c62f1e5a (commit)
       via  983e7c81e1a185eda8f5c71ab78c4b207116edf3 (commit)
       via  94b57902775c4aaa02cb7532f07415729ef353df (commit)
       via  08717a74dbc29916e4466119d52863b095e271a4 (commit)
       via  de67953481cbedc480822f97cdfe5eb6dffcf0d3 (commit)
       via  e24125041ad492b45c97feffb33a037c5adda734 (commit)
       via  3c36af3d62af5e4ee98aaafab80858182fb5cab8 (commit)
       via  86860e7d65589bc9d93df5b514baee3fc5a5103a (commit)
       via  9136a1b1314084e149f86ceec16d1482ccf5d8af (commit)
       via  4f99b52f6866deaeb4b614e165b7af0cb3a6adba (commit)
       via  c674deff8855005e39b5ddf230372cb241bc22b3 (commit)
       via  916a3a8b0dc64709b32e491cf249fcafe0762e65 (commit)
       via  86e078ae126f6651428219c726c34da3bd7f7495 (commit)
       via  e36e6c4e56b4c0667ee3c75cd20e78382327aee9 (commit)
       via  29a1b2c8894db8e6c6b840220b45371c521a17d2 (commit)
       via  f8e6cb30ca6a3cdb20be47f7a81663d4affd0b7f (commit)
       via  b3a23a94b826de04ae02b889eba4e71d9a4ee11f (commit)
       via  2fe2dca0080d20a257e9d750cd6ca9d094f01a61 (commit)
       via  528f9bb789c2c7f5fbf0838732d470a332292901 (commit)
       via  0f56ce4b6192c3d8e00d1fcbb9d5a2e1a2d953c9 (commit)
       via  430ed273384c153c9c78c653db8e02fd54aa2e4a (commit)
       via  c550609485691d8107ae364bfc982569f81f1725 (commit)
       via  3355f801d1c2bb243e4091a4f31cc83a5a1a5d77 (commit)
       via  ae34bc2de285f5bce4a3a6537d454a62f2fa52e6 (commit)
       via  a0993e451f8a5e209df74dc9f8f0e55bdf1c73bd (commit)
       via  9ede4c6a9d45033d0874cb3fa8d2356aeae6fa83 (commit)
       via  1fe347f8cf77564a791b9f98963fc73ee6802c4f (commit)
       via  c5cd44ad4ff0b5d65cab30b8eb702ab3e238a499 (commit)
       via  4ccec6c3e6e96edc4917f15769a30e187484ee52 (commit)
       via  de0f310b710c2a05517e231a8b489301300fed11 (commit)
       via  e9be782d70efaf8c9bf3fc0043d8a17dfe776bfc (commit)
       via  6a8f05bbd4e8ae51464ece5ee73898d7f58edd71 (commit)
       via  dcf97f13fa730ba7af3fee9b6d7044592a30a2be (commit)
       via  52a9e646ea8247d5e1446ab98ba72ab2edb5c703 (commit)
       via  4d078362f10fba8b94cce3ecc3ed8b4924b79b41 (commit)
       via  b0ec95277b9be2bcbe8e35008f490855a98cd70e (commit)
       via  d037508525b0e3c09a475e2ccfb5f7dd7f39f62d (commit)
       via  fc9a042a58627303ecfbda7660563fcfbe458ea5 (commit)
       via  7afe2c73ccdaad21b2d36b345a1627c1ad3f51a1 (commit)
       via  dc96093e6a9d30699ea06d65ebe1ffe6d59977b9 (commit)
       via  41caa50724d03189c5b4104c52bd5253974cf535 (commit)
       via  0b1d679afec2976ee170692f4178fdc28eb25a04 (commit)
       via  bb1380996abe05337ba86061f5edd921dd3b9193 (commit)
       via  efa119244cd38090933fbd0ba29f3604889e9aa9 (commit)
       via  976f560ab04bf570ed58664d97b8b6069b314941 (commit)

This update added new revisions after undoing existing revisions.  That is
to say, the old revision is not a strict subset of the new revision.  This
situation occurs when you --force push a change and generate a repository
containing something like this:

 * -- * -- B -- O -- O -- O (d477846a052d7dbc2b4600996a1d991e874762de)
            \
             N -- N -- N (983e7c81e1a185eda8f5c71ab78c4b207116edf3)

When this happens we assume that you've already had alert emails for all
of the O revisions, and so we here report only the revisions in the N
branch from the common base, B.

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit 983e7c81e1a185eda8f5c71ab78c4b207116edf3
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Apr 20 16:43:40 2015 -0400

    5416: Fix protected method that should have been public.

diff --git a/apps/workbench/app/models/repository.rb b/apps/workbench/app/models/repository.rb
index 28f8f0c..b9b9ce3 100644
--- a/apps/workbench/app/models/repository.rb
+++ b/apps/workbench/app/models/repository.rb
@@ -57,6 +57,12 @@ class Repository < ArvadosBase
     @buggy_git_version
   end
 
+  # http_fetch_url returns the first http:// or https:// url (if any)
+  # in the api response's clone_urls attribute.
+  def http_fetch_url
+    clone_urls.andand.select { |u| /^http/ =~ u }.first
+  end
+
   protected
 
   # refresh fetches the latest repository content into the local
@@ -68,12 +74,6 @@ class Repository < ArvadosBase
     @fresh = true
   end
 
-  # http_fetch_url returns the first http:// or https:// url (if any)
-  # in the api response's clone_urls attribute.
-  def http_fetch_url
-    clone_urls.andand.select { |u| /^http/ =~ u }.first
-  end
-
   # run_git sets up the ARVADOS_API_TOKEN environment variable,
   # creates a local git directory for this repository if necessary,
   # executes "git --git-dir localgitdir {args to run_git}", and

commit 94b57902775c4aaa02cb7532f07415729ef353df
Author: Tom Clegg <tom at curoverse.com>
Date:   Thu Apr 16 15:52:20 2015 -0400

    5416: Use http://foo:bar@host:port/ instead of credential helper.

diff --git a/services/arv-git-httpd/auth_handler.go b/services/arv-git-httpd/auth_handler.go
index ef16acb..df2d8d6 100644
--- a/services/arv-git-httpd/auth_handler.go
+++ b/services/arv-git-httpd/auth_handler.go
@@ -52,7 +52,7 @@ func (h *authHandler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
 			w.WriteHeader(statusCode)
 			w.Write([]byte(statusText))
 		}
-		log.Println(quoteStrings(r.RemoteAddr, username, password, wroteStatus, statusText, repoName, r.URL.Path)...)
+		log.Println(quoteStrings(r.RemoteAddr, username, password, wroteStatus, statusText, repoName, r.Method, r.URL.Path)...)
 	}()
 
 	// HTTP request username is logged, but unused. Password is an
@@ -87,7 +87,7 @@ func (h *authHandler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
 	arv.ApiToken = password
 	reposFound := arvadosclient.Dict{}
 	if err := arv.List("repositories", arvadosclient.Dict{
-		"filters": [][]string{[]string{"name", "=", repoName}},
+		"filters": [][]string{{"name", "=", repoName}},
 	}, &reposFound); err != nil {
 		statusCode, statusText = http.StatusInternalServerError, err.Error()
 		return
diff --git a/services/arv-git-httpd/server_test.go b/services/arv-git-httpd/server_test.go
index 82d71ae..d773dd9 100644
--- a/services/arv-git-httpd/server_test.go
+++ b/services/arv-git-httpd/server_test.go
@@ -14,6 +14,12 @@ import (
 
 var _ = check.Suite(&IntegrationSuite{})
 
+const (
+	spectatorToken = "zw2f4gwx8hw8cjre7yp6v1zylhrhn3m5gvjq73rtpwhmknrybu"
+	activeToken    = "3kg6k6lzmp9kj5cpkcoxie963cmvjahbt2fod9zru30k1jqdmi"
+	anonymousToken = "4kg6k6lzmp9kj4cpkcoxie964cmvjahbt4fod9zru44k4jqdmi"
+)
+
 // IntegrationSuite tests need an API server and an arv-git-httpd server
 type IntegrationSuite struct {
 	tmpRepoRoot string
@@ -23,55 +29,43 @@ type IntegrationSuite struct {
 
 func (s *IntegrationSuite) TestPathVariants(c *check.C) {
 	s.makeArvadosRepo(c)
-	// Spectator token
-	os.Setenv("ARVADOS_API_TOKEN", "zw2f4gwx8hw8cjre7yp6v1zylhrhn3m5gvjq73rtpwhmknrybu")
 	for _, repo := range []string{"active/foo.git", "active/foo/.git", "arvados.git", "arvados/.git"} {
-		err := s.runGit(c, "fetch", repo)
+		err := s.runGit(c, spectatorToken, "fetch", repo)
 		c.Assert(err, check.Equals, nil)
 	}
 }
 
 func (s *IntegrationSuite) TestReadonly(c *check.C) {
-	// Spectator token
-	os.Setenv("ARVADOS_API_TOKEN", "zw2f4gwx8hw8cjre7yp6v1zylhrhn3m5gvjq73rtpwhmknrybu")
-	err := s.runGit(c, "fetch", "active/foo.git")
+	err := s.runGit(c, spectatorToken, "fetch", "active/foo.git")
 	c.Assert(err, check.Equals, nil)
-	err = s.runGit(c, "push", "active/foo.git", "master:newbranchfail")
+	err = s.runGit(c, spectatorToken, "push", "active/foo.git", "master:newbranchfail")
 	c.Assert(err, check.ErrorMatches, `.*HTTP code = 403.*`)
 	_, err = os.Stat(s.tmpRepoRoot + "/zzzzz-s0uqq-382brsig8rp3666/.git/refs/heads/newbranchfail")
 	c.Assert(err, check.FitsTypeOf, &os.PathError{})
 }
 
 func (s *IntegrationSuite) TestReadwrite(c *check.C) {
-	// Active user token
-	os.Setenv("ARVADOS_API_TOKEN", "3kg6k6lzmp9kj5cpkcoxie963cmvjahbt2fod9zru30k1jqdmi")
-	err := s.runGit(c, "fetch", "active/foo.git")
+	err := s.runGit(c, activeToken, "fetch", "active/foo.git")
 	c.Assert(err, check.Equals, nil)
-	err = s.runGit(c, "push", "active/foo.git", "master:newbranch")
+	err = s.runGit(c, activeToken, "push", "active/foo.git", "master:newbranch")
 	c.Assert(err, check.Equals, nil)
 	_, err = os.Stat(s.tmpRepoRoot + "/zzzzz-s0uqq-382brsig8rp3666/.git/refs/heads/newbranch")
 	c.Assert(err, check.Equals, nil)
 }
 
 func (s *IntegrationSuite) TestNonexistent(c *check.C) {
-	// Spectator token
-	os.Setenv("ARVADOS_API_TOKEN", "zw2f4gwx8hw8cjre7yp6v1zylhrhn3m5gvjq73rtpwhmknrybu")
-	err := s.runGit(c, "fetch", "thisrepodoesnotexist.git")
+	err := s.runGit(c, spectatorToken, "fetch", "thisrepodoesnotexist.git")
 	c.Assert(err, check.ErrorMatches, `.* not found.*`)
 }
 
 func (s *IntegrationSuite) TestMissingGitdirReadableRepository(c *check.C) {
-	// Active user token
-	os.Setenv("ARVADOS_API_TOKEN", "3kg6k6lzmp9kj5cpkcoxie963cmvjahbt2fod9zru30k1jqdmi")
-	err := s.runGit(c, "fetch", "active/foo2.git")
+	err := s.runGit(c, activeToken, "fetch", "active/foo2.git")
 	c.Assert(err, check.ErrorMatches, `.* not found.*`)
 }
 
 func (s *IntegrationSuite) TestNoPermission(c *check.C) {
-	// Anonymous token
-	os.Setenv("ARVADOS_API_TOKEN", "4kg6k6lzmp9kj4cpkcoxie964cmvjahbt4fod9zru44k4jqdmi")
 	for _, repo := range []string{"active/foo.git", "active/foo/.git"} {
-		err := s.runGit(c, "fetch", repo)
+		err := s.runGit(c, anonymousToken, "fetch", repo)
 		c.Assert(err, check.ErrorMatches, `.* not found.*`)
 	}
 }
@@ -107,18 +101,7 @@ func (s *IntegrationSuite) SetUpTest(c *check.C) {
 
 	// Clear ARVADOS_API_TOKEN after starting up the server, to
 	// make sure arv-git-httpd doesn't use it.
-	os.Setenv("ARVADOS_API_TOKEN", "")
-
-	_, err = exec.Command("git", "config",
-		"--file", s.tmpWorkdir+"/.git/config",
-		"credential.http://"+s.testServer.Addr+"/.helper",
-		"!cred(){ echo password=$ARVADOS_API_TOKEN; };cred").Output()
-	c.Assert(err, check.Equals, nil)
-	_, err = exec.Command("git", "config",
-		"--file", s.tmpWorkdir+"/.git/config",
-		"credential.http://"+s.testServer.Addr+"/.username",
-		"none").Output()
-	c.Assert(err, check.Equals, nil)
+	os.Setenv("ARVADOS_API_TOKEN", "unused-token-placates-client-library")
 }
 
 func (s *IntegrationSuite) TearDownTest(c *check.C) {
@@ -137,14 +120,14 @@ func (s *IntegrationSuite) TearDownTest(c *check.C) {
 	}
 }
 
-func (s *IntegrationSuite) runGit(c *check.C, gitCmd, repo string, args ...string) error {
+func (s *IntegrationSuite) runGit(c *check.C, token, gitCmd, repo string, args ...string) error {
 	cwd, err := os.Getwd()
 	c.Assert(err, check.Equals, nil)
 	defer os.Chdir(cwd)
 	os.Chdir(s.tmpWorkdir)
 
 	gitargs := append([]string{
-		gitCmd, "http://" + s.testServer.Addr + "/" + repo,
+		gitCmd, "http://none:" + token + "@" + s.testServer.Addr + "/" + repo,
 	}, args...)
 	cmd := exec.Command("git", gitargs...)
 	w, err := cmd.StdinPipe()

commit 08717a74dbc29916e4466119d52863b095e271a4
Author: Tom Clegg <tom at curoverse.com>
Date:   Thu Apr 16 16:44:38 2015 -0400

    5416: Remove second trailing slash in breadcrumbs link.

diff --git a/apps/workbench/app/views/repositories/_repository_breadcrumbs.html.erb b/apps/workbench/app/views/repositories/_repository_breadcrumbs.html.erb
index 736c187..14f9ba7 100644
--- a/apps/workbench/app/views/repositories/_repository_breadcrumbs.html.erb
+++ b/apps/workbench/app/views/repositories/_repository_breadcrumbs.html.erb
@@ -3,7 +3,7 @@
   <%= link_to(@commit, show_repository_commit_path(id: @object.uuid, commit: @commit), title: 'show commit message') %>
 </div>
 <p>
-  <%= link_to(@object.name, show_repository_tree_path(id: @object.uuid, commit: @commit, path: '/'), title: 'show root directory of source tree') %>
+  <%= link_to(@object.name, show_repository_tree_path(id: @object.uuid, commit: @commit, path: ''), title: 'show root directory of source tree') %>
   <% parents = ''
      (@path || '').split('/').each do |pathpart|
      parents = parents + pathpart + '/'

commit de67953481cbedc480822f97cdfe5eb6dffcf0d3
Author: Tom Clegg <tom at curoverse.com>
Date:   Thu Apr 16 16:31:22 2015 -0400

    5416: Disable repository browsing (and skip tests) if git version is suspected unreliable.

diff --git a/apps/workbench/app/controllers/repositories_controller.rb b/apps/workbench/app/controllers/repositories_controller.rb
index c5b3501..89dd96b 100644
--- a/apps/workbench/app/controllers/repositories_controller.rb
+++ b/apps/workbench/app/controllers/repositories_controller.rb
@@ -1,5 +1,8 @@
 class RepositoriesController < ApplicationController
   before_filter :set_share_links, if: -> { defined? @object }
+  if Repository.disable_repository_browsing?
+    before_filter :render_browsing_disabled, only: [:show_tree, :show_blob, :show_commit]
+  end
 
   def index_pane_list
     %w(recent help)
@@ -32,4 +35,10 @@ class RepositoriesController < ApplicationController
   def show_commit
     @commit = params[:commit]
   end
+
+  protected
+
+  def render_browsing_disabled
+    render_not_found ActionController::RoutingError.new("Repository browsing features disabled")
+  end
 end
diff --git a/apps/workbench/app/models/repository.rb b/apps/workbench/app/models/repository.rb
index aa77913..28f8f0c 100644
--- a/apps/workbench/app/models/repository.rb
+++ b/apps/workbench/app/models/repository.rb
@@ -48,6 +48,15 @@ class Repository < ArvadosBase
     subtree
   end
 
+  # git 2.1.4 does not use credential helpers reliably, see #5416
+  def self.disable_repository_browsing?
+    return false if Rails.configuration.use_git2_despite_bug_risk
+    if @buggy_git_version.nil?
+      @buggy_git_version = /git version 2/ =~ `git version`
+    end
+    @buggy_git_version
+  end
+
   protected
 
   # refresh fetches the latest repository content into the local
diff --git a/apps/workbench/app/views/pipeline_instances/_running_component.html.erb b/apps/workbench/app/views/pipeline_instances/_running_component.html.erb
index 2ab8da1..80210c4 100644
--- a/apps/workbench/app/views/pipeline_instances/_running_component.html.erb
+++ b/apps/workbench/app/views/pipeline_instances/_running_component.html.erb
@@ -99,7 +99,8 @@
               <% # link to repo tree/file only if the repo is readable
                  # and the commit is a sha1...
                  repo =
-                 (/^[0-9a-f]{40}$/ =~ current_component[:script_version] and
+                 (not Repository.disable_repository_browsing? and
+                 /^[0-9a-f]{40}$/ =~ current_component[:script_version] and
                  Repository.where(name: current_component[:repository]).first)
 
                  # ...and the api server provides an http:// or https:// url
diff --git a/apps/workbench/config/application.default.yml b/apps/workbench/config/application.default.yml
index 4061ee8..e28f76a 100644
--- a/apps/workbench/config/application.default.yml
+++ b/apps/workbench/config/application.default.yml
@@ -211,3 +211,9 @@ common:
 
   # Enable response payload compression in Arvados API requests.
   include_accept_encoding_header_in_api_requests: true
+
+  # Enable repository browsing even if git2 is installed. Repository
+  # browsing requires credential helpers, which do not work reliably
+  # as of git version 2.1.4. If you have git version 2.* and you want
+  # to use it anyway, change this to true.
+  use_git2_despite_bug_risk: false
diff --git a/apps/workbench/test/controllers/repositories_controller_test.rb b/apps/workbench/test/controllers/repositories_controller_test.rb
index 25bf557..852a602 100644
--- a/apps/workbench/test/controllers/repositories_controller_test.rb
+++ b/apps/workbench/test/controllers/repositories_controller_test.rb
@@ -69,6 +69,7 @@ class RepositoriesControllerTest < ActionController::TestCase
 
   [:active, :spectator].each do |user|
     test "show tree to #{user}" do
+      skip "git2 is unreliable" if Repository.disable_repository_browsing?
       reset_api_fixtures_after_test false
       sha1, _, _ = stub_repo_content
       get :show_tree, {
@@ -85,6 +86,7 @@ class RepositoriesControllerTest < ActionController::TestCase
     end
 
     test "show commit to #{user}" do
+      skip "git2 is unreliable" if Repository.disable_repository_browsing?
       reset_api_fixtures_after_test false
       sha1, commit, _ = stub_repo_content
       get :show_commit, {
@@ -96,6 +98,7 @@ class RepositoriesControllerTest < ActionController::TestCase
     end
 
     test "show blob to #{user}" do
+      skip "git2 is unreliable" if Repository.disable_repository_browsing?
       reset_api_fixtures_after_test false
       sha1, _, filedata = stub_repo_content filename: 'COPYING'
       get :show_blob, {
@@ -110,6 +113,7 @@ class RepositoriesControllerTest < ActionController::TestCase
 
   ['', '/'].each do |path|
     test "show tree with path '#{path}'" do
+      skip "git2 is unreliable" if Repository.disable_repository_browsing?
       reset_api_fixtures_after_test false
       sha1, _, _ = stub_repo_content filename: 'COPYING'
       get :show_tree, {
diff --git a/apps/workbench/test/integration/repositories_browse_test.rb b/apps/workbench/test/integration/repositories_browse_test.rb
index a6a85b5..d936877 100644
--- a/apps/workbench/test/integration/repositories_browse_test.rb
+++ b/apps/workbench/test/integration/repositories_browse_test.rb
@@ -13,6 +13,7 @@ class RepositoriesTest < ActionDispatch::IntegrationTest
   end
 
   test "browse repository from jobs#show" do
+    skip "git2 is unreliable" if Repository.disable_repository_browsing?
     sha1 = api_fixture('jobs')['running']['script_version']
     _, fakecommit, fakefile =
       stub_repo_content sha1: sha1, filename: 'crunch_scripts/hash'
@@ -36,6 +37,7 @@ class RepositoriesTest < ActionDispatch::IntegrationTest
   end
 
   test "browse using arv-git-http" do
+    skip "git2 is unreliable" if Repository.disable_repository_browsing?
     repo = api_fixture('repositories')['foo']
     portfile =
       File.expand_path('../../../../../tmp/arv-git-httpd-ssl.port', __FILE__)

commit e24125041ad492b45c97feffb33a037c5adda734
Author: Tom Clegg <tom at curoverse.com>
Date:   Tue Apr 14 16:56:04 2015 -0400

    5416: Add read-only clone_urls attribute to Repository resources, deprecate push_url and fetch_url, tidy up config settings.

diff --git a/apps/workbench/app/models/repository.rb b/apps/workbench/app/models/repository.rb
index 48c7f9e..aa77913 100644
--- a/apps/workbench/app/models/repository.rb
+++ b/apps/workbench/app/models/repository.rb
@@ -59,12 +59,10 @@ class Repository < ArvadosBase
     @fresh = true
   end
 
-  # http_fetch_url returns an http:// or https:// fetch-url which can
-  # accept arvados API token authentication. The API server currently
-  # advertises SSH fetch-urls, which work for users with SSH keys but
-  # are useless for fetching repository content into workbench itself.
+  # http_fetch_url returns the first http:// or https:// url (if any)
+  # in the api response's clone_urls attribute.
   def http_fetch_url
-    "https://git.#{uuid[0,5]}.arvadosapi.com/#{name}.git"
+    clone_urls.andand.select { |u| /^http/ =~ u }.first
   end
 
   # run_git sets up the ARVADOS_API_TOKEN environment variable,
diff --git a/apps/workbench/app/views/pipeline_instances/_running_component.html.erb b/apps/workbench/app/views/pipeline_instances/_running_component.html.erb
index cc3b4c8..2ab8da1 100644
--- a/apps/workbench/app/views/pipeline_instances/_running_component.html.erb
+++ b/apps/workbench/app/views/pipeline_instances/_running_component.html.erb
@@ -97,10 +97,13 @@
           <div class="col-md-6">
             <table>
               <% # link to repo tree/file only if the repo is readable
-                 # and the commit is a sha1
+                 # and the commit is a sha1...
                  repo =
                  (/^[0-9a-f]{40}$/ =~ current_component[:script_version] and
                  Repository.where(name: current_component[:repository]).first)
+
+                 # ...and the api server provides an http:// or https:// url
+                 repo = nil unless repo.andand.http_fetch_url
                  %>
               <% [:script, :repository, :script_version, :supplied_script_version, :nondeterministic].each do |k| %>
                 <tr>
diff --git a/doc/api/schema/Repository.html.textile.liquid b/doc/api/schema/Repository.html.textile.liquid
index 27cc711..0f9b25e 100644
--- a/doc/api/schema/Repository.html.textile.liquid
+++ b/doc/api/schema/Repository.html.textile.liquid
@@ -19,5 +19,7 @@ Each Repository has, in addition to the usual "attributes of Arvados resources":
 table(table table-bordered table-condensed).
 |_. Attribute|_. Type|_. Description|_. Example|
 |name|string|The name of the repository on disk.  Repository names must begin with a letter and contain only alphanumerics.  Unless the repository is owned by the system user, the name must begin with the owner's username, then be separated from the base repository name with @/@.  You may not create a repository that is owned by a user without a username.|@username/project1@|
-|fetch_url|string|The git remote's fetch URL for the repository.  Read-only.||
-|push_url|string|The git remote's push URL for the repository.  Read-only.||
+|clone_urls|array|URLs from which the repository can be cloned. Read-only.|@["git at git.zzzzz.arvadosapi.com:foo/bar.git",
+ "https://git.zzzzz.arvadosapi.com/foo/bar.git"]@|
+|fetch_url|string|URL suggested as a fetch-url in git config. Deprecated. Read-only.||
+|push_url|string|URL suggested as a push-url in git config. Deprecated. Read-only.||
diff --git a/docker/api/application.yml.in b/docker/api/application.yml.in
index 3cfb5a9..627e775 100644
--- a/docker/api/application.yml.in
+++ b/docker/api/application.yml.in
@@ -20,7 +20,11 @@ development:
 
 production:
   host: api.dev.arvados
-  git_host: api.dev.arvados
+
+  git_repo_ssh_base: "git at api.dev.arvados:"
+
+  # Docker setup doesn't include arv-git-httpd yet.
+  git_repo_https_base: false
 
   # At minimum, you need a nice long randomly generated secret_token here.
   # Use a long string of alphanumeric characters (at least 36).
diff --git a/services/api/app/controllers/arvados/v1/schema_controller.rb b/services/api/app/controllers/arvados/v1/schema_controller.rb
index 20e9690..dcc9c63 100644
--- a/services/api/app/controllers/arvados/v1/schema_controller.rb
+++ b/services/api/app/controllers/arvados/v1/schema_controller.rb
@@ -28,7 +28,6 @@ class Arvados::V1::SchemaController < ApplicationController
         description: "The API to interact with Arvados.",
         documentationLink: "http://doc.arvados.org/api/index.html",
         defaultCollectionReplication: Rails.configuration.default_collection_replication,
-        gitHttpBase: Rails.configuration.git_http_base,
         protocol: "rest",
         baseUrl: root_url + "arvados/v1/",
         basePath: "/arvados/v1/",
diff --git a/services/api/app/models/repository.rb b/services/api/app/models/repository.rb
index e83ac41..f361a49 100644
--- a/services/api/app/models/repository.rb
+++ b/services/api/app/models/repository.rb
@@ -13,23 +13,27 @@ class Repository < ArvadosModel
     t.add :name
     t.add :fetch_url
     t.add :push_url
+    t.add :clone_urls
   end
 
   def self.attributes_required_columns
-    super.merge({"push_url" => ["name"], "fetch_url" => ["name"]})
+    super.merge("clone_urls" => ["name"],
+                "fetch_url" => ["name"],
+                "push_url" => ["name"])
   end
 
+  # Deprecated. Use clone_urls instead.
   def push_url
-    prefix = new_record? ? Rails.configuration.uuid_prefix : uuid[0,5]
-    if prefix == Rails.configuration.uuid_prefix
-      host = Rails.configuration.git_host
-    end
-    host ||= "git.%s.arvadosapi.com" % prefix
-    "git@%s:%s.git" % [host, name]
+    ssh_clone_url
   end
 
+  # Deprecated. Use clone_urls instead.
   def fetch_url
-    push_url
+    ssh_clone_url
+  end
+
+  def clone_urls
+    [ssh_clone_url, https_clone_url].compact
   end
 
   def server_path
@@ -88,4 +92,24 @@ class Repository < ArvadosModel
       false
     end
   end
+
+  def ssh_clone_url
+    _clone_url :git_repo_ssh_base, 'git at git.%s.arvadosapi.com:'
+  end
+
+  def https_clone_url
+    _clone_url :git_repo_https_base, 'https://git.%s.arvadosapi.com/'
+  end
+
+  def _clone_url config_var, default_base_fmt
+    configured_base = Rails.configuration.send config_var
+    return nil if configured_base == false
+    prefix = new_record? ? Rails.configuration.uuid_prefix : uuid[0,5]
+    if prefix == Rails.configuration.uuid_prefix and configured_base != true
+      base = configured_base
+    else
+      base = default_base_fmt % prefix
+    end
+    '%s%s.git' % [base, name]
+  end
 end
diff --git a/services/api/config/application.default.yml b/services/api/config/application.default.yml
index b45ed65..0ea685a 100644
--- a/services/api/config/application.default.yml
+++ b/services/api/config/application.default.yml
@@ -59,10 +59,19 @@ common:
   # logic for deciding on a hostname.
   host: false
 
-  # If not false, this is the hostname that will be used to generate
-  # fetch_url and push_url for locally hosted git repositories.  By
-  # default, this is git.(uuid_prefix).arvadosapi.com
-  git_host: false
+  # Base part of SSH git clone url given with repository resources. If
+  # true, the default "git at git.(uuid_prefix).arvadosapi.com:" is
+  # used. If false, SSH clone URLs are not advertised. Include a
+  # trailing ":" or "/" if needed: it will not be added automatically.
+  git_repo_ssh_base: true
+
+  # Base part of HTTPS git clone urls given with repository
+  # resources. This is expected to be an arv-git-httpd service which
+  # accepts API tokens as HTTP-auth passwords. If true, the default
+  # "https://git.(uuid_prefix).arvadosapi.com/" is used. If false,
+  # HTTPS clone URLs are not advertised. Include a trailing ":" or "/"
+  # if needed: it will not be added automatically.
+  git_repo_https_base: true
 
   # If this is not false, HTML requests at the API server's root URL
   # are redirected to this location, and it is provided in the text of
@@ -76,11 +85,6 @@ common:
   # {git_repositories_dir}/arvados/.git
   git_repositories_dir: /var/lib/arvados/git
 
-  # If an arv-git-httpd service is running, advertise it in the
-  # discovery document by adding its public URI base here. Example:
-  # https://git.xxxxx.arvadosapi.com
-  git_http_base: false
-
   # This is a (bare) repository that stores commits used in jobs.  When a job
   # runs, the source commits are first fetched into this repository, then this
   # repository is used to deploy to compute nodes.  This should NOT be a
@@ -236,7 +240,8 @@ common:
   # should be at least 50 characters.
   secret_token: ~
 
-  # email address to which mail should be sent when the user creates profile for the first time
+  # Email address to notify whenever a user creates a profile for the
+  # first time
   user_profile_notification_address: false
 
   default_openid_prefix: https://www.google.com/accounts/o8/id
diff --git a/services/api/test/functional/arvados/v1/repositories_controller_test.rb b/services/api/test/functional/arvados/v1/repositories_controller_test.rb
index fe5bb1c..7f4ed8e 100644
--- a/services/api/test/functional/arvados/v1/repositories_controller_test.rb
+++ b/services/api/test/functional/arvados/v1/repositories_controller_test.rb
@@ -97,26 +97,45 @@ class Arvados::V1::RepositoriesControllerTest < ActionController::TestCase
   end
 
   [
-    {config: "example.com", host: "example.com"},
-    {config: false, host: "git.zzzzz.arvadosapi.com"}
-  ].each do |set_git_host|
-    test "setting git_host to #{set_git_host[:host]} changes fetch/push_url to #{set_git_host[:config]}" do
-      Rails.configuration.git_host = set_git_host[:config]
+    {cfg: :git_repo_ssh_base, cfgval: "git at example.com:", match: %r"^git at example.com:/"},
+    {cfg: :git_repo_ssh_base, cfgval: true, match: %r"^git at git.zzzzz.arvadosapi.com:/"},
+    {cfg: :git_repo_ssh_base, cfgval: false, refute: /^git@/ },
+    {cfg: :git_repo_https_base, cfgval: "https://example.com/", match: %r"https://example.com/"},
+    {cfg: :git_repo_https_base, cfgval: true, match: %r"^https://git.zzzzz.arvadosapi.com/"},
+    {cfg: :git_repo_https_base, cfgval: false, refute: /^http/ },
+  ].each do |expect|
+    test "set #{expect[:cfg]} to #{expect[:cfgval]}" do
+      Rails.configuration.send expect[:cfg].to_s+"=", expect[:cfgval]
       authorize_with :active
-      get(:index)
+      get :index
       assert_response :success
-      assert_includes(json_response["items"].map { |r| r["fetch_url"] },
-                      "git@#{set_git_host[:host]}:active/foo.git")
-      assert_includes(json_response["items"].map { |r| r["push_url"] },
-                      "git@#{set_git_host[:host]}:active/foo.git")
+      json_response['items'].each do |r|
+        if expect[:refute]
+          r['clone_urls'].each do |u|
+            refute_match expect[:refute], u
+          end
+        else
+          assert r['clone_urls'].any? do |u|
+            expect[:prefix].match u
+          end
+        end
+      end
     end
   end
 
-  test "can select push_url in index" do
+  test "select push_url in index" do
     authorize_with :active
     get(:index, {select: ["uuid", "push_url"]})
     assert_response :success
     assert_includes(json_response["items"].map { |r| r["push_url"] },
                     "git at git.zzzzz.arvadosapi.com:active/foo.git")
   end
+
+  test "select clone_urls in index" do
+    authorize_with :active
+    get(:index, {select: ["uuid", "clone_urls"]})
+    assert_response :success
+    assert_includes(json_response["items"].map { |r| r["clone_urls"] }.flatten,
+                    "git at git.zzzzz.arvadosapi.com:active/foo.git")
+  end
 end

commit 3c36af3d62af5e4ee98aaafab80858182fb5cab8
Author: Tom Clegg <tom at curoverse.com>
Date:   Wed Apr 8 11:35:56 2015 -0400

    5416: Do not override git urls for remote hosted repos.

diff --git a/services/api/app/models/repository.rb b/services/api/app/models/repository.rb
index d2da6ea..e83ac41 100644
--- a/services/api/app/models/repository.rb
+++ b/services/api/app/models/repository.rb
@@ -20,11 +20,12 @@ class Repository < ArvadosModel
   end
 
   def push_url
-    if Rails.configuration.git_host
-      "git@%s:%s.git" % [Rails.configuration.git_host, name]
-    else
-      "git at git.%s.arvadosapi.com:%s.git" % [Rails.configuration.uuid_prefix, name]
+    prefix = new_record? ? Rails.configuration.uuid_prefix : uuid[0,5]
+    if prefix == Rails.configuration.uuid_prefix
+      host = Rails.configuration.git_host
     end
+    host ||= "git.%s.arvadosapi.com" % prefix
+    "git@%s:%s.git" % [host, name]
   end
 
   def fetch_url
diff --git a/services/api/config/application.default.yml b/services/api/config/application.default.yml
index 0d936b7..b45ed65 100644
--- a/services/api/config/application.default.yml
+++ b/services/api/config/application.default.yml
@@ -59,9 +59,9 @@ common:
   # logic for deciding on a hostname.
   host: false
 
-  # If not false, this is the hostname that will be used to generate fetch_url
-  # and push_url for git repositories.  By default, this will be
-  # git.(uuid_prefix).arvadosapi.com
+  # If not false, this is the hostname that will be used to generate
+  # fetch_url and push_url for locally hosted git repositories.  By
+  # default, this is git.(uuid_prefix).arvadosapi.com
   git_host: false
 
   # If this is not false, HTML requests at the API server's root URL
diff --git a/services/api/test/unit/repository_test.rb b/services/api/test/unit/repository_test.rb
index 5acef1b..288e118 100644
--- a/services/api/test/unit/repository_test.rb
+++ b/services/api/test/unit/repository_test.rb
@@ -108,6 +108,7 @@ class RepositoryTest < ActiveSupport::TestCase
 
   test "fetch_url" do
     repo = new_repo(:active, name: "active/fetchtest")
+    repo.save
     assert_equal(default_git_url("fetchtest", "active"), repo.fetch_url)
   end
 
@@ -115,11 +116,13 @@ class RepositoryTest < ActiveSupport::TestCase
     set_user_from_auth :admin
     repo = Repository.new(owner_uuid: users(:system_user).uuid,
                           name: "fetchtest")
+    repo.save
     assert_equal(default_git_url("fetchtest"), repo.fetch_url)
   end
 
   test "push_url" do
     repo = new_repo(:active, name: "active/pushtest")
+    repo.save
     assert_equal(default_git_url("pushtest", "active"), repo.push_url)
   end
 
@@ -127,6 +130,7 @@ class RepositoryTest < ActiveSupport::TestCase
     set_user_from_auth :admin
     repo = Repository.new(owner_uuid: users(:system_user).uuid,
                           name: "pushtest")
+    repo.save
     assert_equal(default_git_url("pushtest"), repo.push_url)
   end
 

commit 86860e7d65589bc9d93df5b514baee3fc5a5103a
Author: Tom Clegg <tom at curoverse.com>
Date:   Wed Apr 8 10:22:05 2015 -0400

    5416: Run arv-git-httpd and nginx ssl proxy in test suite.

diff --git a/apps/workbench/test/integration/jobs_test.rb b/apps/workbench/test/integration/jobs_test.rb
index ce14ec6..2cae500 100644
--- a/apps/workbench/test/integration/jobs_test.rb
+++ b/apps/workbench/test/integration/jobs_test.rb
@@ -98,7 +98,7 @@ class JobsTest < ActionDispatch::IntegrationTest
       # that the error message says something appropriate for that
       # situation.
       if expect_options && use_latest
-        assert_text "Script version #{job['supplied_script_version']} does not resolve to a commit"
+        assert_text "077ba2ad3ea24a929091a9e6ce545c93199b8e57"
       else
         assert_text "Script version #{job['script_version']} does not resolve to a commit"
       end
diff --git a/apps/workbench/test/integration/repositories_browse_test.rb b/apps/workbench/test/integration/repositories_browse_test.rb
index 147bf46..a6a85b5 100644
--- a/apps/workbench/test/integration/repositories_browse_test.rb
+++ b/apps/workbench/test/integration/repositories_browse_test.rb
@@ -34,4 +34,20 @@ class RepositoriesTest < ActionDispatch::IntegrationTest
     click_on sha1
     assert_text fakecommit
   end
+
+  test "browse using arv-git-http" do
+    repo = api_fixture('repositories')['foo']
+    portfile =
+      File.expand_path('../../../../../tmp/arv-git-httpd-ssl.port', __FILE__)
+    gitsslport = File.read(portfile)
+    Repository.any_instance.
+      stubs(:http_fetch_url).
+      returns "https://localhost:#{gitsslport}/#{repo['name']}.git"
+    commit_sha1 = '1de84a854e2b440dc53bf42f8548afa4c17da332'
+    visit page_with_token('active', "/repositories/#{repo['uuid']}/commit/#{commit_sha1}")
+    assert_text "Date:   Tue Mar 18 15:55:28 2014 -0400"
+    visit page_with_token('active', "/repositories/#{repo['uuid']}/tree/#{commit_sha1}")
+    assert_selector "tbody td a", "foo"
+    assert_text "12 bytes"
+  end
 end
diff --git a/apps/workbench/test/test_helper.rb b/apps/workbench/test/test_helper.rb
index 630a64f..f335722 100644
--- a/apps/workbench/test/test_helper.rb
+++ b/apps/workbench/test/test_helper.rb
@@ -137,7 +137,7 @@ class ApiServerForTests
   @main_process_pid = $$
   @@server_is_running = false
 
-  def check_call *args
+  def check_output *args
     output = nil
     Bundler.with_clean_env do
       output = IO.popen *args do |io|
@@ -153,7 +153,12 @@ class ApiServerForTests
   def run_test_server
     env_script = nil
     Dir.chdir PYTHON_TESTS_DIR do
-      env_script = check_call %w(python ./run_test_server.py start --auth admin)
+      # These are no-ops if we're running within run-tests.sh (except
+      # that we do get a useful env_script back from "start", even
+      # though it doesn't need to start up a new server).
+      env_script = check_output %w(python ./run_test_server.py start --auth admin)
+      check_output %w(python ./run_test_server.py start_arv-git-httpd)
+      check_output %w(python ./run_test_server.py start_nginx)
     end
     test_env = {}
     env_script.each_line do |line|
@@ -169,8 +174,10 @@ class ApiServerForTests
 
   def stop_test_server
     Dir.chdir PYTHON_TESTS_DIR do
-      # This is a no-op if we're running within run-tests.sh
-      check_call %w(python ./run_test_server.py stop)
+      # These are no-ops if we're running within run-tests.sh
+      check_output %w(python ./run_test_server.py stop_nginx)
+      check_output %w(python ./run_test_server.py stop_arv-git-httpd)
+      check_output %w(python ./run_test_server.py stop)
     end
     @@server_is_running = false
   end
@@ -196,7 +203,7 @@ class ApiServerForTests
 
   def run_rake_task task_name, arg_string
     Dir.chdir ARV_API_SERVER_DIR do
-      check_call ['bundle', 'exec', 'rake', "#{task_name}[#{arg_string}]"]
+      check_output ['bundle', 'exec', 'rake', "#{task_name}[#{arg_string}]"]
     end
   end
 end
diff --git a/sdk/python/tests/nginx.conf b/sdk/python/tests/nginx.conf
new file mode 100644
index 0000000..6196605
--- /dev/null
+++ b/sdk/python/tests/nginx.conf
@@ -0,0 +1,31 @@
+daemon off;
+error_log stderr info;          # Yes, must be specified here _and_ cmdline
+events {
+}
+http {
+  access_log /dev/stderr combined;
+  upstream arv-git-http {
+    server localhost:{{GITPORT}};
+  }
+  server {
+    listen *:{{GITSSLPORT}} ssl default_server;
+    server_name _;
+    ssl_certificate {{SSLCERT}};
+    ssl_certificate_key {{SSLKEY}};
+    location  / {
+      proxy_pass http://arv-git-http;
+    }
+  }
+  upstream keepproxy {
+    server localhost:{{KEEPPROXYPORT}};
+  }
+  server {
+    listen *:{{KEEPPROXYSSLPORT}} ssl default_server;
+    server_name _;
+    ssl_certificate {{SSLCERT}};
+    ssl_certificate_key {{SSLKEY}};
+    location  / {
+      proxy_pass http://keepproxy;
+    }
+  }
+}
diff --git a/sdk/python/tests/run_test_server.py b/sdk/python/tests/run_test_server.py
index 51fe43f..c54dad0 100644
--- a/sdk/python/tests/run_test_server.py
+++ b/sdk/python/tests/run_test_server.py
@@ -1,5 +1,6 @@
 #!/usr/bin/env python
 
+from __future__ import print_function
 import argparse
 import atexit
 import httplib2
@@ -41,6 +42,7 @@ if not os.path.exists(TEST_TMPDIR):
     os.mkdir(TEST_TMPDIR)
 
 my_api_host = None
+_cached_config = {}
 
 def find_server_pid(PID_PATH, wait=10):
     now = time.time()
@@ -178,6 +180,13 @@ def run(leave_running_atexit=False):
             '-subj', '/CN=0.0.0.0'],
         stdout=sys.stderr)
 
+    # Install the git repository fixtures.
+    gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
+    gittarball = os.path.join(SERVICES_SRC_DIR, 'api', 'test', 'test.git.tar')
+    if not os.path.isdir(gitdir):
+        os.makedirs(gitdir)
+    subprocess.check_output(['tar', '-xC', gitdir, '-f', gittarball])
+
     port = find_available_port()
     env = os.environ.copy()
     env['RAILS_ENV'] = 'test'
@@ -258,13 +267,14 @@ def _start_keep(n, keep_args):
     keep_cmd = ["keepstore",
                 "-volume={}".format(keep0),
                 "-listen=:{}".format(port),
-                "-pid={}".format("{}/keep{}.pid".format(TEST_TMPDIR, n))]
+                "-pid="+_pidfile('keep{}'.format(n))]
 
     for arg, val in keep_args.iteritems():
         keep_cmd.append("{}={}".format(arg, val))
 
-    kp0 = subprocess.Popen(keep_cmd)
-    with open("{}/keep{}.pid".format(TEST_TMPDIR, n), 'w') as f:
+    kp0 = subprocess.Popen(
+        keep_cmd, stdin=open('/dev/null'), stdout=sys.stderr)
+    with open(_pidfile('keep{}'.format(n)), 'w') as f:
         f.write(str(kp0.pid))
 
     with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'w') as f:
@@ -307,7 +317,7 @@ def run_keep(blob_signing_key=None, enforce_permissions=False):
         }).execute()
 
 def _stop_keep(n):
-    kill_server_pid("{}/keep{}.pid".format(TEST_TMPDIR, n), 0)
+    kill_server_pid(_pidfile('keep{}'.format(n)), 0)
     if os.path.exists("{}/keep{}.volume".format(TEST_TMPDIR, n)):
         with open("{}/keep{}.volume".format(TEST_TMPDIR, n), 'r') as r:
             shutil.rmtree(r.read(), True)
@@ -320,6 +330,8 @@ def stop_keep():
     _stop_keep(1)
 
 def run_keep_proxy():
+    if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
+        return
     stop_keep_proxy()
 
     admin_token = auth_token('admin')
@@ -328,9 +340,9 @@ def run_keep_proxy():
     env['ARVADOS_API_TOKEN'] = admin_token
     kp = subprocess.Popen(
         ['keepproxy',
-         '-pid={}/keepproxy.pid'.format(TEST_TMPDIR),
+         '-pid='+_pidfile('keepproxy'),
          '-listen=:{}'.format(port)],
-        env=env)
+        env=env, stdin=open('/dev/null'), stdout=sys.stderr)
 
     api = arvados.api(
         version='v1',
@@ -347,9 +359,100 @@ def run_keep_proxy():
         'service_ssl_flag': False,
     }}).execute()
     os.environ["ARVADOS_KEEP_PROXY"] = "http://localhost:{}".format(port)
+    _setport('keepproxy', port)
 
 def stop_keep_proxy():
-    kill_server_pid(os.path.join(TEST_TMPDIR, "keepproxy.pid"), 0)
+    if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
+        return
+    kill_server_pid(_pidfile('keepproxy'), wait=0)
+
+def run_arv_git_httpd():
+    if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
+        return
+    stop_arv_git_httpd()
+
+    gitdir = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'git')
+    gitport = find_available_port()
+    env = os.environ.copy()
+    del env['ARVADOS_API_TOKEN']
+    agh = subprocess.Popen(
+        ['arv-git-httpd',
+         '-repo-root='+gitdir+'/test',
+         '-address=:'+str(gitport)],
+        env=env, stdin=open('/dev/null'), stdout=sys.stderr)
+    with open(_pidfile('arv-git-httpd'), 'w') as f:
+        f.write(str(agh.pid))
+    _setport('arv-git-httpd', gitport)
+
+def stop_arv_git_httpd():
+    if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
+        return
+    kill_server_pid(_pidfile('arv-git-httpd'), wait=0)
+
+def run_nginx():
+    if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
+        return
+    nginxconf = {}
+    nginxconf['KEEPPROXYPORT'] = _getport('keepproxy')
+    nginxconf['KEEPPROXYSSLPORT'] = find_available_port()
+    nginxconf['GITPORT'] = _getport('arv-git-httpd')
+    nginxconf['GITSSLPORT'] = find_available_port()
+    nginxconf['SSLCERT'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.pem')
+    nginxconf['SSLKEY'] = os.path.join(SERVICES_SRC_DIR, 'api', 'tmp', 'self-signed.key')
+
+    conftemplatefile = os.path.join(MY_DIRNAME, 'nginx.conf')
+    conffile = os.path.join(TEST_TMPDIR, 'nginx.conf')
+    with open(conffile, 'w') as f:
+        f.write(re.sub(
+            r'{{([A-Z]+)}}',
+            lambda match: str(nginxconf.get(match.group(1))),
+            open(conftemplatefile).read()))
+
+    env = os.environ.copy()
+    env['PATH'] = env['PATH']+':/sbin:/usr/sbin:/usr/local/sbin'
+    nginx = subprocess.Popen(
+        ['nginx',
+         '-g', 'error_log stderr info;',
+         '-g', 'pid '+_pidfile('nginx')+';',
+         '-c', conffile],
+        env=env, stdin=open('/dev/null'), stdout=sys.stderr)
+    _setport('keepproxy-ssl', nginxconf['KEEPPROXYSSLPORT'])
+    _setport('arv-git-httpd-ssl', nginxconf['GITSSLPORT'])
+
+def stop_nginx():
+    if 'ARVADOS_TEST_PROXY_SERVICES' in os.environ:
+        return
+    kill_server_pid(_pidfile('nginx'), wait=0)
+
+def _pidfile(program):
+    return os.path.join(TEST_TMPDIR, program + '.pid')
+
+def _portfile(program):
+    return os.path.join(TEST_TMPDIR, program + '.port')
+
+def _setport(program, port):
+    with open(_portfile(program), 'w') as f:
+        f.write(str(port))
+
+# Returns 9 if program is not up.
+def _getport(program):
+    try:
+        return int(open(_portfile(program)).read())
+    except IOError:
+        return 9
+
+def _apiconfig(key):
+    if _cached_config:
+        return _cached_config[key]
+    def _load(f):
+        return yaml.load(os.path.join(SERVICES_SRC_DIR, 'api', 'config', f))
+    cdefault = _load('application.default.yml')
+    csite = _load('application.yml')
+    _cached_config = {}
+    for section in [cdefault.get('common',{}), cdefault.get('test',{}),
+                    csite.get('common',{}), csite.get('test',{})]:
+        _cached_config.update(section)
+    return _cached_config[key]
 
 def fixture(fix):
     '''load a fixture yaml file'''
@@ -431,14 +534,21 @@ class TestCaseWithServers(unittest.TestCase):
 
 
 if __name__ == "__main__":
-    actions = ['start', 'stop',
-               'start_keep', 'stop_keep',
-               'start_keep_proxy', 'stop_keep_proxy']
+    actions = [
+        'start', 'stop',
+        'start_keep', 'stop_keep',
+        'start_keep_proxy', 'stop_keep_proxy',
+        'start_arv-git-httpd', 'stop_arv-git-httpd',
+        'start_nginx', 'stop_nginx',
+    ]
     parser = argparse.ArgumentParser()
     parser.add_argument('action', type=str, help="one of {}".format(actions))
     parser.add_argument('--auth', type=str, metavar='FIXTURE_NAME', help='Print authorization info for given api_client_authorizations fixture')
     args = parser.parse_args()
 
+    if args.action not in actions:
+        print("Unrecognized action '{}'. Actions are: {}.".format(args.action, actions), file=sys.stderr)
+        sys.exit(1)
     if args.action == 'start':
         stop(force=('ARVADOS_TEST_API_HOST' not in os.environ))
         run(leave_running_atexit=True)
@@ -460,5 +570,13 @@ if __name__ == "__main__":
         run_keep_proxy()
     elif args.action == 'stop_keep_proxy':
         stop_keep_proxy()
+    elif args.action == 'start_arv-git-httpd':
+        run_arv_git_httpd()
+    elif args.action == 'stop_arv-git-httpd':
+        stop_arv_git_httpd()
+    elif args.action == 'start_nginx':
+        run_nginx()
+    elif args.action == 'stop_nginx':
+        stop_nginx()
     else:
-        print("Unrecognized action '{}'. Actions are: {}.".format(args.action, actions))
+        raise Exception("action recognized but not implemented!?")
diff --git a/services/api/test/helpers/git_test_helper.rb b/services/api/test/helpers/git_test_helper.rb
index 9abdc4f..6fce321 100644
--- a/services/api/test/helpers/git_test_helper.rb
+++ b/services/api/test/helpers/git_test_helper.rb
@@ -14,9 +14,15 @@ require 'tmpdir'
 module GitTestHelper
   def self.included base
     base.setup do
-      @tmpdir = Dir.mktmpdir()
-      system("tar", "-xC", @tmpdir, "-f", "test/test.git.tar")
+      # Extract the test repository data into the default test
+      # environment's Rails.configuration.git_repositories_dir. (We
+      # don't use that config setting here, though: it doesn't seem
+      # worth the risk of stepping on a real git repo root.)
+      @tmpdir = Rails.root.join 'tmp', 'git'
+      FileUtils.mkdir_p @tmpdir
+      system("tar", "-xC", @tmpdir.to_s, "-f", "test/test.git.tar")
       Rails.configuration.git_repositories_dir = "#{@tmpdir}/test"
+
       intdir = Rails.configuration.git_internal_dir
       if not File.exist? intdir
         FileUtils.mkdir_p intdir

commit 9136a1b1314084e149f86ceec16d1482ccf5d8af
Author: Tom Clegg <tom at curoverse.com>
Date:   Wed Apr 1 18:34:38 2015 -0400

    5416: Browse git repository contents in workbench.

diff --git a/apps/workbench/app/controllers/repositories_controller.rb b/apps/workbench/app/controllers/repositories_controller.rb
index d32c92a..c5b3501 100644
--- a/apps/workbench/app/controllers/repositories_controller.rb
+++ b/apps/workbench/app/controllers/repositories_controller.rb
@@ -16,4 +16,20 @@ class RepositoriesController < ApplicationController
     panes.delete('Attributes') if !current_user.is_admin
     panes
   end
+
+  def show_tree
+    @commit = params[:commit]
+    @path = params[:path] || ''
+    @subtree = @object.ls_subtree @commit, @path.chomp('/')
+  end
+
+  def show_blob
+    @commit = params[:commit]
+    @path = params[:path]
+    @blobdata = @object.cat_file @commit, @path
+  end
+
+  def show_commit
+    @commit = params[:commit]
+  end
 end
diff --git a/apps/workbench/app/models/repository.rb b/apps/workbench/app/models/repository.rb
index b062dda..48c7f9e 100644
--- a/apps/workbench/app/models/repository.rb
+++ b/apps/workbench/app/models/repository.rb
@@ -12,4 +12,105 @@ class Repository < ArvadosBase
       []
     end
   end
+
+  def show commit_sha1
+    refresh
+    run_git 'show', commit_sha1
+  end
+
+  def cat_file commit_sha1, path
+    refresh
+    run_git 'cat-file', 'blob', commit_sha1 + ':' + path
+  end
+
+  def ls_tree_lr commit_sha1
+    refresh
+    run_git 'ls-tree', '-l', '-r', commit_sha1
+  end
+
+  # subtree returns a list of files under the given path at the
+  # specified commit. Results are returned as an array of file nodes,
+  # where each file node is an array [file mode, blob sha1, file size
+  # in bytes, path relative to the given directory]. If the path is
+  # not found, [] is returned.
+  def ls_subtree commit, path
+    path = path.chomp '/'
+    subtree = []
+    ls_tree_lr(commit).each_line do |line|
+      mode, type, sha1, size, filepath = line.split
+      next if type != 'blob'
+      if filepath[0,path.length] == path and
+          (path == '' or filepath[path.length] == '/')
+        subtree << [mode.to_i(8), sha1, size.to_i,
+                    filepath[path.length,filepath.length]]
+      end
+    end
+    subtree
+  end
+
+  protected
+
+  # refresh fetches the latest repository content into the local
+  # cache. It is a no-op if it has already been run on this object:
+  # this (pretty much) avoids doing more than one remote git operation
+  # per Workbench request.
+  def refresh
+    run_git 'fetch', http_fetch_url, '+*:*' unless @fresh
+    @fresh = true
+  end
+
+  # http_fetch_url returns an http:// or https:// fetch-url which can
+  # accept arvados API token authentication. The API server currently
+  # advertises SSH fetch-urls, which work for users with SSH keys but
+  # are useless for fetching repository content into workbench itself.
+  def http_fetch_url
+    "https://git.#{uuid[0,5]}.arvadosapi.com/#{name}.git"
+  end
+
+  # run_git sets up the ARVADOS_API_TOKEN environment variable,
+  # creates a local git directory for this repository if necessary,
+  # executes "git --git-dir localgitdir {args to run_git}", and
+  # returns the output. It raises GitCommandError if git exits
+  # non-zero.
+  def run_git *gitcmd
+    if not @workdir
+      workdir = File.expand_path uuid+'.git', Rails.configuration.repository_cache
+      if not File.exists? workdir
+        FileUtils.mkdir_p Rails.configuration.repository_cache
+        [['git', 'init', '--bare', workdir],
+        ].each do |cmd|
+          system *cmd
+          raise GitCommandError.new($?.to_s) unless $?.exitstatus == 0
+        end
+      end
+      @workdir = workdir
+    end
+    [['git', '--git-dir', @workdir, 'config', '--local',
+      "credential.#{http_fetch_url}.username", 'none'],
+     ['git', '--git-dir', @workdir, 'config', '--local',
+      "credential.#{http_fetch_url}.helper",
+      '!token(){ echo password="$ARVADOS_API_TOKEN"; }; token'],
+     ['git', '--git-dir', @workdir, 'config', '--local',
+           'http.sslVerify',
+           Rails.configuration.arvados_insecure_https ? 'false' : 'true'],
+     ].each do |cmd|
+      system *cmd
+      raise GitCommandError.new($?.to_s) unless $?.exitstatus == 0
+    end
+    env = {}.
+      merge(ENV).
+      merge('ARVADOS_API_TOKEN' => Thread.current[:arvados_api_token])
+    cmd = ['git', '--git-dir', @workdir] + gitcmd
+    io = IO.popen(env, cmd, err: [:child, :out])
+    output = io.read
+    io.close
+    # "If [io] is opened by IO.popen, close sets $?." --ruby 2.2.1 docs
+    unless $?.exitstatus == 0
+      raise GitCommandError.new("`git #{gitcmd.join ' '}` #{$?}: #{output}")
+    end
+    output
+  end
+
+  class GitCommandError < StandardError
+  end
 end
diff --git a/apps/workbench/app/views/pipeline_instances/_running_component.html.erb b/apps/workbench/app/views/pipeline_instances/_running_component.html.erb
index 1a9cb35..cc3b4c8 100644
--- a/apps/workbench/app/views/pipeline_instances/_running_component.html.erb
+++ b/apps/workbench/app/views/pipeline_instances/_running_component.html.erb
@@ -96,6 +96,12 @@
         <div class="row">
           <div class="col-md-6">
             <table>
+              <% # link to repo tree/file only if the repo is readable
+                 # and the commit is a sha1
+                 repo =
+                 (/^[0-9a-f]{40}$/ =~ current_component[:script_version] and
+                 Repository.where(name: current_component[:repository]).first)
+                 %>
               <% [:script, :repository, :script_version, :supplied_script_version, :nondeterministic].each do |k| %>
                 <tr>
                   <td style="padding-right: 1em">
@@ -104,6 +110,12 @@
                   <td>
                     <% if current_component[k].nil? %>
                       (none)
+                    <% elsif repo and k == :repository %>
+                      <%= link_to current_component[k], show_repository_tree_path(id: repo.uuid, commit: current_component[:script_version], path: '/') %>
+                    <% elsif repo and k == :script %>
+                      <%= link_to current_component[k], show_repository_blob_path(id: repo.uuid, commit: current_component[:script_version], path: 'crunch_scripts/'+current_component[:script]) %>
+                    <% elsif repo and k == :script_version %>
+                      <%= link_to current_component[k], show_repository_commit_path(id: repo.uuid, commit: current_component[:script_version]) %>
                     <% else %>
                       <%= current_component[k] %>
                     <% end %>
diff --git a/apps/workbench/app/views/repositories/_repository_breadcrumbs.html.erb b/apps/workbench/app/views/repositories/_repository_breadcrumbs.html.erb
new file mode 100644
index 0000000..736c187
--- /dev/null
+++ b/apps/workbench/app/views/repositories/_repository_breadcrumbs.html.erb
@@ -0,0 +1,13 @@
+<div class="pull-right">
+  <span class="deemphasize">Browsing <%= @object.name %> repository at commit</span>
+  <%= link_to(@commit, show_repository_commit_path(id: @object.uuid, commit: @commit), title: 'show commit message') %>
+</div>
+<p>
+  <%= link_to(@object.name, show_repository_tree_path(id: @object.uuid, commit: @commit, path: '/'), title: 'show root directory of source tree') %>
+  <% parents = ''
+     (@path || '').split('/').each do |pathpart|
+     parents = parents + pathpart + '/'
+     %>
+    / <%= link_to pathpart, show_repository_tree_path(id: @object.uuid, commit: @commit, path: parents) %>
+  <% end %>
+</p>
diff --git a/apps/workbench/app/views/repositories/show_blob.html.erb b/apps/workbench/app/views/repositories/show_blob.html.erb
new file mode 100644
index 0000000..acc34d1
--- /dev/null
+++ b/apps/workbench/app/views/repositories/show_blob.html.erb
@@ -0,0 +1,13 @@
+<%= render partial: 'repository_breadcrumbs' %>
+
+<% if not @blobdata.valid_encoding? %>
+  <div class="alert alert-warning">
+    <p>
+      This file has an invalid text encoding, so it can't be shown
+      here.  (This probably just means it's a binary file, not a text
+      file.)
+    </p>
+  </div>
+<% else %>
+  <pre><%= @blobdata %></pre>
+<% end %>
diff --git a/apps/workbench/app/views/repositories/show_commit.html.erb b/apps/workbench/app/views/repositories/show_commit.html.erb
new file mode 100644
index 0000000..3690be6
--- /dev/null
+++ b/apps/workbench/app/views/repositories/show_commit.html.erb
@@ -0,0 +1,3 @@
+<%= render partial: 'repository_breadcrumbs' %>
+
+<pre><%= @object.show @commit %></pre>
diff --git a/apps/workbench/app/views/repositories/show_tree.html.erb b/apps/workbench/app/views/repositories/show_tree.html.erb
new file mode 100644
index 0000000..4e2fcec
--- /dev/null
+++ b/apps/workbench/app/views/repositories/show_tree.html.erb
@@ -0,0 +1,40 @@
+<%= render partial: 'repository_breadcrumbs' %>
+
+<table class="table table-condensed table-hover">
+  <thead>
+    <tr>
+      <th>File</th>
+      <th class="data-size">Size</th>
+    </tr>
+  </thead>
+  <tbody>
+    <% @subtree.each do |mode, sha1, size, subpath| %>
+      <tr>
+        <td>
+          <span style="opacity: 0.6">
+            <% pathparts = subpath.sub(/^\//, '').split('/')
+               basename = pathparts.pop
+               parents = @path
+               pathparts.each do |pathpart| %>
+              <% parents = parents + '/' + pathpart %>
+              <%= link_to pathpart, url_for(path: parents) %>
+              /
+            <% end %>
+          </span>
+          <%= link_to basename, url_for(action: :show_blob, path: parents + '/' + basename) %>
+        </td>
+        <td class="data-size">
+          <%= human_readable_bytes_html(size) %>
+        </td>
+      </tr>
+    <% end %>
+    <% if @subtree.empty? %>
+      <tr>
+        <td>
+          No files found.
+        </td>
+      </tr>
+    <% end %>
+  </tbody>
+  <tfoot></tfoot>
+</table>
diff --git a/apps/workbench/config/application.default.yml b/apps/workbench/config/application.default.yml
index 8aeacf4..4061ee8 100644
--- a/apps/workbench/config/application.default.yml
+++ b/apps/workbench/config/application.default.yml
@@ -139,8 +139,14 @@ common:
   default_openid_prefix: https://www.google.com/accounts/o8/id
   send_user_setup_notification_email: true
 
-  # Set user_profile_form_fields to enable and configure the user profile page.
-  # Default is set to false. A commented setting with full description is provided below.
+  # Scratch directory used by the remote repository browsing
+  # feature. If it doesn't exist, it (and any missing parents) will be
+  # created using mkdir_p.
+  repository_cache: <%= File.expand_path 'tmp/git', Rails.root %>
+
+  # Set user_profile_form_fields to enable and configure the user
+  # profile page. Default is set to false. A commented example with
+  # full description is provided below.
   user_profile_form_fields: false
 
   # Below is a sample setting of user_profile_form_fields config parameter.
diff --git a/apps/workbench/config/routes.rb b/apps/workbench/config/routes.rb
index 7ed02e7..44d7ded 100644
--- a/apps/workbench/config/routes.rb
+++ b/apps/workbench/config/routes.rb
@@ -26,6 +26,11 @@ ArvadosWorkbench::Application.routes.draw do
   resources :repositories do
     post 'share_with', on: :member
   end
+  # {format: false} prevents rails from treating "foo.png" as foo?format=png
+  get '/repositories/:id/tree/:commit' => 'repositories#show_tree'
+  get '/repositories/:id/tree/:commit/*path' => 'repositories#show_tree', as: :show_repository_tree, format: false
+  get '/repositories/:id/blob/:commit/*path' => 'repositories#show_blob', as: :show_repository_blob, format: false
+  get '/repositories/:id/commit/:commit' => 'repositories#show_commit', as: :show_repository_commit
   match '/logout' => 'sessions#destroy', via: [:get, :post]
   get '/logged_out' => 'sessions#index'
   resources :users do
diff --git a/apps/workbench/test/controllers/repositories_controller_test.rb b/apps/workbench/test/controllers/repositories_controller_test.rb
index f95bb77..25bf557 100644
--- a/apps/workbench/test/controllers/repositories_controller_test.rb
+++ b/apps/workbench/test/controllers/repositories_controller_test.rb
@@ -1,7 +1,9 @@
 require 'test_helper'
+require 'helpers/repository_stub_helper'
 require 'helpers/share_object_helper'
 
 class RepositoriesControllerTest < ActionController::TestCase
+  include RepositoryStubHelper
   include ShareObjectHelper
 
   [
@@ -62,4 +64,61 @@ class RepositoriesControllerTest < ActionController::TestCase
       end
     end
   end
+
+  ### Browse repository content
+
+  [:active, :spectator].each do |user|
+    test "show tree to #{user}" do
+      reset_api_fixtures_after_test false
+      sha1, _, _ = stub_repo_content
+      get :show_tree, {
+        id: api_fixture('repositories')['foo']['uuid'],
+        commit: sha1,
+      }, session_for(user)
+      assert_response :success
+      assert_select 'tr td a', 'COPYING'
+      assert_select 'tr td', '625 bytes'
+      assert_select 'tr td a', 'apps'
+      assert_select 'tr td a', 'workbench'
+      assert_select 'tr td a', 'Gemfile'
+      assert_select 'tr td', '33.7 KiB'
+    end
+
+    test "show commit to #{user}" do
+      reset_api_fixtures_after_test false
+      sha1, commit, _ = stub_repo_content
+      get :show_commit, {
+        id: api_fixture('repositories')['foo']['uuid'],
+        commit: sha1,
+      }, session_for(user)
+      assert_response :success
+      assert_select 'pre', h(commit)
+    end
+
+    test "show blob to #{user}" do
+      reset_api_fixtures_after_test false
+      sha1, _, filedata = stub_repo_content filename: 'COPYING'
+      get :show_blob, {
+        id: api_fixture('repositories')['foo']['uuid'],
+        commit: sha1,
+        path: 'COPYING',
+      }, session_for(user)
+      assert_response :success
+      assert_select 'pre', h(filedata)
+    end
+  end
+
+  ['', '/'].each do |path|
+    test "show tree with path '#{path}'" do
+      reset_api_fixtures_after_test false
+      sha1, _, _ = stub_repo_content filename: 'COPYING'
+      get :show_tree, {
+        id: api_fixture('repositories')['foo']['uuid'],
+        commit: sha1,
+        path: path,
+      }, session_for(:active)
+      assert_response :success
+      assert_select 'tr td', 'COPYING'
+    end
+  end
 end
diff --git a/apps/workbench/test/helpers/repository_stub_helper.rb b/apps/workbench/test/helpers/repository_stub_helper.rb
new file mode 100644
index 0000000..b7d0573
--- /dev/null
+++ b/apps/workbench/test/helpers/repository_stub_helper.rb
@@ -0,0 +1,33 @@
+module RepositoryStubHelper
+  # Supply some fake git content.
+  def stub_repo_content opts={}
+    fakesha1 = opts[:sha1] || 'abcdefabcdefabcdefabcdefabcdefabcdefabcd'
+    fakefilename = opts[:filename] || 'COPYING'
+    fakefilesrc = File.expand_path('../../../../../'+fakefilename, __FILE__)
+    fakefile = File.read fakefilesrc
+    fakecommit = <<-EOS
+      commit abcdefabcdefabcdefabcdefabcdefabcdefabcd
+      Author: Fake R <fake at example.com>
+      Date:   Wed Apr 1 11:59:59 2015 -0400
+
+          It's a fake commit.
+
+    EOS
+    Repository.any_instance.stubs(:ls_tree_lr).with(fakesha1).returns <<-EOS
+      100644 blob eec475862e6ec2a87554e0fca90697e87f441bf5     226    .gitignore
+      100644 blob acbd7523ed49f01217874965aa3180cccec89d61     625    COPYING
+      100644 blob d645695673349e3947e8e5ae42332d0ac3164cd7   11358    LICENSE-2.0.txt
+      100644 blob c7a36c355b4a2b94dfab45c9748330022a788c91     622    README
+      100644 blob dba13ed2ddf783ee8118c6a581dbf75305f816a3   34520    agpl-3.0.txt
+      100644 blob 9bef02bbfda670595750fd99a4461005ce5b8f12     695    apps/workbench/.gitignore
+      100644 blob b51f674d90f68bfb50d9304068f915e42b04aea4    2249    apps/workbench/Gemfile
+      100644 blob b51f674d90f68bfb50d9304068f915e42b04aea4    2249    apps/workbench/Gemfile
+      100755 blob cdd5ebaff27781f93ab85e484410c0ce9e97770f    1012    crunch_scripts/hash
+    EOS
+    Repository.any_instance.
+      stubs(:cat_file).with(fakesha1, fakefilename).returns fakefile
+    Repository.any_instance.
+      stubs(:show).with(fakesha1).returns fakecommit
+    return fakesha1, fakecommit, fakefile
+  end
+end
diff --git a/apps/workbench/test/integration/repositories_browse_test.rb b/apps/workbench/test/integration/repositories_browse_test.rb
new file mode 100644
index 0000000..147bf46
--- /dev/null
+++ b/apps/workbench/test/integration/repositories_browse_test.rb
@@ -0,0 +1,37 @@
+require 'integration_helper'
+require 'helpers/repository_stub_helper'
+require 'helpers/share_object_helper'
+
+class RepositoriesTest < ActionDispatch::IntegrationTest
+  include RepositoryStubHelper
+  include ShareObjectHelper
+
+  reset_api_fixtures :after_each_test, false
+
+  setup do
+    need_javascript
+  end
+
+  test "browse repository from jobs#show" do
+    sha1 = api_fixture('jobs')['running']['script_version']
+    _, fakecommit, fakefile =
+      stub_repo_content sha1: sha1, filename: 'crunch_scripts/hash'
+    show_object_using 'active', 'jobs', 'running', sha1
+    click_on api_fixture('jobs')['running']['script']
+    assert_text fakefile
+    click_on 'crunch_scripts'
+    assert_selector 'td a', text: 'hash'
+    click_on 'foo'
+    assert_selector 'td a', text: 'crunch_scripts'
+    click_on sha1
+    assert_text fakecommit
+
+    show_object_using 'active', 'jobs', 'running', sha1
+    click_on 'active/foo'
+    assert_selector 'td a', text: 'crunch_scripts'
+
+    show_object_using 'active', 'jobs', 'running', sha1
+    click_on sha1
+    assert_text fakecommit
+  end
+end
diff --git a/apps/workbench/test/test_helper.rb b/apps/workbench/test/test_helper.rb
index ade6292..630a64f 100644
--- a/apps/workbench/test/test_helper.rb
+++ b/apps/workbench/test/test_helper.rb
@@ -270,12 +270,17 @@ class ActiveSupport::TestCase
   end
 
   def after_teardown
-    if self.class.want_reset_api_fixtures[:after_each_test]
+    if self.class.want_reset_api_fixtures[:after_each_test] and
+        @want_reset_api_fixtures != false
       self.class.reset_api_fixtures_now
     end
     super
   end
 
+  def reset_api_fixtures_after_test t=true
+    @want_reset_api_fixtures = t
+  end
+
   protected
   def self.reset_api_fixtures_now
     # Never try to reset fixtures when we're just using test
diff --git a/services/api/test/fixtures/jobs.yml b/services/api/test/fixtures/jobs.yml
index c662062..23b32ef 100644
--- a/services/api/test/fixtures/jobs.yml
+++ b/services/api/test/fixtures/jobs.yml
@@ -7,6 +7,8 @@ running:
   created_at: <%= 3.minute.ago.to_s(:db) %>
   started_at: <%= 3.minute.ago.to_s(:db) %>
   finished_at: ~
+  script: hash
+  repository: active/foo
   script_version: 1de84a854e2b440dc53bf42f8548afa4c17da332
   running: true
   success: ~
@@ -31,6 +33,8 @@ running_cancelled:
   created_at: <%= 4.minute.ago.to_s(:db) %>
   started_at: <%= 3.minute.ago.to_s(:db) %>
   finished_at: ~
+  script: hash
+  repository: active/foo
   script_version: 1de84a854e2b440dc53bf42f8548afa4c17da332
   running: true
   success: ~
@@ -56,6 +60,8 @@ uses_nonexistent_script_version:
   created_at: <%= 5.minute.ago.to_s(:db) %>
   started_at: <%= 3.minute.ago.to_s(:db) %>
   finished_at: <%= 2.minute.ago.to_s(:db) %>
+  script: hash
+  repository: active/foo
   running: false
   success: true
   output: d41d8cd98f00b204e9800998ecf8427e+0

commit 4f99b52f6866deaeb4b614e165b7af0cb3a6adba
Author: Tom Clegg <tom at curoverse.com>
Date:   Tue Mar 31 10:05:47 2015 -0400

    5416: Fix comment.

diff --git a/apps/workbench/config/application.default.yml b/apps/workbench/config/application.default.yml
index 40c0bfe..8aeacf4 100644
--- a/apps/workbench/config/application.default.yml
+++ b/apps/workbench/config/application.default.yml
@@ -203,5 +203,5 @@ common:
   # in the directory where your API server is running.
   anonymous_user_token: false
 
-  # Include Accept-Encoding header when making API requests
+  # Enable response payload compression in Arvados API requests.
   include_accept_encoding_header_in_api_requests: true

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list