Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
  • 9 commits
  • 19 files changed
  • 0 comments
  • 1 contributor
19  Gemfile
@@ -6,19 +6,19 @@ gem 'rails', '>= 3.2.6'
6 6
 # gem 'rails', :git => 'git://github.com/rails/rails.git',
7 7
 #              :branch => '3-0-stable'
8 8
 gem 'mysql2', '>= 0.3.11'
9  
-gem 'sqlite3', '>= 1.3.5'
  9
+gem 'sqlite3', '>= 1.3.6'
10 10
 
11 11
 # Gems used only for assets and not required
12 12
 # in production environments by default.
13 13
 group :assets do
14  
-  gem 'sass-rails', '>= 3.2.3'
15  
-  gem 'coffee-rails', '>= 3.2.1'
16  
-  gem 'uglifier'
  14
+  gem 'sass-rails', '>= 3.2.5'
  15
+  gem 'coffee-rails', '>= 3.2.2'
  16
+  gem 'uglifier', '>= 1.2.6'
17 17
   
18  
-  gem 'therubyracer', '>= 0.9.10'
  18
+  gem 'therubyracer', '>= 0.10.1'
19 19
 end
20 20
 
21  
-gem 'jquery-rails', '>= 2.0.0'
  21
+gem 'jquery-rails', '>= 2.0.2'
22 22
 
23 23
 # Use unicorn as the web server
24 24
 # gem 'unicorn'
@@ -29,13 +29,14 @@ gem 'jquery-rails', '>= 2.0.0'
29 29
 # To use debugger
30 30
 # gem 'ruby-debug'
31 31
 
32  
-gem 'authpwn_rails', '>= 0.10.9'
  32
+gem 'authpwn_rails', '>= 0.11.0'
33 33
 gem 'configvars_rails', '>= 0.5.2'
34 34
 gem 'gravatar-ultimate', '>= 1.0.3'
35 35
 gem 'grit', :git => 'https://github.com/pwnall/grit.git', :branch => 'hunks'
36 36
 gem 'json', :platforms => [:mri_18, :jruby]
37 37
 gem 'markdpwn', '>= 0.1.2'
38 38
 gem 'net-ssh', '>= 2.3.0', :require => 'net/ssh'
  39
+gem 'posix-spawn', '>= 0.3.6'
39 40
 gem 'rbtree', '>= 0.3.0', :platform => :mri
40 41
 gem 'rbtree-pure', '>= 0.1.1', :require => 'rbtree',
41 42
                                :platforms => [:jruby, :rbx]
@@ -46,9 +47,9 @@ gem 'topological_sort', '>= 0.1.1'
46 47
 # and rake tasks are available in development mode:
47 48
 group :development, :test do
48 49
   gem 'railroady', '>= 0.4.5'
49  
-  gem 'thin', '>= 1.3.1'
  50
+  gem 'thin', '>= 1.4.1'
50 51
 end
51 52
 
52 53
 group :test do
53  
-  gem 'mocha', '>= 0.10.4'
  54
+  gem 'mocha', '>= 0.12.0'
54 55
 end
29  Gemfile.lock
@@ -40,9 +40,9 @@ GEM
40 40
       i18n (~> 0.6)
41 41
       multi_json (~> 1.0)
42 42
     arel (3.0.2)
43  
-    authpwn_rails (0.10.11)
  43
+    authpwn_rails (0.11.0)
44 44
       fbgraph_rails (>= 0.2.2)
45  
-      rails (>= 3.2.3)
  45
+      rails (>= 3.2.6)
46 46
     blankslate (2.1.2.4)
47 47
     builder (3.0.0)
48 48
     coffee-rails (3.2.2)
@@ -108,7 +108,7 @@ GEM
108 108
       wikicloth (>= 0.7.1)
109 109
     metaclass (0.0.1)
110 110
     mime-types (1.19)
111  
-    mocha (0.11.4)
  111
+    mocha (0.12.0)
112 112
       metaclass (~> 0.0.1)
113 113
     multi_json (1.3.6)
114 114
     multipart-post (1.1.5)
@@ -120,7 +120,7 @@ GEM
120 120
       jwt (~> 0.1.4)
121 121
       multi_json (~> 1.0)
122 122
       rack (~> 1.2)
123  
-    org-ruby (0.6.3)
  123
+    org-ruby (0.7.0)
124 124
       rubypants (>= 0.2.0)
125 125
     polyglot (0.3.3)
126 126
     posix-spawn (0.3.6)
@@ -173,7 +173,7 @@ GEM
173 173
     sqlite3 (1.3.6)
174 174
     therubyracer (0.10.1)
175 175
       libv8 (~> 3.3.10)
176  
-    thin (1.3.1)
  176
+    thin (1.4.1)
177 177
       daemons (>= 1.0.9)
178 178
       eventmachine (>= 0.12.6)
179 179
       rack (>= 1.0.0)
@@ -195,24 +195,25 @@ PLATFORMS
195 195
   ruby
196 196
 
197 197
 DEPENDENCIES
198  
-  authpwn_rails (>= 0.10.9)
199  
-  coffee-rails (>= 3.2.1)
  198
+  authpwn_rails (>= 0.11.0)
  199
+  coffee-rails (>= 3.2.2)
200 200
   configvars_rails (>= 0.5.2)
201 201
   gravatar-ultimate (>= 1.0.3)
202 202
   grit!
203  
-  jquery-rails (>= 2.0.0)
  203
+  jquery-rails (>= 2.0.2)
204 204
   json
205 205
   markdpwn (>= 0.1.2)
206  
-  mocha (>= 0.10.4)
  206
+  mocha (>= 0.12.0)
207 207
   mysql2 (>= 0.3.11)
208 208
   net-ssh (>= 2.3.0)
  209
