20200625のGitに関する記事は5件です。

FreeBSD12.1+apacheでgitサーバ(smart http)

gitレポジトリのサーバを立てるにあたり、プロトコルはsshでいいや、と思っていたのだけど、意外とhttpプロトコルでの設定も簡単にできるとのこと。

とりあえず、試してみました。

環境

  • VirtualBox on ubuntu20.04
  • FreeBSD 12.1

上記の環境で、インストールしてネットワークが接続できるところまで設定し終わっている事を前提とします。

設定

apacheとgitのインストール。

pkg install apache24 git

apacheの設定。
/usr/local/etc/apache24/Include/000-default.conf」というファイルを作成して、設定します。

/usr/local/etc/apache24/Include/000-default.conf
<VirtualHost *:80>
    ServerName hoge.example.com

    SetEnv GIT_PROJECT_ROOT /var/html/git
    SetEnv GIT_HTTP_EXPORT_ALL
    ScriptAlias /git /usr/local/libexec/git-core/git-http-backend
    <Location /git>
        AuthName "Git Repository"
        AuthType Basic
        AuthUserFile /usr/local/etc/apache24/git.passwd
        Require valid-user
    </Location>
</VirtualHost>

/etc/rc.conf」で、apacheサービスを有効設定にします。

/etc/rc.conf
apache24_enable="YES"

パスワードファイルを作成します。
以下のコマンドを実行すると、パスワードを尋ねられるので、入力します。

htpasswd -c /usr/local/etc/apache24/git.passwd katsuko

レポジトリを作成します。

mkdir -p /var/html/git
git init --bare --shared /var/html/git/project.git
cd /var/html/git/project.git
git update-server-info
chown -R www:www .

サービスを起動します。

service apache24 start

これで、他のクライアントから「git clone http://katsuko@hoge.example.com/git/project.git」でクローンができる…と思いきや、

fatal: repository 'http://hoge.example.com/git/project.git/' not found

とのエラーが。
ふと「/usr/local/etc/apache24/httpd.conf」を見てみると、

/usr/local/etc/apache24/httpd.conf
<IfModule !mpm_prefork_module>
        #LoadModule cgid_module libexec/apache24/mod_cgid.so
</IfModule>
<IfModule mpm_prefork_module>
        #LoadModule cgi_module libexec/apache24/mod_cgi.so
</IfModule>

というように、mod_cgiがコメントアウトされているので、このコメントを外してやりましょう。
いやー、CGI動かすの久々なんで、ちょっと悩みましたよ。

ちなみに、「Options ExecCGI」は要らないのかな、と思ったのだけど、apacheのマニュアルによると、必要なのは「ScriptAlias」の指定以外でCGIを動かす場合のようです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails Tutorial 第6版 学習まとめ 第10章

概要

この記事は私の知識をより確実なものにするためにRailsチュートリアル解説記事を書くことで理解を深め
勉強の一環としています。稀にとんでもない内容や間違えた内容が書いてあるかもしれませんので
ご了承ください。
できればそれとなく教えてくれますと幸いです・・・

出典
Railsチュートリアル第6版

この章でやること

ユーザーの作成、ログイン、ログイン情報の記憶が実装できたので
次はユーザーリソースで放置していた更新、表示、削除機能を作成する。

ユーザーを更新する

ユーザーを更新するにはeditアクションを編集する。
今迄に実装してきたsessionsコントローラのnewアクションや
Usersコントローラのnewアクションのようにフォームを用意し、
フォームの入力値をupdateアクションに送るという動作を実装すればいい。
もちろん編集が可能なのはユーザー本人だがこれまでに実装した認証を使って
アクセス制御を実装していく。

編集フォーム

editページはURLに対象のユーザーのIDが含まれる
ex) users/1/edit

これを利用してURLのIDからユーザーを取り出しインスタンス変数に保存しておく。

  def edit
    @user = User.find(id: params[:id])
  end

こうすることで次に作成するフォームでモデルオブジェクトに@userオブジェクトを指定する。

edit.html.erb
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(model: @user, local: true) do |f| %>
      <%= render 'shared/error_messages' %>

      <%= f.label :name %>
      <%= f.text_field :name, class: 'form-control' %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'form-control' %>

      <%= f.submit "Save changes", class: "btn btn-primary" %>
    <% end %>

    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="https://gravatar.com/emails" target="_blank">change</a>
    </div>
  </div>
</div>

フォームに無効な値を入力した際のエラーメッセージを表示するために_error_messagesパーシャルを再利用している。

またgravatarのリンク部にtarget="_blank"とあるが、このように記述することで新しいタブでリンク先を表示できる。

さらにフォームの入力欄には@user変数に現在入っている値が自動入力される。
Rails側で自動的に保存されている属性情報を引っ張ってきて表示してくれるらしい。ぐう有能。

このerbから生成される実際のHTMLをのぞくと
<input type="hidden" name="_method" value="patch">
このような記述がある。Webブラウザは更新のリクエストであるPATCHリクエストを送信できないためRailsが
隠しinputフィールドにpatchを指定して、疑似的にPATCHリクエストとして偽造している。

もう一つ覚えておきたい事項としてnewアクションとeditアクションでほぼ変わらないerbコードを使っているのに
なぜRailsが新規のユーザーか既存のユーザーか判別できるかだが
ActiveRecordのnew_record?メソッドで新規か既存か判別できるからである。

