Skip to content
This repository

Show Routes while Debugging RoutingError #6696

Merged
merged 3 commits into from almost 2 years ago

11 participants

Richard Schneeman Rafael Mendonça França Damien Mathieu José Valim Aaron Patterson Robert Starsi Rodrigo Alves Danilo Assis Olek Janiszewski Mike Burns Mattt Thompson
Richard Schneeman
Collaborator

If someone receives a routing error, they likely need to view the routes. Rather than making them visit '/rails/info/routes' or run rake routes we can give them that information on the page.

Screenshot:
Screeshot

ActionDispatch Tests Pass

Richard Schneeman
Collaborator

The only real question was where to put the code to format the routes. In a more traditional architecture it would belong in the controller 100%. However, in other exceptions there are code blocks in the views , and while it doesn't make for clean view code, it does keep the middleware lean.

Rafael Mendonça França
Owner

Very cool. I would leave the formatter code in the middleware.

actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
... ...
@@ -1,5 +1,7 @@
1 1
 require 'action_dispatch/http/request'
2 2
 require 'action_dispatch/middleware/exception_wrapper'
  3
+require 'rails/application/route_inspector'
2
Rafael Mendonça França Owner

I just realized that this will only work if railties is loaded in your application. As railties is not a actionpack dependency this can fail with a LoadError

Rafael Mendonça França Owner

@josevalim thoughts about this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Damien Mathieu
Collaborator

:+1: love the idea.

José Valim
Owner

As @rafaelfranca, this is backwards, actionpack cannot depend on railties. In fact, every time you see a reference to the Rails constant in actionpack, it is something that needs to be fixed by providing a proper configuration or abstract (except in railties files).

That said, it seems the best way would be to move the router inspector to actionpack.

Aaron Patterson
Owner

Agree with @josevalim, we need to move the inspector to AP. I do like this idea, but we should be careful about performance of the 500 pages.

José Valim
Owner
Richard Schneeman
Collaborator

ActionDispatch already knows quite a bit about routes, formatting them to be human readable doesn't seem like too far of a leap.

On the perf side it looks like DebugExceptions is in the middleware in production, but the custom error pages are ignored unless env['action_dispatch.show_detailed_exceptions'] is set. Restrict any route formatting/rendering code to within that section and there won't be any performance degradation.

Richard Schneeman
Collaborator

@josevalim, @rafaelfranca, & @tenderlove I paired with @mattt to move the Route Inspector out of Railties, and into Actionpack (action_dispatch). Re-ran all railties tests (https://gist.github.com/b298e74a2d20863c9486) and AR with sqlite3 (https://gist.github.com/b298e74a2d20863c9486). ATP.

Now action_dispatch can render routes when routing errors are received independently of railties.

José Valim
Owner

Thanks @schneems ! One thing left: could we move the route inspector tests to inside action dispatch too? Thanks!

actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
... ...
@@ -78,5 +81,13 @@ def logger(env)
78 81
     def stderr_logger
79 82
       @stderr_logger ||= ActiveSupport::Logger.new($stderr)
80 83
     end
  84
+
  85
+    private
  86
+    def formatted_routes(exception)
  87
+      if exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error)
  88
+        inspector = ActionDispatch::Routing::RouteInspector.new
  89
+        inspector.format(Rails.application.routes.routes).join("\n")
2
Richard Schneeman Collaborator
schneems added a note July 06, 2012

We missed this reference Rails.application.routes.routes. Moving the inspector was the easy part, getting rid of that line will require more code changes. Right now railties builds routes using ActionDispatch, but ActionDispatch doesn't actually know what routes it has. This is the method in railties for Rails.application.routes.

def routes
  @routes ||= ActionDispatch::Routing::RouteSet.new.tap do |routes|
    routes.draw_paths.concat paths["config/routes"].paths
  end

  @routes.append(&Proc.new) if block_given?
  @routes
end

My first reaction is to keep ActionDispatch agnostic about where the routes come from, but to store the results, so that Railties can inject the path where it wants the routes to be drawn from. ActionDispatch, rather than just spitting out routes can hold onto them.