+  posix-spawn (>= 0.3.6)
209 210
   railroady (>= 0.4.5)
210 211
   rails (>= 3.2.6)
211 212
   rbtree (>= 0.3.0)
212 213
   rbtree-pure (>= 0.1.1)
213  
-  sass-rails (>= 3.2.3)
214  
-  sqlite3 (>= 1.3.5)
215  
-  therubyracer (>= 0.9.10)
216  
-  thin (>= 1.3.1)
  214
+  sass-rails (>= 3.2.5)
  215
+  sqlite3 (>= 1.3.6)
  216
+  therubyracer (>= 0.10.1)
  217
+  thin (>= 1.4.1)
217 218
   topological_sort (>= 0.1.1)
218  
-  uglifier
  219
+  uglifier (>= 1.2.6)
20  app/controllers/application_controller.rb
@@ -17,11 +17,29 @@ def current_user_can_edit_repo
17 17
     _current_user_can_x_repo :can_edit?
18 18
   end
19 19
   
20  
-  # Implements the user_can_*_repo filters.
  20
+  # Implements the current_user_can_*_repo filters.
21 21
   def _current_user_can_x_repo(message)
22 22
     @profile = Profile.where(:name => params[:profile_name]).first
23 23
     @repository = @profile.repositories.where(:name => params[:repo_name]).first
24 24
     
25 25
     bounce_user unless @repository.send message, current_user
26 26
   end
  27
+
  28
+  # before_filter verifying the HTTP Basic user's access to the repo in params.  
  29
+  def http_user_can_read_repo
  30
+    _http_user_can_x_repo :can_read?
  31
+  end
  32
+  
  33
+  # before_filter verifying the HTTP Basic user's access to the repo in params.  
  34
+  def http_user_can_commit_to_repo
  35
+    _http_user_can_x_repo :can_commit?
  36
+  end
  37
+  
  38
+  # Implements the http_user_can_*_repo filters.
  39
+  def _http_user_can_x_repo(message)
  40
+    @profile = Profile.where(:name => params[:profile_name]).first
  41
+    @repository = @profile.repositories.where(:name => params[:repo_name]).first
  42
+    
  43
+    bounce_to_http_basic unless @repository.send message, current_user
  44
+  end
27 45
 end
3  app/controllers/repositories_controller.rb
@@ -159,8 +159,7 @@ def change_notice
159 159
     @repository = Repository.find_by_ssh_path params[:repo_path]
160 160
 
161 161
     if @repository
162  
-      changes = @repository.integrate_changes
163  
-      @repository.publish_changes @ssh_key.user.profile, changes
  162
+      @repository.record_push @ssh_key.user
164 163
       success = true
165 164
       message = 'OK'
166 165
     else
81  app/controllers/smart_http_controller.rb
... ...
@@ -0,0 +1,81 @@
  1
+class SmartHttpController < ApplicationController
  2
+  # TODO(pwnall): figure out some CSRF protection
  3
+  skip_before_filter :verify_authenticity_token
  4
+  
  5
+  # Rejecting session cookies slightly mitigates the risk of CSRF. Users might
  6
+  # still type their credentials into the auth window popped out by the
  7
+  # browser, but we'll deal with that later.
  8
+  skip_before_filter :authenticate_using_session, :except => :index
  9
+
  10
+  # Git is capable of using this to authenticate over HTTP.
  11
+  authenticates_using_http_basic
  12
+  
  13
+  before_filter :http_user_can_read_repo, :except => [:index, :receive_pack]
  14
+  before_filter :http_user_can_commit_to_repo, :only => [:receive_pack]
  15
+
  16
+  # GET costan/rails.git
  17
+  def index
  18
+    @profile = Profile.where(:name => params[:profile_name]).first
  19
+    @repository = @profile.repositories.where(:name => params[:repo_name]).first
  20
+    redirect_to profile_repository_path(@profile, @repository)
  21
+  end
  22
+
  23
+  # GET costan/rails.git/info/refs
  24
+  def info_refs
  25
+    command = git_command
  26
+    unless command
  27
+      # Using the dumb HTTP protocol.
  28
+      params[:path] = 'info/refs'
  29
+      return git_file
  30
+    end
  31
+
  32
+    output = @repository.run_command('git', [command, '--stateless-rpc',
  33
+                                             '--advertise-refs', '.'])
  34
+    git_header = "# service=#{params[:service]}\n"
  35
+    data = ['%04x' % (git_header.length + 4), git_header, '0000',
  36
+            output].join ''
  37
+    send_data data, :type => "application/x-git-#{command}-advertisement"
  38
+  end
  39
+
  40
+  # GET costan/rails.git/....
  41
+  def git_file
  42
+    file_path = @repository.internal_file_path params[:path]
  43
+    mime_type = @repository.internal_file_mime_type params[:path]
  44
+    if File.exist?(file_path)
  45
+      send_file file_path, :type => mime_type
  46
+    else
  47
+      head :not_found
  48
+    end
  49
+  end
  50
+
  51
+  # POST costan/rails.git/git-upload-pack
  52
+  def upload_pack
  53
+    self.headers['Content-Type'] = 'application/x-git-upload-pack-result'
  54
+    self.response_body = @repository.stream_command 'git', ['upload-pack',
  55
+        '--stateless-rpc', '.'], request.body
  56
+  end
  57
+
  58
+  # POST costan/rails.git/git-receive-pack
  59
+  def receive_pack
  60
+    self.headers['Content-Type'] = 'application/x-git-receive-pack-result'
  61
+    command_streamer = @repository.stream_command 'git', ['receive-pack',
  62
+        '--stateless-rpc', '.'], request.body do
  63
+      @repository.record_push current_user
  64
+    end
  65
+    self.response_body = command_streamer
  66
+    # TODO(pwnall): update repository state
  67
+  end
  68
+
  69
