Rails と Rack について

この記事は、「フィヨルドブートキャンプ Part 2 Advent Calendar 2023」15日目の記事です。

フィヨルドブートキャンプ Part 2 Advent Calendar 2023 - Adventar

Part 1はこちらになります。

フィヨルドブートキャンプ Part 1 Advent Calendar 2023 - Adventar

前回は、dawaさんの「テストとは。最初のイシューでエラーに遭遇した話。」でした。

この記事は、Rails.applicationを調べた過程で学んだことをまとめたものになります。

この記事で伝えたいことは、次の2つです。

  • RailsはRackアプリケーションである
  • Rackとは「WebサーバーとRuby製Webアプリケーションがやりとりを行うための規約」

では、私がRails.applicationの存在を知ったきっかけから始めましょう。

Rails.applicationとの出会い

以前Railsコンソール上でpolymorphic_urlの挙動を調べようとしたことがあり、その時にappメソッドの存在を知りました。

Rails::ConsoleMethods

このメソッドを利用すると、次の様に名前付きルーティングヘルパーを呼び出したり、リクエストを投げることが可能です。

irb(main):001> app.books_path
=> "/books"

irb(main):002> app.polymorphic_url(Book.new)
=> "http://www.example.com/books"

irb(main):003> app.get "http://localhost:3000/books"
Started GET "/books" for 127.0.0.1 at 2023-12-10 10:53:13 +0900
  ActiveRecord::SchemaMigration Load (0.5ms)  SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
Processing by BooksController#index as HTML
  Rendering layout layouts/application.html.erb
  Rendering books/index.html.erb within layouts/application
  Book Load (0.6ms)  SELECT "books".* FROM "books"
  Rendered books/_book.html.erb (Duration: 0.3ms | Allocations: 275)
  Rendered books/_book.html.erb (Duration: 0.0ms | Allocations: 54)
  Rendered books/_book.html.erb (Duration: 0.0ms | Allocations: 53)
  Rendered books/_book.html.erb (Duration: 0.0ms | Allocations: 53)
  Rendered books/index.html.erb within layouts/application (Duration: 2.7ms | Allocations: 2243)
Everything's up-to-date. Nothing to do
  Rendered layout layouts/application.html.erb (Duration: 15.9ms | Allocations: 15124)
Completed 200 OK in 30ms (Views: 17.9ms | ActiveRecord: 0.2ms | Allocations: 19944)


=> 200

RailsAPIにはappメソッドの説明もありました。

reference the global “app” instance, created on demand. To recreate the instance, pass a non-false value as the parameter.

「グローバルな"app"インスタンスを参照する」とあります。 グローバルな"app"インスタンスってなんだ?と気になり、コードを見てみることに。

module Rails
  module ConsoleMethods
    # reference the global "app" instance, created on demand. To recreate the
    # instance, pass a non-false value as the parameter.
    def app(create = false)
      @app_integration_instance = nil if create
      @app_integration_instance ||= new_session
    end

    # create a new session. If a block is given, the new session will be yielded
    # to the block before being returned.
    def new_session
      app = Rails.application
      session = ActionDispatch::Integration::Session.new(app)

      # This makes app.url_for and app.foo_path available in the console
      session.extend(app.routes.url_helpers)
      session.extend(app.routes.mounted_helpers)

      session
    end

rails/railties/lib/rails/console/app.rb at 6b93fff8af32ef5e91f4ec3cfffb081d0553faf0 · rails/rails

appメソッド内で呼ばれているnew_sessionメソッドは、Rails.applicationを引数にしてActionDispatch::Integration::Sessionインスタンス化したものを返しています。

ActionDispatch::Integration::Sessionインスタンスは、テストプロセスによって順次実行されるリクエストとレスポンスのセットを表す、とのこと。

Rails.applicationに関しては、Railsガイドに説明がありました。

Rails.applicationは、Railsアプリケーションにおける主要なRackアプリケーションです。Rackに準拠したWebサーバーで、Railsアプリケーションを提供するには、Rails.applicationオブジェクトを使う必要があります。

Rails と Rack - Railsガイド

Rack って聞いたことはあるけど何なんだろう?と気になったので、Rack について調べてみることにしました。

Rackとは?

Rackについて簡単にまとめると、次の様になります。

「Rackとは、WebサーバーとRuby製Webアプリケーションがやりとりを行うための規約」のこと。

1PumaやUnicornといったWebサーバーとRailsアプリケーションがやりとりを行えるのは、RailsがRackの規約に沿って実装されているからなのです。

WebサーバーとRailsアプリケーションのやり取りの中身は、Rackアプリケーションの仕様から知ることができます。

Rackアプリケーションとは、以下の条件を満たすRubyオブジェクトのことを言います。

  • environmentという引数一つを受け取る、callメソッドを呼び出すことができる。
  • status headers bodyという三つの値を含む、凍結されていない配列を返す

rack/SPEC.rdoc at main · rack/rack

Webサーバーは「envハッシュ」と呼ばれるHTTPリクエストの情報が格納されたハッシュを用意し、それを引数としてcallメソッドを呼び出します。