def routes
  @routes ||= ActionDispatch::Routing.set_from_paths(paths["config/routes"].paths)

  @routes.append(&Proc.new) if block_given?
  @routes
end

Then we could have a getter in ActionDispatch along the lines of ActionDispatch::Routing.routes, how does that sound?

(on a side note we should also make Journey an injectable dependency rather than hard coding it into ActionDispatch, though this isn't the place or time for that)

José Valim Owner
josevalim added a note July 06, 2012

You can simply pass the route set or Rails.application as argument to the middleware initialization.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Richard Schneeman
Collaborator

Updated the middleware to take an optional routes_app that should respond to :routes. Also moved the route inspector test to action_dispatch, which you'll be happy to know shaves off 2 seconds from the railties test suite!!

ATP on ActionPack https://gist.github.com/56da1c69f269900bb51c, ActiveRecord, and Railties

railties/lib/rails/application.rb
... ...
@@ -266,7 +266,7 @@ def default_middleware_stack
266 266
         middleware.use ::ActionDispatch::RequestId
267 267
         middleware.use ::Rails::Rack::Logger, config.log_tags # must come after Rack::MethodOverride to properly log overridden methods
268 268
         middleware.use ::ActionDispatch::ShowExceptions, config.exceptions_app || ActionDispatch::PublicExceptions.new(Rails.public_path)
269  
-        middleware.use ::ActionDispatch::DebugExceptions
  269
+        middleware.use ::ActionDispatch::DebugExceptions, Rails.application
2
Olek Janiszewski
exviva added a note July 06, 2012

Couldn't this simply be self instead of Rails.application?

Richard Schneeman Collaborator
schneems added a note July 06, 2012

yes, and there is a line a little lower that sets app = self, it might be more clear to move that line up and use the app var rather than relying on the hard coded

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Richard Schneeman
Collaborator

updated

José Valim
Owner

This looks good to be merged in my opinion. It needs to be rebased though and we need a changelog entry. :) Thanks!

and others added some commits June 09, 2012
Richard Schneeman show routes while debugging RoutingError
If someone receives a routing error, they likely need to view the routes. Rather than making them visit '/rails/info/routes' or run `rake routes` we can give them that information on the page.
fa714ec
Mattt Thompson move route_inspector to actionpack
this is so we can show route output in the development when we get a routing error. Railties can use features of ActionDispatch, but ActionDispatch should not depend on Railties.
ef91cdd
Richard Schneeman
Collaborator

Rebased against upstream master, added changelog entry, let me know if there is anything else. Thanks for your help!

José Valim josevalim merged commit 7404cda into from July 08, 2012
José Valim josevalim closed this July 08, 2012
Mike Burns

Fantastic!

Robert Starsi

So cool.

Rodrigo Alves

Great idea!

Danilo Assis

Simple but GREAT!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 3 unique commits by 2 authors.

Jul 07, 2012
Richard Schneeman show routes while debugging RoutingError
If someone receives a routing error, they likely need to view the routes. Rather than making them visit '/rails/info/routes' or run `rake routes` we can give them that information on the page.
fa714ec
Mattt Thompson move route_inspector to actionpack
this is so we can show route output in the development when we get a routing error. Railties can use features of ActionDispatch, but ActionDispatch should not depend on Railties.
ef91cdd
Richard Schneeman show routes while debugging added to changelog bbfd29a
This page is out of date. Refresh to see the latest.
2  actionpack/CHANGELOG.md
Source Rendered
... ...
@@ -1,5 +1,7 @@
1 1
 ## Rails 4.0.0 (unreleased) ##
2 2
 
  3
+*   Add routes to page while debugging a RoutingError in development. *Richard Schneeman and Mattt Thompson*
  4
+
3 5
 *   Add `ActionController::Flash.add_flash_types` method to allow people to register their own flash types. e.g.:
4 6
 
5 7
         class ApplicationController
19  actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
... ...
@@ -1,5 +1,7 @@
1 1
 require 'action_dispatch/http/request'
2 2
 require 'action_dispatch/middleware/exception_wrapper'
  3
+require 'action_dispatch/routing/inspector'
  4