+  # The git command targeted by the HTTP request.
  70
+  def git_command
  71
+    case params[:service]
  72
+    when 'git-receive-pack'
  73
+      'receive-pack'
  74
+    when 'git-upload-pack'
  75
+      'upload-pack'
  76
+    else
  77
+      nil
  78
+    end
  79
+  end
  80
+  private :git_command
  81
+end
177  app/models/repository.rb
@@ -338,6 +338,8 @@ def contents_added(git_commits)
338 338
   #                 deleted: array of Tag models removed from the on-disk
339 339
   #                          repository
340 340
   def integrate_changes
  341
+    self.update_http_info
  342
+
341 343
     changes = {}
342 344
     
343 345
     branch_delta = self.branch_changes
@@ -408,6 +410,181 @@ def integrate_changes
408 410
       :tags => { :added => new_tags, :changed => changed_tags,
409 411
                  :deleted => tag_delta[:deleted] } }
410 412
   end
  413
+
  414
+  # Updates all data structures to reflect a git push on this repository.
  415
+  #
  416
+  # This method sync the database information with the on-disk repository, and
  417
+  # updates all the repository's feeds. The method is called for both
  418
+  # git-over-ssh and git-over-http pushes.
  419
+  #
  420
+  # @param [User] user the user behind the push
  421
+  def record_push(user)
  422
+    changes = integrate_changes
  423
+    publish_changes user.profile, changes
  424
+  end
  425
+end
  426
+
  427
+# :nodoc: support for http sync
  428
+class Repository
  429
+  # Path to a file inside the raw repository.
  430
+  # 
  431
+  # This should only be used to implement low-level functionality, such as
  432
+  # git-over-http.
  433
+  def internal_file_path(file)
  434
+    File.join local_path, file
  435
+  end
  436
+
  437
+  # The MIME type for a file inside a repository.
  438
+  def internal_file_mime_type(file)
  439
+    if file[0, 8] == 'objects/'
  440
+      if file[8, 5] == 'pack/'
  441
+        return 'application/x-git-packed-objects' if file[-5, 5] == '.pack'
  442
+        return 'application/x-git-packed-objects-toc' if file[-4, 4] == '.idx'
  443
+      elsif file[8, 5] == 'info/'
  444
+        return 'text/plain; charset=utf-8' if file == 'objects/info/packs'
  445
+      elsif /^objects\/[0-9a-f]+\/[0-9a-f]+$/ =~ file
  446
+        return 'application/x-git-loose-object'
  447
+      end
  448
+    elsif file == 'info/refs'
  449
+      return 'text/plain; charset=utf-8'
  450
+    end
  451
+    'text/plain'
  452
+  end
  453
+
  454
+  # True if the given file inside a repository will never change.
  455
+  # 
  456
+  # This is intended to help make caching decisions.
  457
+  def internal_file_immutable?(file)
  458
+    # loose files
  459
+    return true if /^objects\/[0-9a-f]+\/[0-9a-f]+$/ =~ file
  460
+    # packs
  461
+    return true if /^objects\/pack\// =~ file
  462
+
  463
+    false
  464
+  end
  465
+
  466
+  # Runs a command inside the repository's directory.
  467
+  #
  468
+  # This method should be used for running low-level commands on the
  469
+  # repository, such as git gc.
  470
+  #
  471
+  # @param [String] binary the program to be executed
  472
+  # @param [Array<String>] args arguments for the program to be executed
  473
+  # @return [String] the program's stdout
  474
+  def run_command(binary, args = [])
  475
+    child = POSIX::Spawn::Child.new binary, *args, :chdir => local_path
  476
+    return child.out if child.status.success?
  477
+
  478
+    # TODO(pwnall): consider logging the command and stderr to some admin tool
  479
+    raise "Non-zero exit code #{child.status.exitstatus} running #{binary} #{args.inspect}."
  480
+  end
  481
+
  482
+  # Re-generates the files used by the dumb git-over-HTTP protocol.
  483
+  def update_http_info
  484
+    run_command 'git', ['repack']
  485
+    run_command 'git', ['update-server-info']
  486
+  end
  487
+
  488
+  # Runs a data-intensive command inside the repository's directory.
  489
+  # 
  490
+  # This method should be used for running low-level commands on the
  491
+  # repository, such as git gc. The method is optimized for commands that
  492
+  # consume or produce a lot of data.
  493
+  #
  494
+  # @param [String] binary the program to be executed
  495
+  # @param [Array<String>] args arguments for the program to be executed
  496
+  # @param [#read, #eof?] input_io IO-like object supplying the command's stdin
  497
+  # @param [Integer] buffer_size the size of the read/write buffer to be used
  498
+  #     for streaming data into and out of the sub-process running the command
  499
+  # @yield Yielded once, after the command has completed.
  500
+  # @return [#each] object implementing the Rails protocol for streaming output
  501
+  def stream_command(binary, args = [], input_io = nil, buffer_size = 8192,
  502
+                     &done_proc)
  503
+    StreamCommandWrapper.new binary, args, local_path, input_io, buffer_size,
  504
+                             done_proc
  505
+  end
  506
+
  507
+  # :nodoc: implements stream_command
  508
+  class StreamCommandWrapper
  509
+    # Launches a sub-process running the command.
  510
+    def initialize(binary, args, working_dir, input_io, buffer_size, done_proc)
  511
+      @binary = binary
  512
+      @input_io = input_io
  513
+      @buffer_size = buffer_size
  514
+      @done_proc = done_proc
  515
+
  516
+      @pid, @stdin, @stdout, @stderr =
  517
+           POSIX::Spawn.popen4(binary, *args, { :chdir => working_dir })
  518
+      @stdin.set_encoding Encoding::BINARY, Encoding::BINARY
  519
+      @stdout.set_encoding Encoding::BINARY, Encoding::BINARY
  520