>> new_user = User.new
   (1.3ms)  SELECT sqlite_version(*)
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, password_digest: nil, remember_digest: nil>
>> user1 = User.first
  User Load (0.6ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "take", email: "take.webengineer@gmail.com", created_at: "2020-06-14 02:57:10", updated_at: "2020-06-20 03:53:57", password_digest: [FILTERED], remember_digest: "$2a$12$tYO.HIfYezXpTk2zRp9s6uqJY4wUkPM28NfYuJ7vxq/...">
>> new_user.new_record?
=> true
>> user1.new_record?
=> false
>> 

実際のerbではform_withを使った際モデルオブジェクトに対してnew_record?```の結果を見て
postかpatchかを判定する。

最後にナビゲーションバーのeditアクションへのリンクを設定する。
<li><%= link_to "Settings", edit_user_path(current_user) %></li>

演習

1.興味がわいたので具体的にどのような脆弱性があるか調べた結果、以下の記事が参考になったので置いておく。
https://webegins.com/target-blank/
"noopener"超重要!

2.newビューとeditビューではフォームの部分がほぼ共通で、違う点といったら
submitボタンのテキストぐらいである。
そのため、provideメソッドを使ってsubmitボタンのテキストコンテンツを変更するようにし、
パーシャルにまとめてリファクタリングする。

_form.html.erb
   <%= form_with(model: @user, local: true) do |f| %>
      <%= render 'shared/error_messages' %>

      <%= f.label :name %>
      <%= f.text_field :name, class: 'form-control' %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'form-control' %>

      <%= f.submit yield(:btn_text), class: "btn btn-primary" %>
    <% end %>
new.html.erb
<% provide(:title, 'Sign up') %>
<% provide(:btn_text, "Create my account") %>
<h1>Sign up</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= render 'form' %>
  </div>
</div>
edit.html.erb
<% provide(:title, "Edit user") %>
<% provide(:btn_text, "Save changes") %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= render 'form' %>
    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="https://gravatar.com/emails" target="_blank" rel="noopener">change</a>
    </div>
  </div>
</div>

編集の失敗

ユーザー登録と同じく、無効な値で更新しようとした際の編集の失敗に関して実装していく。
updateアクションを実装していくがcreateアクションでparamsを使ってユーザーを作成したのと同じく
editアクションから送信されたparamsを使って更新する。
構造はかなり似ている。
もちろんparamsでDBを直接更新するのは危険なため今回もStrongParameter(以前定義したuser_paramsメソッド)を使う。

  def update
    @user = User.find(params[:id])
    if @user.update(user_params)

    else
      render 'edit'
    end
  end

現時点でUserモデルのバリデーションと_error_messagesパーシャルが存在するため
無効な値にはエラーを返してくれるようになっている。

演習

1.失敗する。
image.png

編集失敗時のテスト

ユーザーの編集関連の統合テストを作成する。
今回は題目通り編集失敗時のテストを書いていく。
rails g integration_test user_edit

users_edit_test.rb
require 'test_helper'

class UserEditTest < ActionDispatch::IntegrationTest
  def setup 
    @user = users(:michael)
  end

  test "unsuccessful edit" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    patch user_path(@user) , params:{user:{name: "",
                                           email: "foo@bar",
                                           password: "foo",
                                           password_confirmation: "bar"}}
    assert_template 'users/edit'
  end
end

1.editページにGETリクエスト、editページが描画されているか確認
2.updateアクションにpatchリクエスト、無効な値を送信
3.editページが再描画されているか確認。

の順のテストになる。

演習

1.assert_select 'div.alert', "The form contains 4 errors."

TDDで編集を成功させる

今度は成功時の動作を実装していく。
ユーザ⁻画像はGravatarで実装しているためすでに動作する。
name,email,passwordなどほかの属性の編集の成功を実装していく。

機能を実装する前に統合テストを書き、機能を実装し終わったときその機能が受け入れ可能な状態かどうか決めるテスト
を「受け入れテスト」と呼ぶ。
実際にTDDで編集成功を実装してみる。

先程実装した失敗時のテストを参考に実装していくとわかりやすい。(もちろん有効なデータを送信するが)

  test "successful edit" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    name = "foo"
    email = "foo@bar.com"
    patch user_path(@user) , params:{user:{name: name,
                                           email: email,
                                           password: "",
                                           password_confirmation: ""}}

    assert_not flash.empty?
    assert_redirected_to @user
    @user.reload
    assert_equal name, @user.name
    assert_equal email, @user.email
  end

り確実なものにするためにRailsチュートリアル解説記事を書くことで理解を深め
勉強の一環としています。稀にとんでもない内容や間違えた内容が書いてあるかもしれませんので
ご了承ください。
できればそれとなく教えてくれますと幸いです・・・

出典
Railsチュートリアル第6版

この章でやること

ユーザーの作成、ログイン、ログイン情報の記憶が実装できたので
次はユーザーリソースで放置していた更新、表示、削除機能を作成する。

ユーザーを更新する

ユーザーを更新するにはeditアクションを編集する。
今迄に実装してきたsessionsコントローラのnewアクションや
Usersコントローラのnewアクションのようにフォームを用意し、
フォームの入力値をupdateアクションに送るという動作を実装すればいい。
もちろん編集が可能なのはユーザー本人だがこれまでに実装した認証を使って
アクセス制御を実装していく。

編集フォーム

editページはURLに対象のユーザーのIDが含まれる
ex) users/1/edit

これを利用してURLのIDからユーザーを取り出しインスタンス変数に保存しておく。

  def edit
    @user = User.find(id: params[:id])
  end

こうすることで次に作成するフォームでモデルオブジェクトに@userオブジェクトを指定する。

edit.html.erb
<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(model: @user, local: true) do |f| %>
      <%= render 'shared/error_messages' %>

      <%= f.label :name %>
      <%= f.text_field :name, class: 'form-control' %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'form-control' %>

      <%= f.submit "Save changes", class: "btn btn-primary" %>
    <% end %>

    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="https://gravatar.com/emails" target="_blank">change</a>
    </div>
  </div>
</div>

フォームに無効な値を入力した際のエラーメッセージを表示するために_error_messagesパーシャルを再利用している。

またgravatarのリンク部にtarget="_blank"とあるが、このように記述することで新しいタブでリンク先を表示できる。

さらにフォームの入力欄には@user変数に現在入っている値が自動入力される。
Rails側で自動的に保存されている属性情報を引っ張ってきて表示してくれるらしい。ぐう有能。

このerbから生成される実際のHTMLをのぞくと
<input type="hidden" name="_method" value="patch">
このような記述がある。Webブラウザは更新のリクエストであるPATCHリクエストを送信できないためRailsが
隠しinputフィールドにpatchを指定して、疑似的にPATCHリクエストとして偽造している。

もう一つ覚えておきたい事項としてnewアクションとeditアクションでほぼ変わらないerbコードを使っているのに
なぜRailsが新規のユーザーか既存のユーザーか判別できるかだが
ActiveRecordのnew_record?メソッドで新規か既存か判別できるからである。

>> new_user = User.new
   (1.3ms)  SELECT sqlite_version(*)
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, password_digest: nil, remember_digest: nil>
>> user1 = User.first
  User Load (0.6ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "take", email: "take.webengineer@gmail.com", created_at: "2020-06-14 02:57:10", updated_at: "2020-06-20 03:53:57", password_digest: [FILTERED], remember_digest: "$2a$12$tYO.HIfYezXpTk2zRp9s6uqJY4wUkPM28NfYuJ7vxq/...">
>> new_user.new_record?
=> true
>> user1.new_record?
=> false
>> 

実際のerbではform_withを使った際モデルオブジェクトに対してnew_record?```の結果を見て
postかpatchかを判定する。

最後にナビゲーションバーのeditアクションへのリンクを設定する。
<li><%= link_to "Settings", edit_user_path(current_user) %></li>

演習

1.興味がわいたので具体的にどのような脆弱性があるか調べた結果、以下の記事が参考になったので置いておく。
https://webegins.com/target-blank/
"noopener"超重要!

2.newビューとeditビューではフォームの部分がほぼ共通で、違う点といったら
submitボタンのテキストぐらいである。
そのため、provideメソッドを使ってsubmitボタンのテキストコンテンツを変更するようにし、
パーシャルにまとめてリファクタリングする。

_form.html.erb
   <%= form_with(model: @user, local: true) do |f| %>
      <%= render 'shared/error_messages' %>

      <%= f.label :name %>
      <%= f.text_field :name, class: 'form-control' %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'form-control' %>

      <%= f.submit yield(:btn_text), class: "btn btn-primary" %>
    <% end %>
new.html.erb
<% provide(:title, 'Sign up') %>
<% provide(:btn_text, "Create my account") %>
<h1>Sign up</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= render 'form' %>
  </div>
</div>
edit.html.erb
<% provide(:title, "Edit user") %>
<% provide(:btn_text, "Save changes") %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= render 'form' %>
    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="https://gravatar.com/emails" target="_blank" rel="noopener">change</a>
    </div>
  </div>
</div>

編集の失敗

ユーザー登録と同じく、無効な値で更新しようとした際の編集の失敗に関して実装していく。
updateアクションを実装していくがcreateアクションでparamsを使ってユーザーを作成したのと同じく
editアクションから送信されたparamsを使って更新する。
構造はかなり似ている。
もちろんparamsでDBを直接更新するのは危険なため今回もStrongParameter(以前定義したuser_paramsメソッド)を使う。

  def update
    @user = User.find(params[:id])
    if @user.update(user_params)

    else
      render 'edit'
    end
  end

現時点でUserモデルのバリデーションと_error_messagesパーシャルが存在するため
無効な値にはエラーを返してくれるようになっている。

演習

1.失敗する。
image.png

編集失敗時のテスト

ユーザーの編集関連の統合テストを作成する。
今回は題目通り編集失敗時のテストを書いていく。
rails g integration_test user_edit

users_edit_test.rb
require 'test_helper'

class UserEditTest < ActionDispatch::IntegrationTest
  def setup 
    @user = users(:michael)
  end

  test "unsuccessful edit" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    patch user_path(@user) , params:{user:{name: "",
                                           email: "foo@bar",
                                           password: "foo",
                                           password_confirmation: "bar"}}
    assert_template 'users/edit'
  end
end

1.editページにGETリクエスト、editページが描画されているか確認
2.updateアクションにpatchリクエスト、無効な値を送信
3.editページが再描画されているか確認。

の順のテストになる。

演習

1.assert_select 'div.alert', "The form contains 4 errors."

TDDで編集を成功させる

今度は成功時の動作を実装していく。
ユーザ⁻画像はGravatarで実装しているためすでに動作する。
name,email,passwordなどほかの属性の編集の成功を実装していく。

機能を実装する前に統合テストを書き、機能を実装し終わったときその機能が受け入れ可能な状態かどうか決めるテスト
を「受け入れテスト」と呼ぶ。
実際にTDDで編集成功を実装してみる。

先程実装した失敗時のテストを参考に実装していくとわかりやすい。(もちろん有効なデータを送信するが)

  test "successful edit" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    name = "foo"
    email = "foo@bar.com"
    patch user_path(@user) , params:{user:{name: name,
                                           email: email,
                                           password: "",
                                           password_confirmation: ""}}

    assert_not flash.empty?
    assert_redirected_to @user
    @user.reload
    assert_equal name, @user.name
    assert_equal email, @user.email
  end

もちろんテストは失敗する。
まず、flashメッセージを実装していないこと。リダイレクトの指定をしていないこと。この二つが引っかかる。
そして一番大切な部分。パスワードの値を空にしているため、バリデーションに引っかかり、正常に更新できない。

前者の二つはこの行で実装する。

  def update
    @user = User.find(params[:id])
    if @user.update(user_params)
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'

    end
  end

この時点では@user.updateはパスワードが空欄でバリデーションに引っかかり、else文に分岐するため、
テストもうまく動かない。
この対策としてパスワードが空の時の例外処理を加える。
このような際にはallow_nil: trueというオプションを使うと便利。
これで空欄でもバリデーションにひっかからなくなる。
このオプションで存在性の検証を通過してしまうが、has_secure_passwordメソッド側のオブジェクト生成時の
存在性のバリデーションが働くため、新規作成時はnilをはじき、更新時はnilならパスワードを変更しないという
動作を実現できる。
さらにこのallow_nil: trueオプションを追加したことでモデルに定義したバリデーションと
has_secure_passwordメソッドのバリデーションが重複して同じエラーメッセージが表示される不具合も解決する。

演習

1.成功する
image.png

2.デフォルトのGravatar画像が代わりに表示される。
image.png

認可

Webアプリにおける認証はユーザーを識別すること。認可はユーザーの実行可能な操作範囲を管理すること。
今まで実装してきたupdate,editアクションでは大きな欠陥があり、
現在の状態ではどのユーザーがログインしていようとすべてのユーザーを編集できてしまう。
ナビゲーションバーのSettingリンクはログインしているユーザーのeditページを表示するが
直接URLに様々なユーザーのeditアクションを指定してしまえばアクセス可能だし、更新もできてしまう。

これはまずいので動作を正しいものに変える。
具体的には
未ログイン時はログインページに転送する+メッセージを表示。
ログイン済みだが別のユーザーにアクセスしようとしている場合はルートURLに転送する。

ユーザーにログインを要求する

Usersコントローラでbeforeフィルターを使ってedit,updateアクションが実行される前に
必ずログインを強制するよう実装していく。

  before_action :logged_in_user, only:[:edit,update]
  .
  .
  .
    def logged_in_user
      unless logged_in?
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

このように実装することでedit,updateアクションの実行前に必ずlogged_in_userメソッドが実行され
ログインしていない際にはフラッシュメッセージでログインを促すメッセージを表示し、
ログインページにリダイレクトする。

そして今の段階ではログインしていない状態でeditビューにアクセスするとログインページに
飛ばされてしまうようになったためテストは失敗する。

テストが通るようにuser_edit_test.rbではeditアクションにアクセスする前にログインするようにする。
log_in_asメソッドをテスト用に定義しているためそれをつかう。

これでテストはパスするようになる。だがbefore_actionの行をコメントアウトしてもテストではじかれない。
これは重大なセキュリティホールでテストではじかれなければまずいので
しっかりテストではじくよう修正していく。

  test "should redirect edit when not logged in" do
    get edit_user_path(@user)
    assert_not flash.empty?
    assert_redirected_to login_path
  end

  test "should redirect update when not logged in" do
    patch user_path(@user), params:{ user: {name: @user.name,  
                                            email: @user.email }}
    assert_not flash.empty?
    assert_redirected_to login_path
  end 

このようにテストを追加していくことで
edit,updateアクションを実行する前にlog_in_userが実行されているかを必ずテストするため
セキュリティホールをテストではじいてくれるようになる。

演習

1.newページやsignupアクションが実行できなくなりエラーとなる。

 FAIL["test_should_get_new", #<Minitest::Reporters::Suite:0x00007f1d1cf4dab8 @name="UsersControllerTest">, 0.06502773099964543]
 test_should_get_new#UsersControllerTest (0.07s)
        Expected response to be a <2XX: success>, but was a <302: Found> redirect to <http://www.example.com/login>
        Response body: <html><body>You are being <a href="http://www.example.com/login">redirected</a>.</body></html>
        test/controllers/users_controller_test.rb:10:in `block in <class:UsersControllerTest>'

 FAIL["test_invalid_signup_information", #<Minitest::Reporters::Suite:0x00007f1d1cff74c8 @name="UsersSignupTest">, 0.08553676799965615]
 test_invalid_signup_information#UsersSignupTest (0.09s)
        expecting <"users/new"> but rendering with <[]>
        test/integration/users_signup_test.rb:12:in `block in <class:UsersSignupTest>'

 FAIL["test_valid_signup_information", #<Minitest::Reporters::Suite:0x00007f1d1d0494d0 @name="UsersSignupTest">, 0.09624041300003228]
 test_valid_signup_information#UsersSignupTest (0.10s)
        "User.count" didn't change by 1.
        Expected: 2
          Actual: 1
        test/integration/users_signup_test.rb:20:in `block in <class:UsersSignupTest>'

  9/9: [================================================================] 100% Time: 00:00:01, Time: 00:00:01

正しいユーザーを要求する

次はログインしていても本人でなければ編集できないようにしていく。
TDDで進めていく。

まずは別のユーザーでログインする状況を作るためにfixtureに2人目のユーザーを追加する。

users.yml
archer: 
  name: Sterling Archer
  email: duchess@example.gov
  password_digest: <%= User.digest('password') %>

続いて、テストで@other_userとしてログインし、
@userの更新を行うテストを書く。

  test "should redirect edit when logged in as wrong user" do
    log_in_as(@other_user)
    get edit_user_path(@user)
    assert flash.empty?
    assert_redirected_to root_path
  end

  test "should redirect update when logged in as wrong user" do
    log_in_as(@other_user)
    patch user_path(@user), params:{ user: { name: @user.name,
                                             email: @user.email}}
    assert flash.empty?
    assert_redirected_to root_path
  end

flashメッセージは特に表示せずルートURLに飛ばすだけなのでこのようなテストになる。

テストを書き、もちろんパスしないので
テストをパスさせるようにコードを書く。

具体的にはcorrect_userメソッドを作成し、edit,updateアクション実行前にユーザーがあっていなかったら
ルートURLに飛ばす処理を書く。

users_controller.rb
  before_action :correct_user, only:[:edit,:update]



  private
    def correct_user
      @user = User.find(params[:id])
      redirect_to root_url unless @user == current_user
    end

これでテストがパスするようになる。

最後にcurrent_user?メソッドを定義し、先ほど定義したcorrect_userメソッドに組み込む

    def correct_user
      @user = User.find(params[:id])
      redirect_to root_url unless current_user?(@user)
    end
  def current_user?(user)
    user && user == current_user
  end
演習

1.updateアクションを保護しなかった場合、editアクション(editページ)を経由せずcurlコマンドなどで直接
値を送った場合に更新できてしまうから。

2.editアクションのほうが簡単にテストできる。(実際にログインして別のユーザのeditパスを表示すればいい)

フレンドリーフォワーディング

さらにこの更新の機能を便利にする。具体的には
logged_in_userメソッドで未ログインのユーザーのeditページへのアクセスをはじいてログインページに飛ばした際
そのままログインすると問答無用でユーザー詳細ページ(show)に飛ばされてしまうが、
editページにアクセスしたくてログインしたのにshowページが表示されてしまうのは少々不便である。
これを改良しログインしたらeditページに飛ばしてくれるようにする(フレンドリーフォワーディング)

テストもこの通り実装すればいいので
未ログインでeditページにアクセスし、ログインしたらeditページにリダイレクトすることをチェックする。

test "successful edit with friendly forwarding" do
    get edit_user_path(@user)
    log_in_as(@user)
    assert_redirected_to edit_user_url(@user)
    name = "foo"
    email = "foo@bar.com"
    patch user_path(@user) , params:{user:{name: name,
                                           email: email,
                                           password: "",
                                           password_confirmation: ""}}

    assert_not flash.empty?
    assert_redirected_to @user
    @user.reload
    assert_equal name, @user.name
    assert_equal email, @user.email
  end

現時点で失敗するテストが書けたのでつぎはこのテストがパスするようにコードを書いていく。
リクエスト時のページを保存しておき、ログイン時にそこへリダイレクトする処理を書く。

sessions_helperにメソッドを定義する。

sessions_helper.rb
  def redirect_back_or(default)
    redirect_to(session[:forwarding_url] || default)
    session.delete(:forwarding_url)
  end

  def store_location
    session[:forwarding_url] = request.original_url if request.get?
  end
一時セッションにリクエスト先のURLを保存する処理を書いている。
この時GETリクエストだけを保存するようにしないと

万が一ログインしてフォームページににアクセスし、意図的に保存されたログイン情報のcookiesを削除した上で
フォームの内容を送信するとpost,patchなどのURLが保存されてしまう。
その状態で```redirect_back_or```メソッドを使うとリダイレクトでpost,patchなどを期待するURLに対してGETリクエストが
送られてしまい、エラーが発生する可能性が高い。
GETリクエストに絞ることでこういったリスクを回避できる。

```logged_in_user```メソッドに```store_location```メソッドを入れて、リクエスト先URLを保存し、
sessions_controllerのcreateアクションに```redirect_back_orメソッド```を入れることで、
ログイン時、もしセッションに保存されたURLがあればそちらにリダイレクトするようにする。

```rb:sessions_controller.rb
  def create
    @user = User.find_by(email: params[:session][:email].downcase)
    if @user&.authenticate(params[:session][:password])
      log_in(@user)
      params[:session][:remember_me] == '1' ? remember(@user) : forget(@user)
      redirect_back_or @user
    else
      flash.now[:danger] = "Invalid email/password combination"
      render 'new'
    end
  end
users_controller.rb
    def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

ちなみにreturnやメソッドの最終行の直接呼出しがない限りは
リダイレクトはメソッドの最後で行われる。

上記の内容でテストはパスする。

演習

1.ログインしてeditページにリダイレクトした時点で保存したURLが消えていることを確認すればいい。

user_edit_test.rb
  test "successful edit with friendly forwarding" do
    get edit_user_path(@user)
    log_in_as(@user)
    assert_nil session[:forwarding_url] #ここを追加
    assert_redirected_to edit_user_url(@user)
    name = "foo"
    email = "foo@bar.com"
    patch user_path(@user) , params:{user:{name: name,
                                           email: email,
                                           password: "",
                                           password_confirmation: ""}}

    assert_not flash.empty?
    assert_redirected_to @user
    @user.reload
    assert_equal name, @user.name
    assert_equal email, @user.email
  end

2.

(byebug) session[:forwarding_url]
"https://12b7e3b6aec94b45960b81560e233372.vfs.cloud9.us-east-2.amazonaws.com/users/1/edit"
(byebug) request.get?
true

最後に

だんだん章の最後にやることが増えてきたので
とりあえずまとめておく。

rails t
git add -A
git commit -m "Finish user edit, update, index and destroy actions"
git co master
git merge updating-users
git push
rails t 
git push heroku
heroku pg:reset DATABASE
heroku run rails db:migrate
heroku run rails db:seed

本番用DBはpg:reset DATABASEで行う。なお、間違いを防ぐためにDBをリセットするアプリ名の
入力を求められるので入力してリセット
それか--confirmオプションを使って
heroku pg:reset DATABASE -c アプリ名
としてもいい。

あとはheroku上でマイグレートとサンプルの追加を行って終わり。

前の章へ

次の章へ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【やらかし別】commit履歴を整理するためのGitコマンド

ケース別commit履歴の整理法

正直な話、たった一人でプロダクトを開発していると、僕みたいな人間はcommitの粒度などに無頓着になってくるのですが、最近は開発したプロダクトのコードレビューをしていただく機会が多くなったこともあり、気をつけ始めています。
相手としても読みにくいだろうし、見せるこちらとしてもやはり恥ずかしいですもんね。。。

この記事は、ローカルのcommit履歴を整理するための手順をまとめたものです。
僕自身の備忘録でもあり、おそらく今後もよく見返すであろう手順をここにまとめています。

僕は驚くほど無知なので、もし何かより良い提案などございましたら、ぜひ教えていただければ幸いです。

(テンションおかしいのは無視してください)

ケース1「あっ。直前のcommitを修正したい...」

やだーーー!!
fugas_controllerのhogeアクションとviewのhoge.html.erbを消して「delete fugas#hoge」とcommitしたのに、ルーティングの設定が残ったままじゃん!

このcommitを取り消そう。
commitを取り消すにしても、commitを間違えた歴史は残したくないから、revertじゃなくて、resetですね。
参考記事:Git commit 取り消したい

indexとワーキングツリーはそのままでいいから、--hardじゃなくて、--softですね。
参考記事:[git reset (--hard/--soft)]ワーキングツリー、インデックス、HEADを使いこなす方法

取り消すのは直前のcommitなので、HEAD^ですね。

git reset --soft HEAD^

routes.rbを変更したから、indexに追加しよう。

git add config/routes.rb

変更はちゃんと全部indexに追加されてるかなぁ〜〜〜???
git statusで確認しよう。

git status

どれどれ〜??

出力結果
 modified:   app/controllers/fugas_controller.rb
 deleted:    app/views/fugas/hoge.html.erb
 modified:   config/routes.rb

問題ないですね。

よし、commitしよう。

git commit -m "delete fuga#hoge" -m "fugaコントローラーのhogeアクションを削除した。"

完了!!

ケース2「げっ!いくつか前のcommitを修正したい...」

やだーーー!!
fugas_(ry

それもいくつか前のcommitだなぁ...。修正しなきゃ〜...。

で?修正したいのは、どれくらい前のcommitだっけ?
git logで確認や!
修正内容まで見なくても、コミットメッセージだけ表示してくれればわかるので、--onelineつけとこう。

git log --oneline

どれどれ??

出力結果
 dc77776b (HEAD -> refactoring) modify footer
 3fb04106 delete fugas#hoge
 0764bcaf delete landing_page
 29970ee1 fix pagenate bug

"delete fugas#hoge"は2つ前のcommitか。
了解了解。
q を入力して離脱しよう。

さて、ここからがちょっと面倒くさい。
commit履歴が少し過去にまで遡るので、delete fugas#hogeのコミットまでgit resetしてしまうと、modify footerのコミットまで消えてしまいます。

だからここで使うコマンドは、git rebase -i <変更したいコミットの「1つ前」のハッシュ値>です。

これを使えば、ピンポイントで特定のcommitの内容だけを修正できる。
注意すべきは、rebase -iで指定するのは、「変更したいコミットの1つ前のコミットのハッシュ値」だということ。
ここではつまり、delete landing_pageのハッシュ値になる。
だから打つべきコマンドは、下になる。

git rebase -i 0764bcaf

すると次の入力画面が現れる。

出力結果(変更前)
 pick 3fb04106 delete fugas#hoge
 pick dc77776b modify footer

# Rebase 0764bcaf..dc77776b onto 0764bcaf (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell

さて、過去のcommitを書き換えるには、この画面の1行目のpick 3fb04106 delete fugas#hogeを、edit 3fb04106 delete fugas#hogeに書き換えなくてはならない。
しかし、初心者にとっては、ここが意外にも難所だったりします。
ここで表示される入力画面は、普段触る機会の少ない『Vim』と呼ばれるクセのあるエディターだからです。
Vimの使い方については、下の記事が詳しいです。
(参考記事:知識0から始めるVim講座)
 
読む暇もないという方は、これから僕が唱える呪文を順番に打ってください。
表示されたばかりの状態では文字入力はできないので、まずはiのキーを叩くことで、文字入力できる状態にします。
叩いた?文字入力できるようになった?
では次に、1行目のpick 3fb04106 delete fugas#hogeを、edit 3fb04106 delete fugas#hogeに書き換えます。
書き換えられた?
次に、escキーを押します。
押した?また文字入力できなくなった?
次に、:wqを押して、保存して終了します。
終了した?
もし下の画面が出力されたら、とりあえず成功です。

出力結果
 Stopped at 3fb04106...  delete fugas#hoge
You can amend the commit now, with

  git commit --amend 

Once you are satisfied with your changes, run

  git rebase --continue

そしたら、delete fugas#hogeのコミットに追加したいファイルなり変更なりをstagingしましょう。

git add config/routes.rb

そしていつものようにcommmitするのですが、ここでちょっと注意。
オプションは--amendにするのだ。

git commit --amend 

commitした??
すると、またこんな感じでVimの画面が表示されたと思う。

出力結果
delete fugas#hoge

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Thu Jun 25 15:06:15 2020 +0900
#
# interactive rebase in progress; onto 7339e063
# Last command done (1 command done):
#    edit 3fb04106 delete fugas#hoge
# Next commands to do (2 remaining commands):
#    pick dc77776b modify footer

とくにコミットメッセージなど変更するつもりがないなら、:wqで編集画面を終了しましょう。
メッセージを修正したいなら、さっき紹介したiから始まる手順を踏めばいい。
 
さて、これでcommitできたわけだけれど、ここで安心するのはまだ早い。
まだrebase作業は終わっていない。
最後に下のコマンドを入力しましょう。

git rebase --continue

これで終わり!!!
お疲れ様!!

(参考記事:rebase -i でコミットを修正する)

ケース3「あ〜。1つのファイルに複数の変更を加えてstagingしちゃったけど、変更はそれぞれ分けてcommitしたいな...」

とりあえず、git diffでどのへんを変更したのか確認しよう。
もうデータはstagingしちゃったので、変更前との差分を見たいなら--stagedをつけよう。
参考記事:git 差分を見る

git diff --staged

どれどれ??

出力結果
 diff --git a/config/routes.rb b/config/routes.rb
index 02fcf8e4..ebcf4fa1 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -18,14 +18,14 @@ Rails.application.routes.draw do

   #get  "how_to_use",     to: "static_pages#how_to_use"
   #
-  #get '/landing_page', to: 'static_pages#landing_page'
index 02fcf8e4..ebcf4fa1 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -18,14 +18,14 @@ Rails.application.routes.draw do

   #get  "how_to_use",     to: "static_pages#how_to_use"
   #
-  #get '/landing_page', to: 'static_pages#landing_page'

なるほどね。
まずはconfig/routes.rbをindexから外さないとね。
indexからファイルをunstagingするには、git reset <ファイル名>git rm --cached <ファイル名>ですね。
(参考記事:git add の取り消し方法と、関連コマンドまとめ)

git reset config/routes

さて、unstagingしたこのファイル変更箇所の一部分だけをstagingし直すには...。
ヘェ〜、git add -p <ファイル名>なんてあるのか。なるほど。
(参考記事:Git 変更のあるファイルの一部だけをコミットしたい。)

git add -p config/routes.rb

どれどれ?

出力結果
 diff --git a/config/routes.rb b/config/routes.rb
index 02fcf8e4..ebcf4fa1 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -18,14 +18,14 @@ Rails.application.routes.draw do

   #get  "how_to_use",     to: "static_pages#how_to_use"
   #
-  #get '/landing_page', to: 'static_pages#landing_page'
+

   get "/select_plan", to: "static_pages#select_plan"
   get "/update", to: "static_pages#update"

   get "/invitation_or_premium", to: "static_pages#invitation_or_premium"

-  get "/new", to: "static_pages#new"
+


(1/1) Stage this hunk [y,n,q,a,d,s,e,?]? 

hunk?
あぁ、stagingする単位のことか。
って、変更が全部1つにまとまっちゃってんじゃ〜ん!
変更を分割するには、sを入力してenter。

出力結果
 (1/1) Stage this hunk [y,n,q,a,d,s,e,?]? s
Split into 2 hunks.
@@ -18,10 +18,10 @@

   #get  "how_to_use",     to: "static_pages#how_to_use"
   #
-  #get '/landing_page', to: 'static_pages#landing_page'
+

   get "/select_plan", to: "static_pages#select_plan"
   get "/update", to: "static_pages#update"

   get "/invitation_or_premium", to: "static_pages#invitation_or_premium"

(1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?

この変更をstagingしよう。
yを入力して、enter。

出力結果
 (1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? y
@@ -22,10 +22,10 @@

   get "/select_plan", to: "static_pages#select_plan"
   get "/update", to: "static_pages#update"

   get "/invitation_or_premium", to: "static_pages#invitation_or_premium"

-  get "/new", to: "static_pages#new"
+


(2/2) Stage this hunk [y,n,q,a,d,K,g,/,e,?]?

この変更はあとでcommitするので、今はstagingしない。
nを入力してスキップ!
OK、終了終了。
 
よし、これで1つのファイルの2つの変更のうち1つだけstagingできたぞ〜!
できてるよね??
不安なので、git statusでindexを確認しよう。

git status

どれどれ〜?

出力結果
 On branch fix_bug
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   config/routes.rb

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   config/routes.rb

よしよし。ちゃんとconfig/routesrbが分割でstagingされてる。

じゃあ、まずは今stagingしたファイルをcommitしよう。

git commit -m "delete landing_page_path" -m "ランディングページのルーティングを削除"

それから残りの変更もstagingして、

git add config/routes.rb

commit!

git commit -m "delete new_path" -m "static_pages#newのルーティングを削除"

終わり!!!

ケース4「あぁ〜、いくつか前のcommit、粒度粗いし分割したい〜〜〜」

過去のcommitの分割は、「ケース1」「ケース2」「ケース3」を合わせた知識でできます。

rebase -iで、過去のcommitに移動したあと、
そのcommitをgit reset --soft HEAD^で取り消す。

その後は、すでにindexにある分割してcommitしたいファイルを、git reset <ファイル名> でunstagingしたり、
「ケース3」で紹介した手順で、1つのファイルの複数の変更を分割してstagingして、何回かに分けてcommitするなどすれば、commitの粒度を細かくできます。

このときのcommitは、commit --amendではなく、commit -m "コミットメッセージ"を使います。

最後に git rebase --continueを打ってrebaseを終了し、
git log --onelineで確認すれば、
きちんとcommitが分割されていることがわかります。

宣伝

Terminalの英語が読めない僕とあなたのために、BooQsという学習の科学に基づいた英単語学習サービスを開発しています。
BooQsの中でもとくに人気のあるコンテンツは、NGSL(New General Service List)と呼ばれる、一般的な英文の9割を網羅した英単語帳です。
よろしければぜひ!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

変更内容を"別ブランチに"反映させない方法

はじめに

git-flowやGitHub Flowを学習し始めると、ブランチを分けてファイルを管理するようになります。
そして、変更内容がなぜか別ブランチにも反映されるなんて珍事件を一度は経験すると思います。
それではブランチを分ける意味がなくなってしまいます。

この記事では、専門用語を解説したうえで、
変更内容を"別ブランチに"反映させない方法を解説していきます。

対象者

「変更内容がなぜか別ブランチにも反映されて困っている」
「とりあえずGitの勉強をしたい」

というGit初心者向けです。

この記事では変更内容を"別ブランチに"反映させない方法について説明しています。
そのため、Gitコマンドの仕組みなどについては詳しくは説明していません。
わからない人向けに参考サイトを用意しましのでご参照ください。
では、さっそく進めていきましょう!

専門用語

Git...どのファイルを追加したか削除したかなどを管理するツール。
GitHub...Gitと提携してみんなとやり取りができるWebサイト。
ブランチ...作業を枝分かれさせる事。
masterブランチ...完成品用のブランチ。
developブランチ...開発用のブランチ。
リポジトリ...ファイルやフォルダを保存する場所。
コミット...どのファイルを追加したか削除したかなどを登録する事。
スタッシュ...コミットせずファイルを一時待避する事。

環境

OS: macOS Catelina (バージョン10.15.4)
ターミナル: Terminal.app
git version: 2.24.2

1.原因

変更内容をコミットまたはスタッシュしないで
別ブランチに移動した場合、
移動先の別ブランチにも同じ内容が反映されます。

コミットするかスタッシュして、別ブランチに反映させないようにしましょう。
スタッシュは、とあるブランチで作業中だけど別のブランチで他のことがやりたい
但し、作業が中途半端だからコミットはしたくない場合に使用します。
コミットする場合は2-1へ。
スタッシュする場合は2-2へ。

2.対処法

Gitコマンドの仕組みはこちらの記事が参考になります。
Gitでよく使うコマンド一覧

2-1.コミットする場合

コミットしたい物を全て選択するには下記Gitコマンドを入力します。

git add .

コミットするには下記Gitコマンドを入力します。
xxxにはコミットのメッセージを入力します。

git commit -m "xxx"

GitHubのリポジトリに反映するには下記Gitコマンドを入力します。
xxxにはブランチ名を入力します。

git push origin xxx

コミット後に別ブランチを移動しましょう。
ブランチを移動するには下記Gitコマンドを入力します。
xxxには移動先のブランチ名を入力します。

git switch xxx

これで完了です。

2-2.スタッシュする場合

スタッシュするには下記Gitコマンドを入力します。

git stash

スタッシュ後に別ブランチを移動しましょう。
別のブランチでの作業が終わったら、
元のブランチに移動しましょう。
ブランチを移動するには下記Gitコマンドを入力します。
xxxには移動先のブランチ名を入力します。

git switch xxx

退避した変更内容を戻しましょう。
まずは、退避した作業の一覧を確認しましょう。
確認するには下記Gitコマンドを入力します。

git stash list

下記のような表示が出てきます。
xxxは変更作業を行なったブランチ名です。

stash@{0}: WIP on xxx
stash@{1}: WIP on xxx
.
.
.
stash@{5}: WIP on xxx

退避した作業を戻すには下記Gitコマンドを入力します。
例)stashの0番を戻すときはxxxには0を入力して下さい。
例)stashの5番を戻すときはxxxには5を入力して下さい。

git stash apply stash@{xxx}

これで完了です。

おわりに

お疲れ様でした!

これでこの問題を対処できるようになりましたね。

この記事をきっかけに、より良いGit生活が送れていただければ嬉しいです!

参考


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gitコマンドでワーキングツリーがクリーンかどうか判定する

用語について

  • ワーキングツリー …… リポジトリからチェックアウトされたファイルのツリー
  • クリーン …… ワーキングツリーの内容がHEADが指すリビジョンと一致していることを表す
  • ダーティー …… ワーキングツリー内に、コミットされていない変更が含まれていることを表す

やり方

おそらく、可能なやり方はたくさん考えられると思いますが、ふつうはこれを使えばよさそうというやり方を紹介します。
また、シェルスクリプトなどで機械的に判定を行う上で便利なやり方を紹介します。

なお、動作確認時のgitのバージョンはv2.17.1です。

gitで追跡されていないファイルを含めて判定する

当然 git status でわかるわけですが、そのままでは機械的に扱うには少々不便です。
--short|-s オプションを使うと出力がよりシンプルな扱いやすい形になります。

$ git status -s
 D bar.txt
D  baz.txt
 M foo.txt
?? new.txt

上の出力結果で、 D は削除されたものを、 M は差分のあるファイルを、 ?? はuntrackedな(git管理外の)ファイルを表しています。
ワーキングツリーがクリーンなときには何も表示されないところが、コマンドで判定する上で嬉しいポイントです。

従って、シェルスクリプト上では例えば次のように判定できます:

判定例
if [ $(git status --short | wc -l) -ne 0 ]; then
  # dirty!
fi

他のやり方としては、 git add コマンドを -N|--intent-to-add オプションを付けて実行した上で、 git diff コマンドを使う、といったやり方もあるようです。
こちらについては、後掲のreboooot․netさんの記事をご覧ください。

gitで追跡されているファイルのみを対象とする

この場合、 git status だけでなく、 git diff コマンドでも判別できます。
機械的な正否判定には git diff --quiet が便利です。

判定例
if ! git diff --quiet; then
  # dirty!
fi

--quiet オプションを付けると一切の出力をせず、 --exit-code オプション同様に差分のあるときに exit 1 してくれるので、検出が可能です。

参考記事

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む