+
3 5
 
4 6
 module ActionDispatch
5 7
   # This middleware is responsible for logging exceptions and
@@ -7,8 +9,9 @@ module ActionDispatch
7 9
   class DebugExceptions
8 10
     RESCUES_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates')
9 11
 
10  
-    def initialize(app)
11  
-      @app = app
  12
+    def initialize(app, routes_app = nil)
  13
+      @app        = app
  14
+      @routes_app = routes_app
12 15
     end
13 16
 
14 17
     def call(env)
@@ -39,7 +42,8 @@ def render_exception(env, exception)
39 42
           :exception => wrapper.exception,
40 43
           :application_trace => wrapper.application_trace,
41 44
           :framework_trace => wrapper.framework_trace,
42  
-          :full_trace => wrapper.full_trace
  45
+          :full_trace => wrapper.full_trace,
  46
+          :routes => formatted_routes(exception)
43 47
         )
44 48
 
45 49
         file = "rescues/#{wrapper.rescue_template}"
@@ -78,5 +82,14 @@ def logger(env)
78 82
     def stderr_logger
79 83
       @stderr_logger ||= ActiveSupport::Logger.new($stderr)
80 84
     end
  85
+
  86
+    private
  87
+    def formatted_routes(exception)
  88
+      return false unless @routes_app.respond_to?(:routes)
  89
+      if exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error)
  90
+        inspector = ActionDispatch::Routing::RouteInspector.new
  91
+        inspector.format(@routes_app.routes.routes).join("\n")
  92
+      end
  93
+    end
81 94
   end
82 95
 end
10  actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb
@@ -10,8 +10,14 @@
10 10
     </ol>
11 11
   </p>
12 12
 <% end %>
  13
+<%= render :template => "rescues/_trace" %>
  14
+
  15
+<h2>
  16
+  Routes
  17
+</h2>
  18
+
13 19
 <p>
14  
-  Try running <code>rake routes</code> for more information on available routes.
  20
+  Routes match in priority from top to bottom
15 21
 </p>
16 22
 
17  
-<%= render :template => "rescues/_trace" %>
  23
+<p><pre><%= @routes %></pre></p>
121  actionpack/lib/action_dispatch/routing/inspector.rb
... ...
@@ -0,0 +1,121 @@
  1
+require 'delegate'
  2
+
  3
+module ActionDispatch
  4
+  module Routing
  5
+    class RouteWrapper < SimpleDelegator
  6
+      def endpoint
  7
+        rack_app ? rack_app.inspect : "#{controller}##{action}"
  8
+      end
  9
+
  10
+      def constraints
  11
+        requirements.except(:controller, :action)
  12
+      end
  13
+
  14
+      def rack_app(app = self.app)
  15
+        @rack_app ||= begin
  16
+          class_name = app.class.name.to_s
  17
+          if class_name == "ActionDispatch::Routing::Mapper::Constraints"
  18
+            rack_app(app.app)
  19
+          elsif ActionDispatch::Routing::Redirect === app || class_name !~ /^ActionDispatch::Routing/
  20
+            app
  21
+          end
  22
+        end
  23
+      end
  24
+
  25
+      def verb
  26
+        super.source.gsub(/[$^]/, '')
  27
+      end
  28
+
  29
+      def path
  30
+        super.spec.to_s
  31
+      end
  32
+
  33
+      def name
  34
+        super.to_s
  35
+      end
  36
+
  37
+      def reqs
  38
+        @reqs ||= begin
  39
+          reqs = endpoint
  40
+          reqs += " #{constraints.inspect}" unless constraints.empty?
  41
+          reqs
  42
+        end
  43
+      end
  44
+
  45
+      def controller
  46
+        requirements[:controller] || ':controller'
  47
+      end
  48
+
  49
+      def action
  50
+        requirements[:action] || ':action'
  51
+      end
  52
+
  53
+      def internal?
  54