+      @stderr.set_encoding Encoding::BINARY, Encoding::BINARY
  521
+    end
  522
+
  523
+    # Communicates with the sub-process running the command.
  524
+    def each(&http_proc)
  525
+      if @input_io
  526
+        loop do
  527
+          in_chunk = ''.force_encoding Encoding::BINARY
  528
+          begin
  529
+            @input_io.readpartial @buffer_size, in_chunk
  530
+          rescue Errno::EAGAIN, Errno::EINTR
  531
+            next  # Try again.
  532
+          rescue EOFError
  533
+            break
  534
+          end
  535
+          begin
  536
+            @stdin.write in_chunk unless in_chunk.empty?
  537
+          rescue IOError  # Closed stream.
  538
+            break
  539
+          end
  540
+        end
  541
+      end
  542
+
  543
+      begin
  544
+        @stdin.close
  545
+      rescue IOError  # Closed stream, so we don't need to close it.
  546
+      end
  547
+
  548
+      loop do
  549
+        out_chunk = ''.force_encoding Encoding::BINARY
  550
+        begin
  551
+          @stdout.readpartial @buffer_size, out_chunk
  552
+        rescue Errno::EAGAIN, Errno::EINTR
  553
+          next  # Try again.
  554
+        rescue IOError  # EOF or closed stream.
  555
+          break
  556
+        end
  557
+        http_proc.call out_chunk unless out_chunk.empty?
  558
+      end
  559
+
  560
+      stderr_text = ''.force_encoding Encoding::BINARY
  561
+      begin
  562
+        unless @stderr.eof?
  563
+          @stderr.read nil, stderr_text
  564
+          # TODO(pwnall): consider logging stderr
  565
+        end
  566
+      rescue IOError  # Closed stream, so we don't need to close it.
  567
+      end
  568
+      
  569
+      _, status = ::Process.wait2 @pid
  570
+
  571
+      begin
  572
+        @stdout.close
  573
+      rescue IOError  # Closed stream, so we don't need to close it.
  574
+      end
  575
+      begin
  576
+        @stderr.close
  577
+      rescue IOError  # Closed stream, so we don't need to close it.
  578
+      end
  579
+
  580
+      if status && !status.success?
  581
+        raise RuntimeError, "Non-zero #{@binary} exit code.\n#{stderr_text}"
  582
+      end
  583
+
  584
+      @done_proc.call if @done_proc
  585
+      self
  586
+    end
  587
+  end
411 588
 end
412 589
 
413 590
 # :nodoc: access control
7  app/views/repositories/_repository.html.erb
@@ -44,6 +44,13 @@
44 44
                   profile_repository_url(repository.profile, repository) %>
45 45
     </dd>
46 46
     <dt>
  47
+      git+http
  48
+    </dt>
  49
+    <dd>
  50
+      <%= link_to git_over_http_url(repository.profile, repository),
  51
+                  git_over_http_url(repository.profile, repository) %>
  52
+    </dd>
  53
+    <dt>
47 54
       git+ssh
48 55
     </dt>
49 56
     <dd>
14  config/routes.rb
@@ -46,6 +46,11 @@
46 46
   end
47 47
       
48 48
   scope ':profile_name', :constraints => { :profile_name => /[^_][^\/]+/ } do
  49
+    # Mis-used git-over http repository link.
  50
+    get ':repo_name.git' => 'smart_http#index',
  51
+        :constraints => { :repo_name => /[^\/]+/ }, :format => false,
  52
+        :as => :git_over_http
  53
+    
49 54
     # Repositories.
50 55
     get ':repo_name(.:format)' => 'repositories#show',
51 56
         :constraints => { :repo_name => /[^\/]+/ },
@@ -67,6 +72,15 @@
67 72
       delete ':repo_name(.:format)' => 'repositories#destroy'      
68 73
     end
69 74
   end
  75
+
  76
+  # HTTP fetch and push.
  77
+  scope ':profile_name/:repo_name.git', :constraints => {
  78
+      :profile_name => /[^_\/]+/, :repo_name => /[^\/]+/ } do
  79
+    post 'git-upload-pack' => 'smart_http#upload_pack'
  80
+    post 'git-receive-pack' => 'smart_http#receive_pack'
  81
+    get 'info/refs' => 'smart_http#info_refs'
  82
+    get '*path' => 'smart_http#git_file', :format => false
  83
+  end
70 84
   
71 85
   scope ':profile_name/:repo_name',
72 86
       :constraints => { :profile_name => /[^_\/]+/, :repo_name => /[^\/]+/ } do
11  test/fixtures/repo.git/info/refs
... ...
@@ -0,0 +1,11 @@
  1
+ddf9d231a59d83803b3090fa93bb46a7fcd4ff8f	refs/heads/branch1
  2
+93d00ea479394cd110116b29748538d16d9b931e	refs/heads/branch2
  3
+88ca4433d478d6abb6558bebb9524fb72300457e	refs/heads/master
  4
+effe47936c7557bf43116023b9729a2edc4ee19e	refs/tags/demo
  5
+93d00ea479394cd110116b29748538d16d9b931e	refs/tags/demo^{}
  6
+1d394a41b8fdf5046b19efd9cc1ed90e1e116112	refs/tags/unicorns
  7
+ddf9d231a59d83803b3090fa93bb46a7fcd4ff8f	refs/tags/unicorns^{}
  8
+456d282305542de6b6d2e649787e437e23c2ff93	refs/tags/v1.0
  9
+09129da5a9d7b16d4f1245abf8ab35eb09a74bf6	refs/tags/v1.0^{}
  10
+1074079141321c69617ac36b310c7e9ec134508a	refs/tags/v2.0
  11
