RubyDay 2015: Deep Diving: How to Explore a New Code Base

RubyDay 2015: Deep Diving: How to Explore a New Code Base

As a developer, diving in a new code base is not uncommon: you've just been hired, you change of project, you want to help an open source project, the open source library your project depends on is buggy, etc. It's like entering in a underwater cave, you don't know the treasures or the monsters you'll find there, neither if the path is treacherous or if it's a true labyrinth where you'll get lost. You can prepare yourself, you can plan your visit, you can equip yourself, you can survive and find the gem you're looking for...

3f8fcddf7ab5d1bd90b0a0a9adfd6527?s=128

Christophe Philemotte

November 13, 2015
Tweet

Transcript

  1. Deep Diving: How to Explore a New Codebase Andy Spearing

    © 2009
  2. _toch toch Hi, I'm Christophe

  3. @matylda © 2011 Intro

  4. @matylda © 2011 The most common task

  5. continuous typing

  6. None
  7. but continuous reading Jono Witts © 2008

  8. Each written line is read at least 1 time Gavin

    St. Ours © 20013
  9. • 300 dev days • 14633 LOC • 634595 Changed

    lines • → 634595 / 14633 ~ 43 changed LOC / LOC
  10. For 1 final line, I read at least 43 lines.

  11. None
  12. How to Dive into a project Stuart Hamilton © 2008

  13. DO NOT Wander without a plan Felix Esteban © 2004

  14. Journey Needs Plan Peter Southwood © 20011

  15. Plan 1. Goal 2. Map 3. Equipment & Dive 4.

    Next? Peter Southwood © 20011
  16. 1. Goal Peter Southwood © 20011

  17. ➔ fix a bug ➔ implement a feature ➔ write

    some doc ➔ style the code ➔ refactor a small piece of code ➔ simply use it
  18. None
  19. http_proxy=http://127.0.0.1 \ ruby -e " require 'open-uri' open( 'http://google.com', proxy:

    nil ) "
  20. ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/net/http.rb:878:in `initialize': Connection refused - connect(2) (Errno::ECONNREFUSED) from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/net/http.rb:878:in `open'

    from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/net/http.rb:878:in `block in connect' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/timeout.rb:52:in `timeout' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/net/http.rb:877:in `connect' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/net/http.rb:862:in `do_start' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/net/http.rb:851:in `start' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/open-uri.rb:313:in `open_http' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/open-uri.rb:708:in `buffer_open' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/open-uri.rb:210:in `block in open_loo from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/open-uri.rb:208:in `catch' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/open-uri.rb:208:in `open_loop' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/open-uri.rb:149:in `open_uri' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/open-uri.rb:688:in `open' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/open-uri.rb:34:in `open' from -e:1:in `<main>'
  21. 2. Map Peter Southwood © 20011

  22. Find the Map ➔ Repository URI ➔ Project URI ➔

    Intro Doc: README
  23. Find the Map • https://github.com/ruby/ruby • https://bugs.ruby-lang.org/ • https://github.com/ruby/ruby/blob/trunk/ README.md

  24. Find the Legend

  25. a Legend

  26. Find the Legend ➔ CONTRIBUTING Guideline

  27. Find the Legend ➔ CONTRIBUTING Guideline https://github.com/ruby/ruby/blob/trunk/ CONTRIBUTING.md

  28. Find the Legend ➔ CONTRIBUTING Guideline https://github.com/ruby/ruby/blob/trunk/ CONTRIBUTING.md

  29. Find the Legend ➔ CONTRIBUTING Guideline ➔ Directory Structure

  30. Find the Legend ➔ CONTRIBUTING Guideline ➔ Directory Structure

  31. Find the Legend ➔ CONTRIBUTING Guideline ➔ Directory Structure ➔

    Talk to other contributors, maintainers
  32. Find the Legend ➔ CONTRIBUTING Guideline ➔ Directory Structure ➔

    Talk to other contributors, maintainers ➔ Participate to the dev meeting
  33. If blocked? ➔ ASK!

  34. Boris Dimitrov © 20009 3. Equipment & Dive

  35. Get a Toolbelt ➔ Pick an Editor

  36. xkcd

  37. Get a Toolbelt ➔ Pick an Editor ➔ Bundle

  38. $ cd GitLab $ bundle viz

  39. $ cd GitLab $ bundle viz

  40. $ cd GitLab $ bundle viz

  41. Get a Toolbelt ➔ Pick an Editor ➔ Bundle ➔

    Run Tests
  42. Get a Toolbelt ➔ Pick an Editor ➔ Bundle ➔

    Run Tests ➔ Others
  43. Get a Toolbelt ➔ ST3 ➔ Apply a Patch &

    Use it ➔ MRI WIKI DeveloperHowto ➔ Sign up to Ruby Redmine
  44. Use your Toolbelt ➔ Run the App & Use it

  45. http_proxy=http://127.0.0.1 \ ruby -e " require 'open-uri' open( 'http://google.com', proxy:

    nil ) "
  46. Use your Toolbelt ➔ Run the App & Use it

    ➔ Read the Code
  47. $ wc -l *.rb 1001 common.rb 257 ftp.rb 1676 generic.rb

    106 http.rb 22 https.rb 260 ldap.rb 20 ldaps.rb 280 mailto.rb 3622 total open-uri
  48. ➔ Find proxy management ➔ Understand how open_http works ➔

    Spot the bug URI
  49. ➔ Trace the Calls Dive into URI

  50. ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/net/http.rb:878:in `initialize': Connection refused - connect(2) (Errno::ECONNREFUSED) from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/net/http.rb:878:in `open'

    from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/net/http.rb:878:in `block in connect' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/timeout.rb:52:in `timeout' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/net/http.rb:877:in `connect' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/net/http.rb:862:in `do_start' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/net/http.rb:851:in `start' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/open-uri.rb:313:in `open_http' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/open-uri.rb:708:in `buffer_open' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/open-uri.rb:210:in `block in open_loo from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/open-uri.rb:208:in `catch' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/open-uri.rb:208:in `open_loop' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/open-uri.rb:149:in `open_uri' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/open-uri.rb:688:in `open' from ~/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/open-uri.rb:34:in `open' from -e:1:in `<main>'
  51. def get_call_graph_on scope = {} trace = TracePoint.new(:call, :line) do

    |tp| case tp.event when :call then puts "#{tp.path}:#{tp.lineno} #{tp.defined_class}::#{tp.method_id} called from " \ "#{scope[:path]}:#{scope[:lineno]} #{scope[:class]}::#{scope[:method_id]}" when :line then scope = { event: :line, lineno: tp.lineno, path: tp.path, class: tp.defined_class, method_id: tp.method_id } end end trace.enable yield trace.disable end
  52. scope = {} trace = TracePoint.new(:call, :line) do |tp| case

    tp.event # ... end end
  53. trace = TracePoint.new(:call, :line) do |tp| case tp.event when :call

    then puts "#{tp.path}:#{tp.lineno} ..." \ "#{scope[:path]}:#{scope[:lineno]} ..." # ... end end
  54. # ... when :line then scope = { event: :line,

    lineno: tp.lineno, path: tp.path, class: tp.defined_class, method_id: tp.method_id }
  55. trace.enable yield trace.disable

  56. require 'open-uri' get_call_graph_on do open( 'http://google.com', proxy: nil ) end

  57. None
  58. require 'open-uri' require 'trace_calls' TraceCalls::on do open('http://google.com', proxy: nil) end

    puts TraceCalls::root
  59. +-Kernel::open +-#<Class:URI>::parse called from Kernel::open at 33 | \-URI::Parser::parse called

    from #<Class:URI>::parse at 747 | | +-URI::Parser::split called from URI::Parser::parse at 211 | | +-#<Class:URI>::scheme_list called from URI::Parser::parse at 213 | | +-#<Class:URI>::scheme_list called from URI::Parser::parse at 214 | | \-URI::HTTP::initialize called from URI::Parser::parse at 660 | | | \-URI::Generic::initialize called from URI::HTTP::initialize at 84 | | | | +-URI::Generic::set_scheme called from URI::Generic::initialize at 203 | | | | +-URI::Generic::set_userinfo called from URI::Generic::initialize at 204 | | | | | \-URI::Generic::split_userinfo called from URI::Generic::set_userinfo at 525 | | | | +-URI::Generic::set_host called from URI::Generic::initialize at 205 | | | | +-URI::Generic::set_port called from URI::Generic::initialize at 206 | | | | +-URI::Generic::set_path called from URI::Generic::initialize at 207 | | | | +-URI::Generic::set_query called from URI::Generic::initialize at 208 | | | | +-URI::Generic::set_opaque called from URI::Generic::initialize at 209 | | | | +-URI::Generic::set_registry called from URI::Generic::initialize at 210 | | | | +-URI::Generic::set_fragment called from URI::Generic::initialize at 211 | | | | +-URI::Generic::default_port called from URI::Generic::initialize at 220 | | | | | \-#<Class:URI::Generic>::default_port called from URI::Generic::default_port at | | | | +-URI::Generic::default_port called from URI::Generic::initialize at 220 | | | | | \-#<Class:URI::Generic>::default_port called from URI::Generic::default_port at | | | | \-URI::Generic::set_port called from URI::Generic::initialize at 30
  60. +-Kernel::open +-#<Class:URI>::parse called from Kernel::open at 33 | \-URI::Parser::parse called

    from #<Class:URI>::parse at 747 | | +-URI::Parser::split called from URI::Parser::parse at 211 | | +-#<Class:URI>::scheme_list called from URI::Parser::parse at 213 | | +-#<Class:URI>::scheme_list called from URI::Parser::parse at 214 | | \-URI::HTTP::initialize called from URI::Parser::parse at 660 | | | \-URI::Generic::initialize called from URI::HTTP::initialize at 84 | | | | +-URI::Generic::set_scheme called from URI::Generic::initialize at 203 | | | | +-URI::Generic::set_userinfo called from URI::Generic::initialize at 204 | | | | | \-URI::Generic::split_userinfo called from URI::Generic::set_userinfo at 525 | | | | +-URI::Generic::set_host called from URI::Generic::initialize at 205 | | | | +-URI::Generic::set_port called from URI::Generic::initialize at 206 | | | | +-URI::Generic::set_path called from URI::Generic::initialize at 207 | | | | +-URI::Generic::set_query called from URI::Generic::initialize at 208 | | | | +-URI::Generic::set_opaque called from URI::Generic::initialize at 209 | | | | +-URI::Generic::set_registry called from URI::Generic::initialize at 210 | | | | +-URI::Generic::set_fragment called from URI::Generic::initialize at 211 | | | | +-URI::Generic::default_port called from URI::Generic::initialize at 220 | | | | | \-#<Class:URI::Generic>::default_port called from URI::Generic::default_port at | | | | +-URI::Generic::default_port called from URI::Generic::initialize at 220 | | | | | \-#<Class:URI::Generic>::default_port called from URI::Generic::default_port at | | | | \-URI::Generic::set_port called from URI::Generic::initialize at 30
  61. +-Kernel::open +-#<Class:URI>::parse called from Kernel::open at 33 | \-URI::Parser::parse called

    from #<Class:URI>::parse at 747 | | +-URI::Parser::split called from URI::Parser::parse at 211 | | +-#<Class:URI>::scheme_list called from URI::Parser::parse at 213 | | +-#<Class:URI>::scheme_list called from URI::Parser::parse at 214 | | \-URI::HTTP::initialize called from URI::Parser::parse at 660 | | | \-URI::Generic::initialize called from URI::HTTP::initialize at 84 | | | | +-URI::Generic::set_scheme called from URI::Generic::initialize at 203 | | | | +-URI::Generic::set_userinfo called from URI::Generic::initialize at 204 | | | | | \-URI::Generic::split_userinfo called from URI::Generic::set_userinfo at 525 | | | | +-URI::Generic::set_host called from URI::Generic::initialize at 205 | | | | +-URI::Generic::set_port called from URI::Generic::initialize at 206 | | | | +-URI::Generic::set_path called from URI::Generic::initialize at 207 | | | | +-URI::Generic::set_query called from URI::Generic::initialize at 208 | | | | +-URI::Generic::set_opaque called from URI::Generic::initialize at 209 | | | | +-URI::Generic::set_registry called from URI::Generic::initialize at 210 | | | | +-URI::Generic::set_fragment called from URI::Generic::initialize at 211 | | | | +-URI::Generic::default_port called from URI::Generic::initialize at 220 | | | | | \-#<Class:URI::Generic>::default_port called from URI::Generic::default_port at | | | | +-URI::Generic::default_port called from URI::Generic::initialize at 220 | | | | | \-#<Class:URI::Generic>::default_port called from URI::Generic::default_port at | | | | \-URI::Generic::set_port called from URI::Generic::initialize at 30
  62. \-OpenURI::OpenRead::open called from Kernel::open at 34 | \-#<Class:OpenURI>::open_uri called from

    OpenURI::OpenRead::open at 688 | | +-#<Class:OpenURI>::scan_open_optional_arguments called from #<Class:OpenURI>::open_ur | | +-#<Class:OpenURI>::check_options called from #<Class:OpenURI>::open_uri at 136 | | \-#<Class:OpenURI>::open_loop called from #<Class:OpenURI>::open_uri at 149 | | | +-OpenURI::Buffer::initialize called from #<Class:OpenURI>::open_loop at 209 | | | +-URI::HTTP::buffer_open called from #<Class:OpenURI>::open_loop at 195 | | | | \-#<Class:OpenURI>::open_http called from URI::HTTP::buffer_open at 708 | | | | | +-URI::Generic::userinfo called from #<Class:OpenURI>::open_http at 259 | | | | | +-Kernel::require called from #<Class:OpenURI>::open_http at 267 | | | | | | +-MonitorMixin::mon_enter called from Kernel::require at 39 | | | | | | +-#<Class:Gem>::find_unresolved_default_spec called from Kernel::require at 43 | | | | | | | \-#<Class:Gem>::suffixes called from #<Class:Gem>::find_unresolved_default_s | | | | | | +-#<Class:Gem::Specification>::unresolved_deps called from Kernel::require at | | | | | | +-#<Class:Gem::Specification>::stubs called from Kernel::require at 63 | | | | | | +-Gem::StubSpecification::activated? called from Kernel::require at 64 | | | | | | | \-Gem::StubSpecification::name called from Gem::StubSpecification::activated | | | | | | +-Gem::StubSpecification::activated? called from Kernel::require at 64 | | | | | | | \-Gem::StubSpecification::name called from Gem::StubSpecification::activated | | | | | | +-Gem::StubSpecification::activated? called from Kernel::require at 64 | | | | | | | \-Gem::StubSpecification::name called from Gem::StubSpecification::activated | | | | | | +-Gem::StubSpecification::activated? called from Kernel::require at 64
  63. \-OpenURI::OpenRead::open called from Kernel::open at 34 | \-#<Class:OpenURI>::open_uri called from

    OpenURI::OpenRead::open at 688 | | +-#<Class:OpenURI>::scan_open_optional_arguments called from #<Class:OpenURI>::open_ur | | +-#<Class:OpenURI>::check_options called from #<Class:OpenURI>::open_uri at 136 | | \-#<Class:OpenURI>::open_loop called from #<Class:OpenURI>::open_uri at 149 | | | +-OpenURI::Buffer::initialize called from #<Class:OpenURI>::open_loop at 209 | | | +-URI::HTTP::buffer_open called from #<Class:OpenURI>::open_loop at 195 | | | | \-#<Class:OpenURI>::open_http called from URI::HTTP::buffer_open at 708 | | | | | +-URI::Generic::userinfo called from #<Class:OpenURI>::open_http at 259 | | | | | +-Kernel::require called from #<Class:OpenURI>::open_http at 267 | | | | | | +-MonitorMixin::mon_enter called from Kernel::require at 39 | | | | | | +-#<Class:Gem>::find_unresolved_default_spec called from Kernel::require at 43 | | | | | | | \-#<Class:Gem>::suffixes called from #<Class:Gem>::find_unresolved_default_s | | | | | | +-#<Class:Gem::Specification>::unresolved_deps called from Kernel::require at | | | | | | +-#<Class:Gem::Specification>::stubs called from Kernel::require at 63 | | | | | | +-Gem::StubSpecification::activated? called from Kernel::require at 64 | | | | | | | \-Gem::StubSpecification::name called from Gem::StubSpecification::activated | | | | | | +-Gem::StubSpecification::activated? called from Kernel::require at 64 | | | | | | | \-Gem::StubSpecification::name called from Gem::StubSpecification::activated | | | | | | +-Gem::StubSpecification::activated? called from Kernel::require at 64 | | | | | | | \-Gem::StubSpecification::name called from Gem::StubSpecification::activated | | | | | | +-Gem::StubSpecification::activated? called from Kernel::require at 64
  64. \-OpenURI::OpenRead::open called from Kernel::open at 34 | \-#<Class:OpenURI>::open_uri called from

    OpenURI::OpenRead::open at 688 | | +-#<Class:OpenURI>::scan_open_optional_arguments called from #<Class:OpenURI>::open_ur | | +-#<Class:OpenURI>::check_options called from #<Class:OpenURI>::open_uri at 136 | | \-#<Class:OpenURI>::open_loop called from #<Class:OpenURI>::open_uri at 149 | | | +-OpenURI::Buffer::initialize called from #<Class:OpenURI>::open_loop at 209 | | | +-URI::HTTP::buffer_open called from #<Class:OpenURI>::open_loop at 195 | | | | \-#<Class:OpenURI>::open_http called from URI::HTTP::buffer_open at 708 | | | | | +-URI::Generic::userinfo called from #<Class:OpenURI>::open_http at 259 | | | | | +-Kernel::require called from #<Class:OpenURI>::open_http at 267 | | | | | | +-MonitorMixin::mon_enter called from Kernel::require at 39 | | | | | | +-#<Class:Gem>::find_unresolved_default_spec called from Kernel::require at 43 | | | | | | | \-#<Class:Gem>::suffixes called from #<Class:Gem>::find_unresolved_default_s | | | | | | +-#<Class:Gem::Specification>::unresolved_deps called from Kernel::require at | | | | | | +-#<Class:Gem::Specification>::stubs called from Kernel::require at 63 | | | | | | +-Gem::StubSpecification::activated? called from Kernel::require at 64 | | | | | | | \-Gem::StubSpecification::name called from Gem::StubSpecification::activated | | | | | | +-Gem::StubSpecification::activated? called from Kernel::require at 64 | | | | | | | \-Gem::StubSpecification::name called from Gem::StubSpecification::activated | | | | | | +-Gem::StubSpecification::activated? called from Kernel::require at 64 | | | | | | | \-Gem::StubSpecification::name called from Gem::StubSpecification::activated | | | | | | +-Gem::StubSpecification::activated? called from Kernel::require at 64
  65. | | | | | +-URI::Generic::hostname called from #<Class:OpenURI>::open_http at

    278 | | | | | +-URI::HTTP::request_uri called from #<Class:OpenURI>::open_http at 280 | | | | | | \-URI::Generic::path_query called from URI::HTTP::request_uri at 96 | | | | | +-#<Class:Net::HTTP>::new called from #<Class:OpenURI>::open_http at 291 | | | | | | +-Net::HTTP::initialize called from #<Class:Net::HTTP>::new at 609 | | | | | | \-#<Class:Net::HTTP>::proxy_class? called from #<Class:Net::HTTP>::new at 611 | | | | | +-Net::HTTP::start called from #<Class:OpenURI>::open_http at 313 | | | | | | +-Net::HTTP::do_start called from Net::HTTP::start at 851 | | | | | | | \-Net::HTTP::connect called from Net::HTTP::do_start at 862 | | | | | | | | +-Net::HTTP::proxy? called from Net::HTTP::connect at 868 | | | | | | | | | \-Net::HTTP::proxy_uri called from Net::HTTP::proxy? at 1014 | | | | | | | | | | +-Kernel::URI called from Net::HTTP::proxy_uri at 1027 | | | | | | | | | | | \-#<Class:URI>::parse called from Kernel::URI at 994 | | | | | | | | | | | | \-URI::Parser::parse called from #<Class:URI>::parse at 747 | | | | | | | | | | | | | +-URI::Parser::split called from URI::Parser::parse at 211 | | | | | | | | | | | | | +-#<Class:URI>::scheme_list called from URI::Parser::parse at 21 | | | | | | | | | | | | | +-#<Class:URI>::scheme_list called from URI::Parser::parse at 21 | | | | | | | | | | | | | \-URI::HTTP::initialize called from URI::Parser::parse at 660 | | | | | | | | | | | | | | \-URI::Generic::initialize called from URI::HTTP::initialize a | | | | | | | | | | | | | | | +-URI::Generic::set_scheme called from URI::Generic::initial | | | | | | | | | | | | | | | +-URI::Generic::set_userinfo called from URI::Generic::initi
  66. http = klass.new( target_host, target_port )

  67. ➔ Trace the Calls ➔ Search the Source Dive into

    URI
  68. Search the Source $ grep -nr "open_http". ./open-uri.rb:253: def OpenURI.open_http(buf,

    target, proxy, option ./open-uri.rb:708: OpenURI.open_http(buf, self, proxy, options) ./open-uri.rb:717: OpenURI.open_http(buf, self, proxy, option
  69. Search the Source $ grep -rn "class HTTP " .

    ./net/http.rb:384: class HTTP < Protocol ./uri/http.rb:22: class HTTP < Generic
  70. Search the Source $ EDITOR=subl \ bundle open gitlab_omniauth-ldap

  71. ➔ Trace the Calls ➔ Search the Source ➔ Get

    Context with a Test Dive into URI
  72. Get Context with a Test context "without params['markdown_img']" do it

    "returns an error" do TraceCalls::on do post :upload_image, id: project.to_param, format: :json end puts TraceCalls::root expect(response.status).to eq(422) end end
  73. Use your Toolbelt ➔ Run the App & Use it

    ➔ Read the Code ➔ Edit the Code
  74. http = proxy ? klass.new(target_host, target_port) : klass.new(target_host, target_port, nil)

  75. Use your Toolbelt ➔ Run the App & Use it

    ➔ Read & Edit the Code ➔ Report a bug ➔ Submit a contribution
  76. None
  77. If blocked? ➔ ASK!

  78. 4. Next? Peter Southwood © 20011

  79. How long? ➔ Keep it reasonable at first ➔ From

    ½ to 1 day
  80. Find Next Goal ➔ Fix a bug ➔ Develop a

    feature ➔ Run Static Analysis Tools ➔ Review a PullRequest
  81. Iterate ➔ :) Jean-Marc Kuffer © 2007

  82. If blocked? ➔ ASK!

  83. Outro 1. Goal 2. Map 3. Equipment & Dive

  84. ? _toch toch