Railsアプリケーションはcallメソッドの返り値として、以下の三つの値を格納した配列をWebサーバーに返します。

  • status : HTTPステータスコードを表す数値
  • headers : レスポンスヘッダーを含むハッシュ
  • body : レスポンスのボディとなるもので、通常は文字列の配列など

では、実際にどのような値が返ってくるのか確認してみましょう!

コンソール上で実験

Railsコンソール上で、Rails.applicationに対しcall`メソッドを呼び出してみます。

下準備として、rails new helloでアプリを新規作成し、/helloにアクセスするとHello, World!と表示される様にします。

helloと表示するアプリの画像

callメソッドに渡す「envハッシュは」、rack gemのRack::MockRequest.env_forを利用して用意します。

irb(main):001> env = Rack::MockRequest.env_for('http://localhost:3000/hello')
=> 
{"rack.input"=>#<StringIO:0x00000001077b1760>,
...

そのままcallメソッドに渡すとBlocked hostと言われてしまうため、「envハッシュ」にHTTP_HOSTヘッダーを追加します。

irb(main):002> env['HTTP_HOST'] = 'localhost'
=> "localhost"

では、「envハッシュ」を引数にRails.applicationに対してcallメソッドを呼び出します。

irb(main):003> status, headers, body = Rails.application.call(env)
Started GET "/hello" for  at 2023-12-10 15:31:40 +0900
Cannot render console from ! Allowed networks: 127.0.0.0/127.255.255.255, ::1
  ActiveRecord::SchemaMigration Load (0.3ms)  SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
Processing by HellosController#index as HTML
  Rendering layout layouts/application.html.erb
  Rendering hellos/index.html.erb within layouts/application
  Rendered hellos/index.html.erb within layouts/application (Duration: 0.2ms | Allocations: 209)
Everything's up-to-date. Nothing to do
  Rendered layout layouts/application.html.erb (Duration: 12.8ms | Allocations: 13220)
Completed 200 OK in 20ms (Views: 14.4ms | ActiveRecord: 0.0ms | Allocations: 17125)


MiniProfiler storage failure: no implicit conversion of nil into String
=> 
[200,

Completed 200 OKとあるので、このリクエストは成功した様ですね。

返り値を確認してみると、HTTPステータス、レスポンスヘッダー、レスポンスボディに対応した値が格納されていることがわかります。

irb(main):004> status
=> 200

irb(main):005> headers
=> 
{"x-frame-options"=>"SAMEORIGIN",
 "x-xss-protection"=>"1; mode=block",
 "x-content-type-options"=>"nosniff",
 "x-download-options"=>"noopen",
 "x-permitted-cross-domain-policies"=>"none",
 "referrer-policy"=>"strict-origin-when-cross-origin",
 "link"=>
  "</assets/application.debug-ead91a25923de99455378da7f1f1bb5a6839a249af27af911ec2b81709b046b7.css>; rel=preload; as=style; nopush,</packs/js/application-d4389a205e3a305a430b.js>; rel=preload; as=script; nopush",
 "content-type"=>"text/html; charset=utf-8",
 "etag"=>"W/\"cd556a718561e5e4f0c288ed64067335\"",
 "cache-control"=>"max-age=0, private, must-revalidate",
 "set-cookie"=>
  ["_hello_session=oNirYlMTT8MTrQFwUPjyqDIKnSZbe1SNGXcQMfesZ5FqJkYFwgpcfBeeGJ7sinDzFkrpGj40DUfjR9G8fOGHTys7Rx3fxmEa06ErCuNjge%2Bu1hWdjLpryWkBIElXgQF9dPBKvRJ2J2m50%2Bvhm5PRHfyy9Dp%2BN6fFvRxnuNtz2igBGQbqdVt9iNnW5Fl4zNlOM4516%2FQxiSQvtdAUhjGMRIQwwU3dHkjGMiYP0jZCPZCKq%2BoqlNDGke19aXZpiu5lealK4wA%2Fu3KIGv1KoEmuXla2tQcPLw%3D%3D--%2BaxhXWvOw%2FzjtOZL--bigBnV371hxh0QYi0EMAUw%3D%3D; path=/; httponly; SameSite=Lax",
   "__profilin=p%3Dt; path=/; httponly; SameSite=Lax"],
 "x-request-id"=>"f404e8ff-5504-4808-8e44-a87547285fab",
 "x-runtime"=>"0.048442"}

irb(main):006> headers
=>
#<Rack::BodyProxy:0x0000000107adc390
 @block=#<Proc:0x0000000107adc368 /Users/kashiyamashintarou/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/actionpack-7.1.2/lib/action_dispatch/middleware/executor.rb:15>,
 途中省略...
            @response=
            #<ActionDispatch::Response:0x0000000107d32630
             途中省略...
             @stream=
              #<ActionDispatch::Response::Buffer:0x0000000106dc0ff8
               @buf=
                ["<!DOCTYPE html>\n<html>\n  <head>\n    <title>Hello</title>\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n    <meta name=\"csrf-param\" content=\"authenticity_token\" />\n<meta name=\"csrf-token\" content=\"TVqQW_tF8fRvPSePyWVN10xc4vV8Fc-nwR8ffja8FhyYM_x59CsJ8KRxawd2VPBiL30Tf0xhAzxQarpFGZONjQ\" />\n    \n
\n    <link rel=\"stylesheet\" href=\"/assets/application.debug-ead91a25923de99455378da7f1f1bb5a6839a249af27af911ec2b81709b046b7.css\" media=\"all\" data-turbolinks-track=\"reloa
d\" />\n    <script src=\"/packs/js/application-d4389a205e3a305a430b.js\" data-turbolinks-track=\"reload\"></script>\n  </head>\n\n  <body>\n    <p>Hello World!</p>\n\n  </body>\
n</html>\n"],

Rackアプリケーションの仕様に基づいて、Rails.applicationがstatus・headers・bodyを返すことが分かりました👍

Rails.applicationのクラス

Rails.applicationはRackアプリケーションであることが分かりましたが、それはどの様なオブジェクトなのでしょうか?

クラスを確認してみます。

irb(main):001> Rails.application.class
=> Hello::Application

Hello::Applicationと表示されましたね。

Railsの起動プロセスについて調べてみると、次の様な記述がありました。

The application is also responsible for setting up and executing the booting process. From the moment you require config/application.rb in your app, the booting process goes like this:

  1. require "config/boot.rb" to set up load paths.
  2. require railties and engines.
  3. Define Rails.application as class MyApp::Application < Rails::Application.

Rails::Application

アプリケーション内でconfig/application.rbrequireした瞬間から起動プロセスが実行され、3番目に「Rails.applicationMyApp::Application < Rails::Applicationとして定義する」とあります。

起動プロセスを通じて、Rails.applicationアプリケーション名::Applicationとなることが分かりました。

config/application.rbを確認してみると、アプリケーション名::Applicationとなっていますね。

require_relative "boot"

require "rails/all"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module Hello
  class Application < Rails::Application
  #...

試しに名前空間を変更してみると、Rails.applicationのクラス名も変更されることが確認できます。

module GoodBye # HelloからGoodByeに変更
  class Application < Rails::Application
irb(main):001> Rails.application.class
=> GoodBye::Application

最後に、Rails.applicationの継承関係を見てみましょう。

祖先にはRails::EngineRails::Railtieというクラスが存在していることが分かりました。

irb(main):001> Rails.application.class
=> Hello::Application
irb(main):002> Rails.application.class.superclass
=> Rails::Application
irb(main):003> Rails.application.class.superclass.superclass
=> Rails::Engine
irb(main):004> Rails.application.class.superclass.superclass.superclass
=> Rails::Railtie
irb(main):005> Rails.application.class.superclass.superclass.superclass.superclass
=> Object

Rails.applicationの継承関係

終わりに

色々と調べる中で、Railsアプリケーションに対する解像度が少し上がった気がします。

Railsの中身を調べることは楽しいですね(コードを読んだり調べたりするのは大変ですが😇)。

今回の調査で十分理解できなかった箇所(Railsの起動プロセスの詳細など)は、レベルアップしてから再び調べることにします。

これからも自分の好奇心を大切に、興味を持ったことをどんどん深掘りしていきたいと思います!

参考文献

小ネタ : 開発環境でRailsアプリの日付を変更する

はじめに

開発環境で特定の日付に移動したい時ってありませんか?

私は チーム開発1のプラクティスを行っていて、「祝日の場合ダッシュボードの表示を変更したい」というIssueに取り組みました。

実装を行って祝日の表示確認を行う際、アプリ内の日付を祝日に変更する必要に迫られたのです。

以下の方法で開発環境の日付を一時的に変更することが可能ですので、今回ご紹介します。

変更方法

config/environments/development.rbに以下を追加して、アプリを再起動する。

require "active_support/core_ext/integer/time" # 追加
require "active_support/testing/time_helpers"  # 追加
include ActiveSupport::Testing::TimeHelpers    # 追加
 
Rails.application.configure do
  config.after_initialize do
    travel_to Time.zone.parse('移動したい日付') # 追加
  end
end

試しに今日の日付を表示する簡単なアプリを作成して、アプリ内の日付が変更されているか確認します。

祝日であるかどうかの確認のために、holiday-jpというGemを利用しています。

(app/views/home/index.slim)

h2 今日の日付

p 
  | 今日の日付は"#{l(Date.current, format: :long)}"です。

p 
  | #{HolidayJp.holiday?(Date.current) ? '今日は祝日です!🥳' : '今日は祝日ではありません🫠'}

h2 現在時刻

p
  | 現在時刻は"#{l(Time.current)}"です。

普通にアプリを起動した場合(日付は9/18日)。

アプリ内の日付を変更した場合(日付を9/25に変更)。

日付が変わっていることが分かります!

何を行っているの?

日付の変更を行うには、travel_toメソッドを利用します。 通常travel_toはテスト内でしか使用することができないため、travel_toを使う下準備として次のことを行います。

  1. travel_toが定義してあるファイル(active_support/testing/time_helpers)をrequireする
  2. モジュール(ActiveSupport::Testing::TimeHelpers)をincludeする

そして、config.after_initializeブロック内でtravel_toを実行し、変更したい日付を渡します。 config.after_initializeブロック内に記述されたコードは、Railsによってアプリケーションの初期化が完了した後に実行されます。

Rails アプリケーションを設定する - Railsガイド

travel_toは、Time.now Date.today DateTime.nowを引数で与えられた時間または日付を返すようにスタブ化することで、現在の時刻を特定の時刻に変更することができます。

スタブはテスト終了時に削除されます。

ActiveSupport::Testing::TimeHelpers

Time.currentとDate.currentについて

RailsAPIのtravel_toの頁に、次のような記述がありました。

Dates are taken as their timestamp at the beginning of the day in the application time zone. Time.current returns said timestamp, and Time.now its equivalent in the system time zone. Similarly, Date.current returns a date equal to the argument, and Date.today the date according to Time.now, which may be different. (Note that you rarely want to deal with Time.now, or Date.today, in order to honor the application time zone please always use Time.current and Date.current.)

簡単にまとめると、「Time.currentDate.currentはアプリケーションのタイムゾーンに応じた時刻と日付を返し、Time.nowDate,todayはシステムのタイムゾーンに応じた時刻と日付を返すため、アプリ内のタイムゾーンを尊重したい場合はTime.currentDate.currentを使いましょう」ということが書かれています。

では、このTime.currentDate.currentという2つのメソッドはどのように現在時刻や日付を返しているのでしょうか? 実装を見てみようと思います。

Time.current

Time.currentは次のようになっています。

    def current
      ::Time.zone ? ::Time.zone.now : ::Time.now
    end

https://github.com/rails/rails/blob/fc734f28e65ef8829a1a939ee6702c1f349a1d5a/activesupport/lib/active_support/core_ext/time/calculations.rb#L39

Time.zoneにはタイムゾーン情報が格納されており、これが存在する場合はTime.zone.nowを、存在しない場合はTime.nowを返しています。

config.time_zoneタイムゾーンを設定すると、Time.zoneは次のようになります。

irb(main):001:0> Time.zone
=> #<ActiveSupport::TimeZone:0x000000010eab9618 @name="Tokyo", @utc_offset=nil, @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>>

では、Time.zone.nowを確認しましょう。

    def now
      time_now.utc.in_time_zone(self)
    end

https://github.com/rails/rails/blob/fc734f28e65ef8829a1a939ee6702c1f349a1d5a/activesupport/lib/active_support/values/time_zone.rb#L507

最初にtime_nowメソッドが呼ばれています。 このメソッドはただTime.nowを読んでいるだけです。

      def time_now
        Time.now
      end

https://github.com/rails/rails/blob/fc734f28e65ef8829a1a939ee6702c1f349a1d5a/activesupport/lib/active_support/values/time_zone.rb#L597C1-L599C10

次のutcメソッドですが、おそらくRubyの組み込みライブラリであるTimeクラスのメソッドが呼ばれているのではないかと思います。

タイムゾーン協定世界時に設定します。 このメソッドを呼び出した後は時刻変換を協定世界時として行ないます。

https://docs.ruby-lang.org/ja/latest/method/Time/i/gmtime.html

最後にin_time_zoneメソッドが呼ばれています。 タイムゾーンが存在する場合はActiveSupport::TimeWithZoneインスタンスを作成していることが分かります。

    def in_time_zone(zone = ::Time.zone)
      time_zone = ::Time.find_zone! zone
      time = acts_like?(:time) ? self : nil

      if time_zone
        time_with_zone(time, time_zone)
      else
        time || to_time
      end
    end

    private
      def time_with_zone(time, zone)
        if time
          ActiveSupport::TimeWithZone.new(time.utc? ? time : time.getutc, zone)
        else
          ActiveSupport::TimeWithZone.new(nil, zone, to_time(:utc))
        end
      end

https://github.com/rails/rails/blob/a967d355c6fee9ad9b8bd115d43bc8b0fc207e7e/activesupport/lib/active_support/core_ext/date_and_time/zones.rb#L20

コンソール上で確認すると、確かにActiveSupport::TimeWithZoneインスタンスが返っていますね。 time_zone属性にタイムゾーンの情報も格納されていることもわかります。

irb(main):004:0> Time.current
=> Mon, 18 Sep 2023 14:41:00.422901000 JST +09:00

irb(main):005:0> Time.current.class
=> ActiveSupport::TimeWithZone

irb(main):006:0> Time.current.time_zone
=> #<ActiveSupport::TimeZone:0x000000010e90e908 @name="Tokyo", @utc_offset=nil, @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>>

Time.currentの実装を簡単ですが追ってみました。

途中でTime.nowメソッドを読んでおり、travel_toTime.nowをスタブ化するのでTime.currentの時間も変わるということが分かりましたね。

Date.current

ではDate.currentの方も見てみましょう。

Date.currentTime.currentと同じく、タイムゾーンが設定されていればTime.zone.todayを、そうでなければDate.todayを返します。

    def current
      ::Time.zone ? ::Time.zone.today : ::Date.today
    end

https://github.com/rails/rails/blob/fc734f28e65ef8829a1a939ee6702c1f349a1d5a/activesupport/lib/active_support/core_ext/date/calculations.rb#L48C1-L50C8

では、Time.zone.todayはどうなっているでしょうか。

    def today
      tzinfo.now.to_date
    end

https://github.com/rails/rails/blob/fc734f28e65ef8829a1a939ee6702c1f349a1d5a/activesupport/lib/active_support/values/time_zone.rb#L512

tzinfoが呼ばれていますね。

ActiveSupport::TimeZoneが初期化される時に@tzinfoに値が格納されます。

    attr_reader :name
    attr_reader :tzinfo

    def initialize(name, utc_offset = nil, tzinfo = nil)
      @name = name
      @utc_offset = utc_offset
      @tzinfo = tzinfo || TimeZone.find_tzinfo(name)
    end

https://github.com/rails/rails/blob/fc734f28e65ef8829a1a939ee6702c1f349a1d5a/activesupport/lib/active_support/values/time_zone.rb#L294-L305

TimeZone.find_tzinfo内ではTZInfo::Timezone.getが呼ばれています。

    def find_tzinfo(name)
      TZInfo::Timezone.get(MAPPING[name] || name)
    end

https://github.com/rails/rails/blob/fc734f28e65ef8829a1a939ee6702c1f349a1d5a/activesupport/lib/active_support/values/time_zone.rb#L205

TZInfo::Timezone.gettzinfoというGemにより提供されるもので、個別のタイムゾーンを表すTZInfo::Timezoneインスタンスを得ることができます。

A TZInfo::Timezone instance representing an individual time zone can be obtained with TZInfo::Timezone.get

https://github.com/tzinfo/tzinfo#example-usage

Time.zone.today内で呼ばれるtzinfoは、ActiveSupport::TimeZone初期化時に設定されたTZInfo::Timezoneインスタンスであることが分かりました。

tzinfo.nowは次のようになっています。 Time.nowを渡してto_localメソッドを呼び出していますね。

    def now
      to_local(Time.now)
    end

    def to_local(time)
      raise ArgumentError, 'time must be specified' unless time

      Timestamp.for(time) do |ts|
        TimestampWithOffset.set_timezone_offset(ts, period_for(ts).offset)
      end
    end

https://github.com/tzinfo/tzinfo/blob/9953fc092424d55deaea2dcdf6279943f3495724/lib/tzinfo/timezone.rb#L992

https://github.com/tzinfo/tzinfo/blob/9953fc092424d55deaea2dcdf6279943f3495724/lib/tzinfo/timezone.rb#L548

最後にto_dateメソッドを呼び出し、日付の文字列が返ってくるようになっています。

次のサンプルコードを実行してわかる通り、タイムゾーンに合わせた現在日時を取得していることが分かりますね。

irb(main):001:0> tz = TZInfo::Timezone.get('America/New_York')
=> #<TZInfo::DataTimezone: America/New_York>
irb(main):002:0> tz.now
=> 2023-09-18 02:25:04.614519 -0400
irb(main):003:0> tz.now.to_date
=> Mon, 18 Sep 2023

irb(main):004:0> tz_japan = TZInfo::Timezone.get('Asia/Tokyo')
=> #<TZInfo::DataTimezone: Asia/Tokyo>
irb(main):005:0> tz_japan.now
=> 2023-09-18 15:24:17.173699 +0900
irb(main):006:0> tz_japan.now.to_date
=> Mon, 18 Sep 2023

Date.currentの実装も簡単ですが追ってみました。 Date.currentも途中でTime.nowを読んでおり、travel_toの影響を受けることが分かりました。

終わりに

Railsアプリケーションの開発環境で日付を変更する方法と、Time.currentDate.currentについて書きました。

Rails内のコードを読むのは難しく完璧に理解はできませんでしたが、いい勉強になりました!

また気になるコードがあれば、コードリーディングに挑戦してみたいと思います💪

参考文献

RubyとRailsにおけるTime, Date, DateTime, TimeWithZoneの違い - Qiita


  1. フィヨルドブートキャンプの終盤にあるプラクティスで、フィヨルドブートキャンプアプリ内で振られたIssueの開発を行う、ということを行います。

コミット修正時に使う操作まとめ

かなり久しぶりの投稿となりました、cassy0220です。

前回の投稿から今までの間に、FBCで様々なプラクティスに取り組み、課題を提出してきました。

課題を提出する際、コミットを整理してから提出するようにしているのですが、コミットの修正を行う時に「この操作はこれで大丈夫かな?」と何度も調べてしまいます。

そこで、備忘録も兼ねて私がコミットを修正する際によく使う操作をまとめることにしました。

確認する操作

ファイルの変更差分を確認する

% git diff

コミットする前に変更差分を確認して、セルフレビューする癖をつけましょう(戒め)。

もし、ステージングに上げた変更差分を確認したい場合は、git diff --cachedで確認することが出来ます。

コミット履歴を確認する

% git log --oneline

git log --oneline で、コミット履歴を分かりやすく表示することが出来ます。

コミットのハッシュ値を確認したい時や、rebase 中にコミット履歴を確認したい時などによく使います。

特定のコミットの内容を確認する

% git show commit_hash

最後にファイル名を指定すると、指定したコミット内で指定したファイルのコミット内容が表示されます。

% git show commit_hash filename

コミットの指定はHEADで行うことも可能です。

% git show HEAD^

コミットを修正する操作

コミットを取り消す

% git reset

git resetコマンドを使うと、現在のHEADを指定された状態にリセットします。

※ HEADとは、一般的にそのブランチで最後のコミットを指します。

git resetコマンドには次のオプションがあり、巻き戻す内容が異なります。

次のファイルを例に、それぞれのオプションの実行結果をみていきます。

Image from Gyazo

  • --soft

HEADだけを動かし、コミットだけが消えます。ファイルはステージ上に残ります。

Image from Gyazo

git reset --soft HEAD^ でコミット取り消し → ファイルを修正 → コミットする」という流れをよくやります。

  • --mixed

HEADとステージを巻き戻します。ファイルの変更差分はそのまま残ります。

Image from Gyazo

  • --hard

HEAD、ステージ、ファイルの変更差分全てを元に戻します。

Image from Gyazo

コミットの順番を変更する

% git rebase -i commit_hash

次のコミット履歴を例に、順番を並び替えてみたいと思います。

Image from Gyazo

「add name」と「add age」のコミットを入れ替えてみましょう。

この場合、git rebase -iで指定するコミットハッシュは、「add name」のひとつ前のコミットを指定することに注意して下さい。

% git rebase -i 3fd37fd

すると、次のようなコミット編集画面が表示されます。

Image from Gyazo

この画面で、「add name」と「add age」のコミットの順番を入れ替えて、保存します。

Image from Gyazo

もしコミットを入れ替えてコンフリクトが発生した場合は、次のことを実行します。

  1. コンフリクトが発生した箇所を修正する
  2. git add git commitでコミットする
  3. git rebase --continueでリベースを続ける

Successfully rebased and updated refs/heads/main.と表示されれば完了です。

コミットの順番を確認してみると、入れ替わっていることが分かります。

Image from Gyazo

コミットをまとめる

% git rebase -i commit_hash

コミットをまとめるには、コミット入れ替えと同じくgit rebase -iを使います。

先ほどの例で、今度は「add age」と「add name」のコミットを1つにまとめてみましょう。

次のコマンドを実行して、コミット編集画面を開きます。

% git rebase -i 3fd37fd

コミット編集画面で、「add name」のコミットのpicksに変更します。

s(quash)を指定すると、指定したコミットとそのひとつ前のコミットを結合することができます。

Image from Gyazo

編集画面を保存すると、結合したコミットのコミットメッセージを編集する画面が開きます。

適当なコミットメッセージを作成して保存すれば、コミットの結合は完了です。

ログと変更差分を確認してみると、うまくいっていることが分かります。

Image from Gyazo

コミットの内容を分割する

% git rebase -i commit_hash
% git add -p

コミットの内容を分割する場合も、おなじみのgit rebase -iを利用します。

また、git add -pを使うと、変更差分の一部を指定してステージングすることが出来ます。

「コミットをまとめる」で「add age」と「add name」を1つのコミットにまとめましたが、今回はまとめたコミットをもとの別々のコミットに戻してみましょう。

次のコマンドを実行して、コミット編集画面を開きます。

% g rebase -i 3fd37fd 

コミット編集画面で、「add age and name」コミットのpickeに変更します。

Image from Gyazo

e(dit)を指定すると、指定したコミットで rebase が一時停止します。

Image from Gyazo

「add age and name」コミットを修正したいので、まずコミットを巻き戻し、ステージング前の状態に戻します。

% git reset --mixed HEAD^

次に、変更差分を指定してステージングを行いたいので、git add -pを実行します。

% git add -p sample.rb

すると、Stage this hunk [y,n,q,a,d,e,?]?(hunk(変更差分の塊のこと)をステージングする?)と聞かれるので、eを選択します。

Image from Gyazo

どの変更差分をステージングするか、編集画面が起動します。

Image from Gyazo

編集画面では、次のことを行います。

  • 追加差分をステージングしたくない場合は、行を削除する
  • 削除差分をステージングしたくない場合は、-を削除する

名前の追加だけをステージングしたいので、次のように編集して保存します。

Image from Gyazo

これで、指定した箇所だけをステージングすることが出来ました。

git diff --cachedで確認すると、先ほど指定した箇所がステージングされていることが分かります。 この内容で、git commitを行います。

Image from Gyazo

残りの変更差分もコミットすれば、コミットの分割は完了です。

最後に、まだ rebase の途中なのでgit rebase --continueして rebase を完了させます。

コミットログと変更差分を確認すると、うまくいっていることが分かります。

Image from Gyazo

Image from Gyazo

おわりに

FBCで勉強を始めた頃は git に対する苦手意識があったのですが、課題の提出時に何度もコミットの修正を行うことで、コミットに対する操作に慣れることができました。

これからの目標として、普段からきれいなコミットを積み上げることを大切にしつつ、問題が起きた時はすぐ修正できるようになっていきたいです。

参考

第6話 git reset 3種類をどこよりもわかりやすい図解で解説!【連載】マンガでわかるGit ~コマンド編~ - itstaffing エンジニアスタイル

コミットを整理してみよう|おだいり|note

[レポート] 『きれいなcommit, pull requestを知りたい/作りたい方のためのgit勉強会』に参加してきました | DevelopersIO

フィヨルドブートキャンプ 一ヶ月経過

こんにちは、プログラミング勉強中のカッシーです。

フィヨルドブートキャンプに入会して一ヶ月(正確には四週間)が経過しました。

今回は、この一ヶ月の振り返りと、反省点を書いていきたいと思います。

一ヶ月の学習内容

学習時間 : 182.5時間(一日あたり約6.5時間)

学習内容

学習による変化

  • 英語のリファレンスや記事を(苦労しながらでも)読む癖がついた。
  • 単にコピペするのではなく、コードの意味を理解する。

意識の変化としてはこの2つでしょうか。

英語を読むのは大変なのですが、公式リファレンス等大切な情報が英語で書かれている場合も多いので、頑張って読んでいます💦

一回調べた単語は記録しておいて、いつでも振り返れる様にしておいた方が良いかな。

「ただコピペするのではなく、きちんとコードの意味を理解して使うこと」という教訓は、プロを目指す上で大切にしていきたいと思います。

実践面での変化は、次の様なものになります。

  • ブラウザやターミナルの操作で、意識的にショートカットキーを使う。
  • Vimに対する恐怖感がなくなる。
  • パスの通し方やシェルの設定が何をしているのか分かる。
  • リモートサーバーにローカルのマシンでssh接続できる。
  • Gitの操作中に競合が発生しても、(以前よりは)冷静に対応できる。

独学の時はよく分からなかったコードやコマンドも、学習を進めていくうちに理解が進んで、苦手意識がなくなる過程は楽しかったです😁

ただ、もちろん学習が順風満帆に進むわけもなく、途中で壁にぶつかることもあります。自分も以前そうなって気持ちが萎えたことがありました。

そんな時は、メンターさんからもらった「色々と試行錯誤した経験は、必ず自分の糧になっている」というアドバイスを思い出して、気持ちを奮い立たせている様にしています。

「自分には無理かもな...」と沈んだ気持ちで挑むのと、「この問題を解決したら、また一ついい武器が手に入るぞ!」という前向きな気持ちで挑むのでは、俄然やる気が違います🚀

課題

1番の課題は、自分がまだまだ受け身の姿勢で学習しているというところです。

先日伊藤さんのブログを読んで、そのことに気づきました。

学校の勉強とプログラミングの勉強は何が違うか(そして技術書をどう読むべきか) - give IT a try

内容としては(とても大雑把なまとめですが)、「プログラミングの勉強は試験勉強とは異なり、技術書の内容を全て完璧に理解する必要はない。技術書を読むときは、後から振り返って参照できるように、頭の中にインデックスを作れれば大丈夫。」ということが書いてあるのですが、その中で次の見出しを見てハッとしたのです。

本の内容を丸暗記したからといって必ず就職できるという保証はない

「カリキュラムをやりさえすれば就職できる」という、甘えた気持ちがあることを見透かされた様でした。

あくまでプログラムを書くことは手段で、目的はサービスを提供すること。就職する上で、プログラムを通じてどのように問題を解決できるのか、そこが大切になってくるのだと思います。

その問題解決の視点を養うために、まずサービスに興味を持って色々と使ってみる。そのサービスは「どうやって作っているの?」、「どうやって広めているの?」、「どうやって儲けているの?」と考えてみることから始めてみたいと思います📚

とりあえず、週に何時間か時間をとって、サービス探訪をやってみようかな。

来月の目標

  • 学習時間は200時間。
  • よりショートカットキーに習熟する。
  • Rubyラクティスを頑張る💪
  • サービスの研究を行う。

最後に

メンターさんの日報や課題に対する温かいコメント、とても励みになっています!ありがとうございます🙇🏻

また、メンターさんや現役生の方の日報やブログなど、アウトプットを参考にさせていただいています🙏

まだまだ自分のプラクティスに追われ余裕のない状態ですが、少しずつでも自分の幅を広げていきたいと思います。

フィヨルドブートキャンプ 二週目

フィヨルドブートキャンプに参加して、二週間が経ちました。

今週はインプットが多い一週間で、エディタやコンソールを開きながら、何かしらのテキストを読んでいた記憶しかありません📘

そんな今週の振り返りになります。

先週の取り組み

  • 学習時間 : 44.5時間 (目標 - 5.5時間 / 累計93.5時間)
  • Vim
  • Terminal
  • Linuxの基礎知識

学習の感想

  • 「パスを通す」など、ただ写経するだけだったコードの意味を分かっていくのは楽しいです😊
  • 先週はChromeのショートカットキーを使うことを意識して操作を行いました。タブの操作はコマンドでできる様になれたと思います。
  • Vimの基本的な操作方法について学びましたが、幅広い操作を全部は覚えきれていないと思います。その都度調べながら、実践で身につけていくしかないですね。
  • 目下Linuxの勉強中ですが、学ぶ分量が多いことに加え、zshなど横道に逸れると深みにはまるため、結構時間がかかっています💦今月中にLinuxのプラクティスを終了できるか怪しい...。

課題

  • 日報は学習内容をまとめたものを書く。どうしてもダラダラと書いてしまいがちなのをなくしていきたい...。
  • 学習の本筋とは関係ないところで詰まってしまい、結構な時間を無駄にしてしまった(4-5時間ほど)。自力で解決できたものの、ある程度のところで見切りをつけて質問する様にしないと貴重な時間が勿体ない。出来ない自分を曝け出すこと。

今週の予定

  • Linuxのプラクティスを完了させる。
  • シェルやターミナル操作のショートカットキーを意識して使っていく。
  • 何か調べたものをブログに書く。(大したことじゃなくても良い)

最後に

  • 今週も引き続きLinuxの勉強が続きますが、Webサービスを作る上で必須な知識なので、気を緩めず勉強を続けていきます!💪

フィヨルドブートキャンプ 一週目

フィヨルドブートキャンプに参加して一週間が経ちました。

新しいことに取り組み、課題を進めていくうちに、あっという間に時間が過ぎている気がします💨

一週間という節目に、先週の振り返りと、今週の予定を立てていきたいと思います。

先週の取り組み

  • 学習時間 : 49時間 (累計49時間)

  • フィヨルドブートキャンプに参加!

  • HTML, Markdown, CSS の学習

  • Vimエディタの学習(ちょこっと)

学習の感想

  • Discordに始まり、SNS、画面をキャプチャするアプリ、Web上でコードを試すことができるツールまで、未知のサービスを沢山使うようになりました。初めは面食らいましたが、自然と使いこなせるようになるまで、どんどん使っていきたいと思います。

  • 課題作成前に読む参考文献がかなりのボリューム!独学では全く知らなかった箇所も多く、とても勉強になっています🙏

  • 課題のレビューがとても丁寧で、分かりやすいです。褒めていただけることもあったりして、さらにモチベーションが上がります!

  • 大変でも、公式のドキュメントをきちんと読むことが大事なのだと学びました。英語がなかなか入っていかない...。

課題

  • アウトプットに時間がとても掛かってしまう。文章で何かを表現することがあまりなかったので、とても苦戦しています。たくさん文章を書いて訓練あるのみですね。

  • まだ新しいことや課題に精一杯で、他の受講者の方のアウトプット(日報やDiscord等)にあまり目を通せていない。どんどん学習を進めながらも、フットワークの軽い先輩がいらっしゃるので、見習いたいです💪

今週の予定

  • とりあえずは次のプラクティス(Linux)を、進めるところまで進めたいです。今月中に終わらせたいですね。

  • ブラウザ操作の便利なコマンドを意識的に使って、少しでも定着させたいです。

  • 学習時間は週50時間を目安に。

  • また一週間後に、経過観察のブログを書く!

最後に

メンターの方や他の受講生の方からの反応が、精神的な励ましになっています🙏

この調子で学習をどんどん進めていこうと思います!

自己紹介

初投稿

自己紹介

初めまして、カッシーと申します!

現在はフリーターをしており、プログラマーとして転職を目指して目下プログラミングの勉強中です。

独学で基礎的な勉強を行ってきたのですが、限界を感じて、に、フィヨルドブートキャンプに参加しました。

趣味は読書で、ジャンルに関係なく気になったものを読んでいます。

もう1つの趣味は観劇です。コロナ禍になってから2年ほど劇場に行けていないのですが、今年末に予定されている『薔薇とサムライ2』は観に行きたいな...

これからの予定

目下は課題をこなしながら、スクールに慣れていきたいと思っています。

ある程度慣れてきたら、将来ポートフォリオを作成するのに向けて、AWSやコンテナの勉強なんかも同時並行で進めていければと思います。

また、SNSでのコミュニケーションを全くしてこなかったので、その訓練も必要ですね。

まずはTwitterでフォローする方を少しずつ増やしていくことから始めようかな...。

最後に

フィヨルドブートキャンプに参加してから、初めてやること・やらなければいけないことが沢山あって目まぐるしいですが、ひとつひとつクリアしていければと思います。

失敗してもくよくよせず、前進せよ!