+88ca4433d478d6abb6558bebb9524fb72300457e	refs/tags/v2.0^{}
2  test/fixtures/repo.git/objects/info/packs
... ...
@@ -0,0 +1,2 @@
  1
+P pack-7f67317db46e457c4fa046b22d8e87593c40a625.pack
  2
+
BIN  test/fixtures/repo.git/objects/pack/pack-7f67317db46e457c4fa046b22d8e87593c40a625.idx
Binary file not shown
BIN  test/fixtures/repo.git/objects/pack/pack-7f67317db46e457c4fa046b22d8e87593c40a625.pack
Binary file not shown
1  test/functional/profiles_controller_test.rb
@@ -2,6 +2,7 @@
2 2
 
3 3
 class ProfilesControllerTest < ActionController::TestCase
4 4
   setup :mock_profile_paths
  5
+  teardown :mock_profile_paths_undo
5 6
   
6 7
   setup do
7 8
     @profile = profiles(:costan)
1  test/functional/repositories_controller_test.rb
@@ -2,6 +2,7 @@
2 2
 
3 3
 class RepositoriesControllerTest < ActionController::TestCase
4 4
   setup :mock_profile_paths
  5
+  teardown :mock_profile_paths_undo
5 6
 
6 7
   setup do
7 8
     @repository = repositories(:dexter_ghost)
255  test/functional/smart_http_controller_test.rb
... ...
@@ -0,0 +1,255 @@
  1
+require 'test_helper'
  2
+
  3
+class SmartHttpControllerTest < ActionController::TestCase
  4
+  setup :mock_any_repository_path
  5
+  
  6
+  setup do
  7
+    @repo = repositories(:dexter_ghost)
  8
+    @profile = @repo.profile
  9
+    @user = @profile.user
  10
+    set_http_basic_user @user
  11
+  end
  12
+
  13
+  test 'index with no user' do
  14
+    set_http_basic_user nil
  15
+    get :index, :profile_name => @profile.to_param,
  16
+                :repo_name => @repo.to_param
  17
+  end
  18
+
  19
+  test 'HEAD' do
  20
+    get :git_file, :profile_name => @profile.to_param,
  21
+                   :repo_name => @repo.to_param, :path => 'HEAD'
  22
+    assert_response :success
  23
+    assert_equal "ref: refs/heads/master\n", response.body
  24
+    assert_equal 'text/plain', response.headers['Content-Type']
  25
+  end
  26
+
  27
+  test 'HEAD with no user' do
  28
+    set_http_basic_user nil
  29
+    get :git_file, :profile_name => @profile.to_param,
  30
+                   :repo_name => @repo.to_param, :path => 'HEAD'
  31
+    assert_response :unauthorized
  32
+  end
  33
+
  34
+  test 'HEAD with session cookies' do
  35
+    set_http_basic_user nil
  36
+    set_session_current_user @user
  37
+    get :git_file, :profile_name => @profile.to_param,
  38
+                   :repo_name => @repo.to_param, :path => 'HEAD'
  39
+    assert_response :unauthorized
  40
+  end
  41
+
  42
+  test 'dumb git pack fetch' do
  43
+    path = 'objects/pack/pack-7f67317db46e457c4fa046b22d8e87593c40a625.pack'
  44
+    get :git_file, :profile_name => @profile.to_param,
  45
+                   :repo_name => @repo.to_param, :path => path
  46
+  
  47
+    assert_response :success
  48
+    assert_equal response.body[0, 4], 'PACK'
  49
+    assert_equal 'application/x-git-packed-objects',
  50
+                 response.headers['Content-Type']
  51
+  end
  52
+
  53
+  test 'dumb git pack fetch with no user' do
  54
+    set_http_basic_user nil
  55
+    path = 'objects/pack/pack-7f67317db46e457c4fa046b22d8e87593c40a625.pack'
  56
+    get :git_file, :profile_name => @profile.to_param,
  57
+                   :repo_name => @repo.to_param, :path => path
  58
+    assert_response :unauthorized
  59
+  end
  60
+  
  61
+  test 'dumb git pack fetch with session cookies' do
  62
+    set_http_basic_user nil
  63
+    set_session_current_user @user
  64
+    path = 'objects/pack/pack-7f67317db46e457c4fa046b22d8e87593c40a625.pack'
  65
+    get :git_file, :profile_name => @profile.to_param,
  66
+                   :repo_name => @repo.to_param, :path => path
  67
+    assert_response :unauthorized
  68
+  end
  69
+  
  70
+  test 'dumb info/refs' do
  71
+    get :info_refs, :profile_name => @profile.to_param,
  72
+                    :repo_name => @repo.to_param
  73
+    assert_response :success
  74
+    assert_includes response.body,
  75
+        "88ca4433d478d6abb6558bebb9524fb72300457e\trefs/heads/master\n"
  76
+    assert_equal 'text/plain; charset=utf-8', response.headers['Content-Type']
  77
+  end
  78
+
  79
+  test 'dumb info/refs with no user' do
  80
+    set_http_basic_user nil
  81
+    get :info_refs, :profile_name => @profile.to_param,
  82
+                    :repo_name => @repo.to_param
  83
+    assert_response :unauthorized
  84
+  end
  85
+
  86
+  test 'dumb info/refs with session cookies' do
  87
+    set_http_basic_user nil
  88
+    set_session_current_user @user
  89
+    get :info_refs, :profile_name => @profile.to_param,
  90
+                    :repo_name => @repo.to_param
  91
+    assert_response :unauthorized
  92
+  end
  93
+
  94
+  test 'smart info/refs for git-upload-pack' do
  95
+    get :info_refs, :profile_name => @profile.to_param,
  96
+                    :repo_name => @repo.to_param,
  97
+                    :service => 'git-upload-pack'
  98
+    assert_response :success
  99
+    assert_equal "001e# service=git-upload-pack\n0000", response.body[0, 34],
  100