+        path =~ %r{/rails/info.*|^#{Rails.application.config.assets.prefix}}
  55
+      end
  56
+
  57
+      def engine?
  58
+        rack_app && rack_app.respond_to?(:routes)
  59
+      end
  60
+    end
  61
+
  62
+    ##
  63
+    # This class is just used for displaying route information when someone
  64
+    # executes `rake routes`.  People should not use this class.
  65
+    class RouteInspector # :nodoc:
  66
+      def initialize
  67
+        @engines = Hash.new
  68
+      end
  69
+
  70
+      def format(all_routes, filter = nil)
  71
+        if filter
  72
+          all_routes = all_routes.select{ |route| route.defaults[:controller] == filter }
  73
+        end
  74
+
  75
+        routes = collect_routes(all_routes)
  76
+
  77
+        formatted_routes(routes) +
  78
+          formatted_routes_for_engines
  79
+      end
  80
+
  81
+      def collect_routes(routes)
  82
+        routes = routes.collect do |route|
  83
+          RouteWrapper.new(route)
  84
+        end.reject do |route|
  85
+          route.internal?
  86
+        end.collect do |route|
  87
+          collect_engine_routes(route)
  88
+
  89
+          {:name => route.name, :verb => route.verb, :path => route.path, :reqs => route.reqs }
  90
+        end
  91
+      end
  92
+
  93
+      def collect_engine_routes(route)
  94
+        name = route.endpoint
  95
+        return unless route.engine?
  96
+        return if @engines[name]
  97
+
  98
+        routes = route.rack_app.routes
  99
+        if routes.is_a?(ActionDispatch::Routing::RouteSet)
  100
+          @engines[name] = collect_routes(routes.routes)
  101
+        end
  102
+      end
  103
+
  104
+      def formatted_routes_for_engines
  105
+        @engines.map do |name, routes|
  106
+          ["\nRoutes for #{name}:"] + formatted_routes(routes)
  107
+        end.flatten
  108
+      end
  109
+
  110
+      def formatted_routes(routes)
  111
+        name_width = routes.map{ |r| r[:name].length }.max
  112
+        verb_width = routes.map{ |r| r[:verb].length }.max
  113
+        path_width = routes.map{ |r| r[:path].length }.max
  114
+
  115
+        routes.map do |r|
  116
+          "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
  117
+        end
  118
+      end
  119
+    end
  120
+  end
  121
+end
170  actionpack/test/dispatch/routing/inspector_test.rb
... ...
@@ -0,0 +1,170 @@
  1
+require 'minitest/autorun'
  2
+require 'action_controller'
  3
+require 'rails/engine'
  4
+require 'action_dispatch/routing/inspector'
  5
+
  6
+module ActionDispatch
  7
+  module Routing
  8
+    class RouteInspectTest < ActiveSupport::TestCase
  9
+      def setup
  10
+        @set = ActionDispatch::Routing::RouteSet.new
  11
+        @inspector = ActionDispatch::Routing::RouteInspector.new
  12
+        app = ActiveSupport::OrderedOptions.new
  13
+        app.config = ActiveSupport::OrderedOptions.new
  14
+        app.config.assets = ActiveSupport::OrderedOptions.new
  15
+        app.config.assets.prefix = '/sprockets'
  16
+        Rails.stubs(:application).returns(app)
  17
+        Rails.stubs(:env).returns("development")
  18
+      end
  19
+
  20
+      def draw(&block)
  21
+        @set.draw(&block)
  22
+        @inspector.format(@set.routes)
  23
+      end
  24
+
  25
+      def test_displaying_routes_for_engines
  26
+        engine = Class.new(Rails::Engine) do
  27
+          def self.to_s
  28
+            "Blog::Engine"
  29
+          end
  30
+        end
  31
+        engine.routes.draw do
  32
+          get '/cart', :to => 'cart#show'
  33
+        end
  34
+
  35
+        output = draw do
  36
+          get '/custom/assets', :to => 'custom_assets#show'
  37
+          mount engine => "/blog", :as => "blog"
  38
+        end
  39
+
  40
+        expected = [
  41
+          "custom_assets GET /custom/assets(.:format) custom_assets#show",
  42
+          "         blog     /blog                    Blog::Engine",
  43
+          "\nRoutes for Blog::Engine:",
  44
+          "cart GET /cart(.:format) cart#show"
  45
+        ]
  46
+        assert_equal expected, output
  47
+      end
  48
+
  49
+      def test_cart_inspect
  50
+        output = draw do
  51
+          get '/cart', :to => 'cart#show'
  52
+        end
  53
+        assert_equal ["cart GET /cart(.:format) cart#show"], output
  54
+      end
  55
+
  56
+      def test_inspect_shows_custom_assets
  57
+        output = draw do
  58
+          get '/custom/assets', :to => 'custom_assets#show'
  59
+        end
  60
+        assert_equal ["custom_assets GET /custom/assets(.:format) custom_assets#show"], output
  61
+      end
  62
+
  63
+      def test_inspect_routes_shows_resources_route
  64
+        output = draw do
  65
+          resources :articles
  66
+        end
  67
+        expected = [
  68
+          "    articles GET    /articles(.:format)          articles#index",
  69
+          "             POST   /articles(.:format)          articles#create",
  70
+          " new_article GET    /articles/new(.:format)      articles#new",
  71
+          "edit_article GET    /articles/:id/edit(.:format) articles#edit",
  72
+          "     article GET    /articles/:id(.:format)      articles#show",
  73
+          "             PATCH  /articles/:id(.:format)      articles#update",
  74
+          "             PUT    /articles/:id(.:format)      articles#update",
  75
+          "             DELETE /articles/:id(.:format)      articles#destroy" ]
  76
+        assert_equal expected, output
  77
+      end
  78
+
  79
+      def test_inspect_routes_shows_root_route
  80
+        output = draw do
  81
+          root :to => 'pages#main'
  82
+        end
  83
+        assert_equal ["root GET / pages#main"], output
  84
+      end
  85
+
  86
+      def test_inspect_routes_shows_dynamic_action_route
  87
+        output = draw do
  88
+          get 'api/:action' => 'api'
  89
+        end
  90
+        assert_equal [" GET /api/:action(.:format) api#:action"], output
  91
+      end
  92
+
  93
+      def test_inspect_routes_shows_controller_and_action_only_route
  94
+        output = draw do
  95
+          get ':controller/:action'
  96
+        end
  97
+        assert_equal [" GET /:controller/:action(.:format) :controller#:action"], output
  98
+      end
  99
+
  100
+      def test_inspect_routes_shows_controller_and_action_route_with_constraints
  101
+        output = draw do
  102
+          get ':controller(/:action(/:id))', :id => /\d+/
  103
+        end
  104
+        assert_equal [" GET /:controller(/:action(/:id))(.:format) :controller#:action {:id=>/\\d+/}"], output
  105
+      end
  106
+
  107
+      def test_rake_routes_shows_route_with_defaults
  108
+        output = draw do
  109
+          get 'photos/:id' => 'photos#show', :defaults => {:format => 'jpg'}
  110
+        end
  111
+        assert_equal [%Q[ GET /photos/:id(.:format) photos#show {:format=>"jpg"}]], output
  112
+      end
  113
+
  114
+      def test_rake_routes_shows_route_with_constraints
  115
+        output = draw do
  116
+          get 'photos/:id' => 'photos#show', :id => /[A-Z]\d{5}/
  117
+        end
  118
+        assert_equal [" GET /photos/:id(.:format) photos#show {:id=>/[A-Z]\\d{5}/}"], output
  119
+      end
  120
+
  121
+      class RackApp
  122
+        def self.call(env)
  123
+        end
  124
+      end
  125
+
  126
+      def test_rake_routes_shows_route_with_rack_app
  127
+        output = draw do
  128
+          get 'foo/:id' => RackApp, :id => /[A-Z]\d{5}/
  129
+        end
  130
+        assert_equal [" GET /foo/:id(.:format) #{RackApp.name} {:id=>/[A-Z]\\d{5}/}"], output
  131
+      end
  132
+
  133
+      def test_rake_routes_shows_route_with_rack_app_nested_with_dynamic_constraints
  134
+        constraint = Class.new do
  135
+          def to_s
  136
+            "( my custom constraint )"
  137
+          end
  138
+        end
  139
+
  140
+        output = draw do
  141
+          scope :constraint => constraint.new do
  142
+            mount RackApp => '/foo'
  143
+          end
  144
+        end
  145
+
  146
+        assert_equal ["  /foo #{RackApp.name} {:constraint=>( my custom constraint )}"], output
  147
+      end
  148
+
  149
+      def test_rake_routes_dont_show_app_mounted_in_assets_prefix
  150
+        output = draw do
  151
+          get '/sprockets' => RackApp
  152
+        end
  153
+        assert_no_match(/RackApp/, output.first)
  154
+        assert_no_match(/\/sprockets/, output.first)
  155
+      end
  156
+
  157
+      def test_redirect
  158
+        output = draw do
  159
+          get "/foo"    => redirect("/foo/bar"), :constraints => { :subdomain => "admin" }
  160
+          get "/bar"    => redirect(path: "/foo/bar", status: 307)
  161
+          get "/foobar" => redirect{ "/foo/bar" }
  162
+        end
  163
+
  164
+        assert_equal "   foo GET /foo(.:format)    redirect(301, /foo/bar) {:subdomain=>\"admin\"}", output[0]
  165
+        assert_equal "   bar GET /bar(.:format)    redirect(307, path: /foo/bar)", output[1]
  166
+        assert_equal "foobar GET /foobar(.:format) redirect(301)", output[2]
  167
+      end
  168
+    end
  169
+  end
  170
+end
4  railties/lib/rails/application.rb
@@ -267,6 +267,7 @@ def reload_dependencies? #:nodoc:
267 267
 
268 268
     def default_middleware_stack #:nodoc:
269 269
       ActionDispatch::MiddlewareStack.new.tap do |middleware|
  270
+        app = self
270 271
         if rack_cache = config.action_controller.perform_caching && config.action_dispatch.rack_cache
271 272
           require "action_dispatch/http/rack_cache"
272 273
           middleware.use ::Rack::Cache, rack_cache
@@ -290,11 +291,10 @@ def default_middleware_stack #:nodoc:
290 291
         middleware.use ::ActionDispatch::RequestId
291 292
         middleware.use ::Rails::Rack::Logger, config.log_tags # must come after Rack::MethodOverride to properly log overridden methods
292 293
         middleware.use ::ActionDispatch::ShowExceptions, config.exceptions_app || ActionDispatch::PublicExceptions.new(Rails.public_path)
293  
-        middleware.use ::ActionDispatch::DebugExceptions
  294
+        middleware.use ::ActionDispatch::DebugExceptions, app
294 295
         middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies
295 296
 
296 297
         unless config.cache_classes
297  
-          app = self
298 298
           middleware.use ::ActionDispatch::Reloader, lambda { app.reload_dependencies? }
299 299
         end
300 300
 
3  railties/lib/rails/info_controller.rb
... ...
@@ -1,4 +1,4 @@
1  
-require 'rails/application/routes_inspector'
  1
+require 'action_dispatch/routing/inspector'
2 2
 
3 3
 class Rails::InfoController < ActionController::Base
4 4
   self.view_paths = File.join(File.dirname(__FILE__), 'templates')
@@ -16,6 +16,7 @@ def properties
16 16
 
17 17
   def routes
18 18
     inspector = Rails::Application::RoutesInspector.new
  19
+    inspector = ActionDispatch::Routing::RouteInspector.new
19 20
     @info     = inspector.format(_routes.routes).join("\n")
20 21
   end
21 22
 
4  railties/lib/rails/tasks/routes.rake
... ...
@@ -1,7 +1,7 @@
1 1
 desc 'Print out all defined routes in match order, with names. Target specific controller with CONTROLLER=x.'
2 2
 task :routes => :environment do
3 3
   all_routes = Rails.application.routes.routes
4  
-  require 'rails/application/routes_inspector'
5  
-  inspector = Rails::Application::RoutesInspector.new
  4
+  require 'action_dispatch/routing/inspector'
  5
+  inspector = ActionDispatch::Routing::RouteInspector.new
6 6
   puts inspector.format(all_routes, ENV['CONTROLLER']).join "\n"
7 7
 end
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.