+                 'Incorrect response header'
  101
+    assert_includes response.body,
  102
+        "003f88ca4433d478d6abb6558bebb9524fb72300457e refs/heads/master\n",
  103
+        "Response doesn't include the master branch"
  104
+    assert_equal 'application/x-git-upload-pack-advertisement',
  105
+                 response.headers['Content-Type']
  106
+  end
  107
+
  108
+  test 'smart info/refs for git-receive-pack' do
  109
+    get :info_refs, :profile_name => @profile.to_param,
  110
+                    :repo_name => @repo.to_param,
  111
+                    :service => 'git-receive-pack'
  112
+    assert_response :success
  113
+    assert_equal "001f# service=git-receive-pack\n0000", response.body[0, 35],
  114
+                 'Incorrect response header'
  115
+    assert_includes response.body,
  116
+        "003f88ca4433d478d6abb6558bebb9524fb72300457e refs/heads/master\n",
  117
+        "Response doesn't include the master branch"
  118
+    assert_equal 'application/x-git-receive-pack-advertisement',
  119
+                 response.headers['Content-Type']
  120
+  end
  121
+
  122
+  test 'null upload-pack' do
  123
+    @request.env['RAW_POST_DATA'] = "0000"
  124
+    @request.headers['CONTENT-TYPE'] = 'application/x-git-upload-pack-request'
  125
+    assert_no_difference 'Tag.count', 'upload-pack recorded a push' do
  126
+      assert_no_difference 'FeedItem.count' do
  127
+        post :upload_pack, :profile_name => @profile.to_param,
  128
+                           :repo_name => @repo.to_param
  129
+        
  130
+        assert_response :success
  131
+        assert_equal '', response.body
  132
+        assert_equal 'application/x-git-upload-pack-result',
  133
+                     response.headers['Content-Type']
  134
+      end
  135
+    end
  136
+  end
  137
+
  138
+  test 'null upload-pack with no user' do
  139
+    set_http_basic_user nil
  140
+    @request.env['RAW_POST_DATA'] = "0000"
  141
+    
  142
+    @request.headers['CONTENT-TYPE'] = 'application/x-git-upload-pack-request'
  143
+    post :upload_pack, :profile_name => @profile.to_param,
  144
+                       :repo_name => @repo.to_param
  145
+        
  146
+    assert_response :unauthorized
  147
+  end
  148
+
  149
+  test 'null upload-pack with session user' do
  150
+    set_http_basic_user nil
  151
+    set_session_current_user @user
  152
+    @request.env['RAW_POST_DATA'] = "0000"
  153
+    
  154
+    @request.headers['CONTENT-TYPE'] = 'application/x-git-upload-pack-request'
  155
+    post :upload_pack, :profile_name => @profile.to_param,
  156
+                       :repo_name => @repo.to_param
  157
+        
  158
+    assert_response :unauthorized
  159
+  end
  160
+
  161
+  test 'upload-pack with data' do
  162
+    @request.env['RAW_POST_DATA'] = "006fwant 88ca4433d478d6abb6558bebb9524fb72300457e multi_ack_detailed no-done side-band-64k thin-pack ofs-delta\n0032want 88ca4433d478d6abb6558bebb9524fb72300457e\n00000009done\n"
  163
+    @request.headers['Content-Type'] = 'application/x-git-upload-pack-request'
  164
+    post :upload_pack, :profile_name => @profile.to_param,
  165
+                       :repo_name => @repo.to_param
  166
+    assert_response :success
  167
+    body = response.body
  168
+    assert_include body, 'Counting objects'
  169
+    assert_include body, 'PACK'
  170
+    assert_equal 'application/x-git-upload-pack-result',
  171
+                 response.headers['Content-Type']
  172
+  end
  173
+
  174
+  test 'null receive-pack' do
  175
+    @request.env['RAW_POST_DATA'] = '0000'
  176
+    @request.headers['Content-Type'] = 'application/x-git-receive-pack-request'
  177
+    assert_difference 'Tag.count', 1 do
  178
+      assert_difference 'FeedItem.count', 7 do
  179
+        post :receive_pack, :profile_name => @profile.to_param,
  180
+                            :repo_name => @repo.to_param
  181
+
  182
+        assert_response :success
  183
+        assert_equal '', response.body
  184
+        assert_equal 'application/x-git-receive-pack-result',
  185
+                     response.headers['Content-Type']
  186
+      end
  187
+    end
  188
+  end
  189
+
  190
+  test 'null receive-pack with no user' do
  191
+    set_http_basic_user nil
  192
+
  193
+    @request.env['RAW_POST_DATA'] = '0000'
  194
+    @request.headers['Content-Type'] = 'application/x-git-receive-pack-request'
  195
+    assert_no_difference 'Tag.count', 1 do
  196
+      assert_no_difference 'FeedItem.count', 7 do
  197
+        post :receive_pack, :profile_name => @profile.to_param,
  198
+                            :repo_name => @repo.to_param
  199
+
  200
+        assert_response :unauthorized
  201
+        # Make sure that any streamed git command completes
  202
+        response.body
  203
+      end
  204
+    end
  205
+  end
  206
+
  207
+  test 'null receive-pack with session user' do
  208
+    set_http_basic_user nil
  209
+    set_session_current_user @user
  210
+
  211
+    @request.env['RAW_POST_DATA'] = '0000'
  212
+    @request.headers['Content-Type'] = 'application/x-git-receive-pack-request'
  213
+    assert_no_difference 'Tag.count', 1 do
  214
+      assert_no_difference 'FeedItem.count', 7 do
  215
+        post :receive_pack, :profile_name => @profile.to_param,
  216
+                            :repo_name => @repo.to_param
  217
+
  218
+        assert_response :unauthorized
  219
+        # Make sure that any streamed git command completes
  220
+        response.body
  221
+      end
  222
+    end
  223
+  end
  224
+
  225
+  test 'smart http routes' do
  226
+    assert_routing({:path => '/costan/rails.git', :method => :get},
  227
+                   {:controller => 'smart_http', :action => 'index',
  228
+                    :profile_name => 'costan', :repo_name => 'rails'})
  229
+    assert_routing({:path => '/costan/rails.git/info/refs', :method => :get},
  230
+                   {:controller => 'smart_http', :action => 'info_refs',
  231
+                    :profile_name => 'costan', :repo_name => 'rails'})
  232
+    assert_routing({:path => '/costan/rails.git/HEAD', :method => :get},
  233
+                   {:controller => 'smart_http', :action => 'git_file',
  234
+                    :profile_name => 'costan', :repo_name => 'rails',
  235
+                    :path => 'HEAD'})
  236
+    assert_routing({:path => '/costan/rails.git/objects/info/http-alternates',
  237
+                    :method => :get},
  238
+                   {:controller => 'smart_http', :action => 'git_file',
  239
+                    :profile_name => 'costan', :repo_name => 'rails',
  240
+                    :path => 'objects/info/http-alternates'})
  241
+    assert_routing({:path => '/costan/rails.git/objects/pack/pack-12345.pack',
  242
+                    :method => :get},
  243
+                   {:controller => 'smart_http', :action => 'git_file',
  244
+                    :profile_name => 'costan', :repo_name => 'rails',
  245
+                    :path => 'objects/pack/pack-12345.pack'})
  246
+    assert_routing({:path => '/costan/rails.git/git-upload-pack',
  247
+                    :method => :post},
  248
+                   {:controller => 'smart_http', :action => 'upload_pack',
  249
+                    :profile_name => 'costan', :repo_name => 'rails'})
  250
+    assert_routing({:path => '/costan/rails.git/git-receive-pack',
  251
+                    :method => :post},
  252
+                   {:controller => 'smart_http', :action => 'receive_pack',
  253
+                    :profile_name => 'costan', :repo_name => 'rails'})
  254
+  end
  255
+end
4  test/helpers/mock_repository_path.rb
@@ -2,13 +2,15 @@ class ActiveSupport::TestCase
2 2
   # Mocks the on-disk repository path to point to the fixture repository.
3 3
   def mock_repository_path(repo)
4 4
     fixture_repo_path = Rails.root.join('test', 'fixtures', 'repo.git').to_s
  5
+    repo.stubs(:local_path).returns fixture_repo_path
5 6
     grit_repo = Grit::Repo.new fixture_repo_path
6  
-    repo.stubs(:grit_repo).returns(grit_repo)
  7
+    repo.stubs(:grit_repo).returns grit_repo
7 8
   end
8 9
   
9 10
   # Mocks Grit so all repositories point to the fixtures repository.
10 11
   def mock_any_repository_path
11 12
     fixture_repo_path = Rails.root.join('test', 'fixtures', 'repo.git').to_s
  13
+    Repository.any_instance.stubs(:local_path).returns fixture_repo_path
12 14
     grit_repo = Grit::Repo.new fixture_repo_path
13 15
     Grit::Repo.stubs(:new).returns(grit_repo)
14 16
   end    
32  test/integration/git_push_test.rb
@@ -127,4 +127,36 @@ def add_commit_push
127 127
     assert Kernel.system('git push -q --tags origin master'),
128 128
            'Git push failed'
129 129
   end
  130
+
  131
+  test "repository http clone push and delete" do
  132
+    FileUtils.rm_r @win_repository.local_path
  133
+    FileUtils.cp_r @fixture_repo_path, @win_repository.local_path
  134
+    FileUtils.chmod_R 0770, @win_repository.local_path    
  135
+    
  136
+    http_url = File.join ConfigVar['app_uri'],
  137
+        git_over_http_path(@win_repository.profile, @win_repository) 
  138
+    # Hacky way of embedding username:password.
  139
+    user = @win_repository.profile.user
  140
+    http_url.sub! '://', "://#{CGI.escape(user.email)}:pa55w0rd@"
  141
+
  142
+    Dir.chdir @temp_dir do
  143
+      assert Kernel.system("git clone -q #{http_url}"),
  144
+             'Failed to clone repository'
  145
+      FileUtils.cp 'git-ssh.sh', 'rwin'
  146
+      Dir.chdir 'rwin' do
  147
+        add_commit_push
  148
+      end
  149
+      
  150
+      assert_equal 'Integration test commit',
  151
+          @win_repository.branches.where(:name => 'master').first.commit.
  152
+                          message, 'Pushed branches not assimilated'
  153
+      assert_equal 'Integration test tag',
  154
+          @win_repository.tags.where(:name => 'integration').first.message
  155
+          'Pushed tags not assimilated'
  156
+    end
  157
+    @win_repository.destroy
  158
+    assert !File.exist?(@win_repository.local_path),
  159
+           'Failed to remove repository'
  160
+  end
130 161
 end
  162
+
1  test/unit/profile_test.rb
@@ -2,6 +2,7 @@
2 2
 
3 3
 class ProfileTest < ActiveSupport::TestCase
4 4
   setup :mock_profile_paths
  5
+  teardown :mock_profile_paths_undo
5 6
   
6 7
   setup do
7 8
     @profile = Profile.new :name => 'awesome',
112  test/unit/repository_test.rb
@@ -2,6 +2,7 @@
2 2
 
3 3
 class RepositoryTest < ActiveSupport::TestCase
4 4
   setup :mock_profile_paths
  5
+  teardown :mock_profile_paths_undo
5 6
 
6 7
   setup do
7 8
     @repo = Repository.new :name => 'awesome', :public => false,
@@ -387,7 +388,7 @@ class RepositoryTest < ActiveSupport::TestCase
387 388
         end
388 389
       end
389 390
     end
390  
-    
  391
+
391 392
     assert_equal ['Easy mode', "Merge branch 'branch1'"],
392 393
                  delta[:commits].map(&:message).sort, 'New commits'
393 394
     assert_equal ['deleted'], delta[:branches][:deleted].map(&:name),
@@ -406,7 +407,114 @@ class RepositoryTest < ActiveSupport::TestCase
406 407
                  'Added tags'
407 408
     assert_equal ['unicorns'], delta[:tags][:changed].map(&:name),
408 409
                  'Changed tags'
409  
-  end  
  410
+  end
  411
+
  412
+  test 'record_push' do
  413
+    pusher = profiles(:costan)
  414
+    repo = repositories(:dexter_ghost)
  415
+    mock_repository_path repo
  416
+    assert_difference 'Tag.count', 1 do
  417
+      assert_difference 'Commit.count', 2 do
  418
+        assert_difference 'FeedItem.count', 7 do
  419
+          repo.record_push pusher.user
  420
+        end
  421
+      end
  422
+    end
  423
+
  424
+    assert_equal pusher, repo.feed_items.last.author
  425
+  end
  426
+
  427
+
  428
+  test 'integrate_changes updates http info' do
  429
+    repo = repositories(:dexter_ghost)
  430
+    mock_repository_path repo
  431
+    http_info_path = File.join repo.local_path, 'info/refs'
  432
+    File.delete http_info_path
  433
+    repo.integrate_changes
  434
+    assert File.exist?(http_info_path), 'HTTP file info/refs not regenerated'
  435
+  end
  436
+    
  437
+  test 'internal_file_path' do
  438
+    mock_repository_path @repo
  439
+    path = @repo.internal_file_path('HEAD')
  440
+    assert_equal "ref: refs/heads/master\n", File.read(path)
  441
+  end
  442
+
  443
+  test 'internal_file_mime_type' do
  444
+    [['info/refs', 'text/plain; charset=utf-8'],
  445
+     ['objects/info/alternatives', 'text/plain'],
  446
+     ['objects/info/http-alternatives', 'text/plain'],
  447
+     ['objects/info/packs', 'text/plain; charset=utf-8'],
  448
+     ['objects/info/whatever', 'text/plain'],
  449
+     ['objects/1a/2b3c4d', 'application/x-git-loose-object'],
  450
+     ['objects/pack/pack-1a2b3c.idx', 'application/x-git-packed-objects-toc'],
  451
+     ['objects/pack/pack-1a2b3c.pack', 'application/x-git-packed-objects']
  452
+    ].each do |file, golden_type|
  453
+      assert_equal golden_type, @repo.internal_file_mime_type(file),
  454
+        "MIME check for #{file}"
  455
+    end
  456
+  end
  457
+
  458
+  test 'internal_file_immutable?' do
  459
+    [['info/refs', false],
  460
+     ['objects/info/alternatives', false],
  461
+     ['objects/info/http-alternatives', false],
  462
+     ['objects/info/packs', false],
  463
+     ['objects/info/whatever', false],
  464
+     ['objects/1a/2b3c4d', true],
  465
+     ['objects/pack/pack-1a2b3c.idx', true],
  466
+     ['objects/pack/pack-1a2b3c.pack', true]
  467
+    ].each do |file, golden|
  468
+      assert_equal golden, @repo.internal_file_immutable?(file),
  469
+        "Immutable content check for #{file}"
  470
+    end
  471
+  end
  472
+
  473
+  test 'run_command' do
  474
+    mock_repository_path @repo
  475
+    output = @repo.run_command 'ls', ['HEAD']
  476
+    assert_equal "HEAD\n", output
  477
+  end
  478
+
  479
+  test 'update_http_info' do
  480
+    repo = repositories(:dexter_ghost)
  481
+    mock_repository_path repo
  482
+    http_info_path = File.join repo.local_path, 'info/refs'
  483
+    packs_path = File.join repo.local_path, 'objects/pack'
  484
+    File.delete http_info_path
  485
+    FileUtils.rm_rf packs_path
  486
+    repo.update_http_info
  487
+    assert File.exist?(http_info_path), 'HTTP file info/refs not regenerated'
  488
+    assert File.exist?(packs_path), 'Git packs not regenerated'
  489
+  end
  490
+
  491
+  test 'stream_command working directory' do
  492
+    mock_repository_path @repo
  493
+    streamer = @repo.stream_command 'ls', ['HEAD']
  494
+    output = ''
  495
+    streamer.each { |data| output << data }
  496
+    assert_equal "HEAD\n", output
  497
+  end
  498
+
  499
+  test 'stream_command buffering and post-completion yield' do
  500
+    binary = Encoding::BINARY
  501
+    in_chunk = "gitty truly owns ".force_encoding(binary) +
  502
+      [0xDE, 0xAD, 0xBE, 0xEF].pack('C*').force_encoding(binary)
  503
+    in_data = (in_chunk * 15).force_encoding binary
  504
+    mock_repository_path @repo
  505
+    done = false
  506
+    streamer = @repo.stream_command 'cat', [], StringIO.new(in_data), 16 do
  507
+      done = true
  508
+    end
  509
+    output = ''.force_encoding binary
  510
+    streamer.each do |data|
  511
+      assert_operator data.length, :<=, 16, 'Buffering error'
  512
+      assert_equal false, done, 'Post-completion block yielded too early'
  513
+      output << data
  514
+    end
  515
+    assert_equal in_data, output, 'Sub-process data streaming error'
  516
+    assert_equal true, done, 'Post-completion block not yielded'
  517
+  end
410 518
     
411 519
   test 'acl for new repository' do
412 520
     @repo.save!

No commit comments for this range

Something went wrong with that request. Please try again.