1
/
5

Gitをよく知って、うまく使おう

こんにちは!プログリットのエンジニア・マネージャーの島本( @diskshima )です!
今回はみなさんが毎日使っているバージョン管理ツールのGitについて、仕組みを詳しく知りたいな、と思い、少し詳しく調べたので、まとめてみました。

読み終わった頃には

  • Git面白い!
  • このテクニック便利!

などと思っていただけたら幸いです。

記事の流れ

今回は主に

  • そもそもGitって何よ?(歴史編&特徴編)
  • そもそもGitって何よ?(中身編)
  • Gitオススメテクニック編
  • Gitの豆知識

という流れで紹介していきたいと思います。

そもそもGitって何よ?(歴史&特徴編)

では始めにGitについて、軽く歴史と特徴を紹介したいと思います。

Git の歴史

知っている方も多いと思いますが、Gitの開発者はLinux Kernelを作ったLinus Torvaldsです。

そもそもLinux Kernelの開発でもともと使っていたBit Keeperというツールの無料ライセンスが無くなってしまうのを機に代替としてLinusが作ったものらしいです。

驚愕なのはその実装スピードです。作り始めてから

  • Git自体をセルフホスト(GitのソースコードをGitで管理する)まで、3日
  • LinuxのカーネルをGitに移行するまで、2週間

しかかかっていないとか… 😲

本人による回答だとそれよりも少ないかも、ということすら言ってます⬇️
https://marc.info/?l=git&m=117254154130732

> Now, exactly when I started git development (ie how long it took before it 
> got to that self-hosting stage), I can't remember. I'd say about two
> weeks, probably.

Actually, it must have been less than that.

次に少し(自分が思う)特徴を紹介させてください。

Gitの特徴1:分散型

一番大きい特徴は

分散型バージョン管理(Distributed Version Control System)

である点だと思います。

「Git & GitHubアタリマエ」な時代では少し意外に思う方もいるかも知れませんが、もともとソースコード管理と言えば中央集権型で、CVS、Subversion、Perforceなどの中央集権型のツールがよく使われていました(もちろん今も使われていますが)。中央集権型のツールでは、中央にサーバ(やファイル)があり、それに対して、作業者が「チェックアウト」コマンドを送って、排他的に編集(他の人が触れないようにロックしておく)などをしておりました。メンバーの役割や触れるコンポーネントが綺麗に分かれている場合は特に問題ないのですが、いざ同じファイルを触れるときなどはお互いに予めコミュニケーションを取ったりと煩雑な面がデメリットです。

それに対してGitは全員同じ立場で同じ内容を持っているという「分散型」の考え方を前提とし、変更を伝えるにはお互いに差分(パッチ)を送る、という発想で動いています。
もともとオープンソースな開発ではお互いにパッチファイルをメールで送り合ったりするのが普通だったそうなので、その流れを組んでいるんだと思います。

Gitの特徴2:高速

冷静に考えると、Gitって、すごく処理が速くないですか?
cloneとかcommitもそうなのですが、git checkout/switchでブランチを切り替えるときも、100個以上ファイルの内容が異なってても「一瞬」の感覚です。SubversionやPerforceなどのバージョン管理ツールを使ってきた自分としては超速と言っても良いぐらいです。
初めて使ったときは、そのスピードが衝撃でした。

Linusは「スピード」をとても意識しており、Gitの開発においても「パッチを1つ適用するのは3秒以上かかってはいけない」というルールを設けているらしいです。
また、Gitの処理速度に関して、JGit(JavaによるGitの実装)の作者がJGitと比較した話が書いていました ⬇️

'Re: Why Git is so fast (was: Re: Eric Sink's blog - notes on git,' - MARC

- JGit struggles with not having mmap()
- JGit struggles with not having unsigned types in Java.
- C Git takes for granted that memcpy(a, b, 20) is dirt cheap

以上のような具体的な話も書いてあったのですが、最初に書いてある

we rely on a lot of small tweaks in the C git code to get really good performance.

という普段から細かいところでこだわっている「チリツモ」な部分が一番大事なのかもしれませんね!

そもそもGitって何よ?(中身編)

さて、歴史とか特徴の話は以上として、Gitの中身について調べたことを書いて行きたいと思います。

すべては.git/の中に

まずは前提として、Gitのデータ(コミットとか、設定とか)はレポジトリの .git/ディレクトリ内に保存されています。

% ls .git/
FETCH_HEAD ORIG_HEAD description index logs packed-refs
HEAD config hooks info objects refs

以下の説明もこのディレクトリ内での話が中心になりますので、みなさんも手元のレポジトリの.git/を横で眺めながら読み進めていただくと面白いと思います。

GitはただのObject Databaseである

Gitは機能があるのですが、その核にあるのはGit Objectと呼ばれているオブジェクトの集まりになります。ディレクトリとしては.git/objects/内に保存されています。

% tree .git/objects/
.git
|
:
├── objects
│ ├── 01
│ │ └── c5dd98dff669f0447468f0bc8a5a3fa56c9627
│ ├── 04
│ │ └── 8f3a9bbf2879e57e582d86e2bb07a085bf556c
│ ├── 05
│ │ └── 192632a5c8dd66431651930368a1e4f677377f
│ ├── 06
│ │ └── 516ba9e068ffcdc0962708a0dd76217382a718
:

treeコマンドはディレクトリをツリー構造で見せてくれるコマンドです。代替の*nix系コマンドラインでは用意されておりますのでインストールしてみてください)

Gitのソースコード履歴は、このobjectで構成されており、以下の種類があります。

  • commit
    • 作者、作成日時、コミッター、コミット日時、署名、などのメタデータに加え、tree objectのハッシュ値を持っています。
  • blob
    • ファイルのデータを持っています。
  • tree
    • ディレクトリデータを保持しており、ファイルの名前と権限、中身(blob)へのハッシュ値を持っています。
  • annotated tag
    • アノテーション付きタグです。 git tagだけで作ったタグとは異なり、誰が作ったか、いつ作ったかなどのメタデータを持たせたタグです。

文字だけで説明されてもイメージしにくいと思うので、例を交えて説明したいと思います。

試しにファイルを追加

最初にファイル追加をしてみたいと思います。
実行前に.git/を確認すると…。

% tree .git
.git
|
:
├── objects # <-- 最初は何もない
│ ├── info
│ └── pack
:

早速ファイルを作ってみます。

% echo "Hello world" > hello.txt  # ファイルを作る
% git add hello.txt # ファイルをaddする。
%tree .git
|
:
├── objects
│ ├── 80
│ │ └── 2992c4220de19a90767f3000a79a31b98d0df7 # <-- こちらが作成されます
:

このobjects配下に作成されたファイルを見てみます。

% cat .git/objects/80/2992c4220de19a90767f3000a79a31b98d0df7
xKOR04bHW(/IBi

意味不明な文字列が出ましたね 😆
実は中身はzlibで圧縮されているので、中身を見たいときはgit cat-file -pという専用コマンドで見る必要があります。

% git cat-file -p 802992c4220de19a90767f3000a79a31b98d0df7
Hello world

objectの種類(コミット、データ等)を知るには同じコマンドの-tオプションで見られます。

% git cat-file -t 802992c4220de19a90767f3000a79a31b98d0df7
blob

普通のファイル(ディレクトリとかではなく)なので、blobと表示されますね!

試しに他のも見ていきましょう。
みなさんも馴染みの深いコミットハッシュを渡すと…

% git cat-file -p d2dd658
tree a50b30eb6b223aef893c367a0b93e9a5b21f155f # <-- ここがディレクトリのobject
author Daisuke Shimamoto <daisuke.shimamoto@progrit.co.jp> 1656302334 +0900
committer Daisuke Shimamoto <daisuke.shimamoto@progrit.co.jp> 1656302334 +0900

Initial commit

なんとなく見慣れたコミットの情報ですね。
この中のtreeが実はコミットの時点でのレポジトリのルートディレクトリを示しています。
こちらも見てましょう。

% git cat-file -p a50b30eb6b223aef893c367a0b93e9a5b21f155f
100644 blob 802992c4220de19a90767f3000a79a31b98d0df7 hello.txt # <-- ここがhello.txt の中身を指しています。

ファイルリストですね。
よく見ると、唯一のエントリーである、802992c4220de19a90767f3000a79a31b98d0df7は先程見ていたファイルの中身やつですね!
試しにもう一度見てみます。

% git cat-file -p 802992c4220de19a90767f3000a79a31b98d0df7
Hello world

当たり前ですが、中身が出ますね!

こういう形でGitではコミット、ディレクトリ、ファイルをそれぞれGit objectとして.git/objectsですべて管理しています。

「ブランチ」はコミットの流れの一番先

コミットの次にみなさんがよく使うのは ブランチ だと思うのですが、これも.gitディレクトリの中を見ると見えてきます。

git checkout -b feature-01
echo "Hello git" > hello_git.txt
git add hello_git.txt
git commit -m 'Commit hello git'

.git/refs/headsの下に新しいファイルが作成されるのが分かります。

% tree .git
.git/
├── COMMIT_EDITMSG
:
├── refs
│ ├── heads
│ │ ├── feature-01 # <-- 新しくできたやつ
│ │ └── main # <-- もともとのやつ
│ └── tags

この新しいfeature-01の中身を見ると…

% cat .git/refs/heads/feature-01
a9f76147da554dc691f682f0fd6e241c3229cc31

見慣れた形式ですね。
こちらも中身を見ると先程のコミットであることが分かります。

% git show a9f76147da554dc691f682f0fd6e241c3229cc31
commit a9f76147da554dc691f682f0fd6e241c3229cc31 (HEAD -> feature-01)
Author: Daisuke Shimamoto <daisuke.shimamoto@progrit.co.jp>
Date: Mon Jun 27 13:02:30 2022 +0900

Commit hello git

つまり、ブランチはコミットへのショートカットみたいなものなんです。

普段はブランチの最新コミット(HEADと言います)しか使わないのですが、⬆️のように、ただその先のコミットのことなので、反対に特定のコミットハッシュを指定してチェックアウトすることも可能です。

% git checkout a9f76147da554dc691f682f0fd6e241c3229cc31
Note: switching to 'a9f76147da554dc691f682f0fd6e241c3229cc31'.
:

これをdetached HEAD stateと言って、ブランチの先っぽでないような(=途中の)コミットを指している状態です。
ちなみに、ここでコミットとか追加してしまうとすぐに「路頭に迷う」ので、長居は無用です😁
用が済んだら、すぐに元のブランチをチェックアウトしましょう。

remoteとはなんぞや?

あと、よく聞く単語として remoteって単語がありますね。
このremote自分のローカルディレクトリでないもの を指しています。
なお、この場合はGitHubにoriginという名前を付けているので、git remote show originというコマンドで見ることができます ⬇️

% git remote show origin
* remote origin
Fetch URL: git@github.com:xxxxx/xxxxxxx.git
Push URL: git@github.com:xxxxx/xxxxxxx.git
HEAD branch: main
Remote branch:
main tracked
Local branch configured for 'git pull':
main merges with remote main
Local ref configured for 'git push':
main pushes to main (up to date)

皆さんの普段使われているレポジトリもこんな感じになっていると思います。

このremoteって実は何も特殊なものではなくて…

自分のローカルと同じオブジェクトとかの情報を持っている

だけです。

例えば先程から何度か見ているmainブランチがこちら⬇️

% git show main
commit d2dd658f8e787b22241693a40747c647c935473b (HEAD -> main, origin/main)
Author: Daisuke Shimamoto <daisuke.shimamoto@progrit.co.jp>
Date: Mon Jun 27 12:58:54 2022 +0900

Initial commit

diff --git a/hello.txt b/hello.txt
new file mode 100644
index 0000000..802992c
--- /dev/null
+++ b/hello.txt
@@ -0,0 +1 @@
+Hello world

remotemainブランチを見ると…⬇️

% git show origin/main
commit d2dd658f8e787b22241693a40747c647c935473b (HEAD -> main, origin/main)
Author: Daisuke Shimamoto <daisuke.shimamoto@progrit.co.jp>
Date: Mon Jun 27 12:58:54 2022 +0900

Initial commit

diff --git a/hello.txt b/hello.txt
new file mode 100644
index 0000000..802992c
--- /dev/null
+++ b/hello.txt
@@ -0,0 +1 @@
+Hello world

同じですね!

git pushgit pullするときは、このオブジェクトをGitHubにアップロードしたり、ダウンロードしてきたりしているだけなんです。

まとめ

つまり、結局のところ、gitコマンドもGitを操作するプログラムも、

  • ディレクトリのファイルを変更したり
  • 別のコンピュータからアップロード・ダウンロードしたり

しているだけ、ということが分かると思います。
仕組みとしては意外とシンプルですね!

Gitオススメテクニック編

さて、長い前置き(?)になってしまいました。
ここから本題の「オススメテクニック」をいろいろ紹介したいと思います!

gitはgで十分

まずは、いきなりgitではなくシェルの話なのですが、いつも使うので、gと短くしましょう!

% alias g=git

~/.bashrc~/.zshrcに入れておいてください。

そうすると

% g status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean

🎉 2文字、得しましたね!🎉

statusなんてstで十分

statusって打つのも面倒ですよね!
Gitでは、サブコマンド(pushとかpullとか)もショートカットを作れます!

% g config alias.st status

これで

% g st

git statusと同じことができちゃいます!
🎉 更に、4文字の得ですね!🎉

git config --globalで永続化

ただ先程のコマンドでは今のシェルで一時的にしか使えないので、--globalで永続化させておくのがオススメです。

% git config --global alias.st status

このgit config --globalで指定したものは~/.gitconfigに保存され、いつでも使えるようになります。
実際に、~/.gitconfigファイルに反映されているのが分かると思います(もちろん、直接~/.gitconfigを編集しても同じ効果が得られます)。

$ cat ~/.gitconfig
:
[alias]
st = status
:

Rebaseは怖くないよ!

次に紹介したいテクニックはrebase(リベース) です。
「Rebase怖い」という声をよく聞くのですが、Gitを使い込めば使い込むほど「必須」になるコマンドです。特に複数人で同じレポジトリを利用している場合などは必ず必要なシーンに遭遇すると思います。

プログリットですし、この英単語を分解してみます。

rebase = 「re(もう一度、やり直す、等)」+ 「base(ベース)」

となります。後者の「base」の意味が気になりますね。

baseは英語では「根元」という意味です。
Gitでも同じ意味で使っていて「今のブランチを作成した元」のことになります。

% git log --graph --date=human --decorate=short --pretty=format:'%h ...'
* 16f1d74 ...
|\
| * 8a917df ... # <-- このブランチにおける
| * 5944ea6 ...
|/
* 659c310 ... # <-- この位置がbase
|\
| * fc9d286 ...
| * 12dbb85 ...
| * 073ba25 ...
:

⬆️の話をまとめると、rebaseは

またbaseを決め直す

という意味になります。
例えば…

# 新しいブランチを作ってコミットを追加する。
git checkout -b feature-02
echo "Feature 2" > feature2.txt
git add feature2.txt
git commit -m "Add feature2"

# メインに戻って、コミットを追加する。
git checkout main
echo "New commit on main" >> hello.txt
git add hello.txt
git commit -m "Update hello.txt"

これで、mainブランチとfeature-02ブランチの両方にコミットがある状態になりますね。

さて、この後にfeature-02ブランチでmainの最新状態に合わせたいときにどうするか?
もちろんmainをマージでも良いですね。

% git checkout feature-02
% git merge main

でも、コミット履歴汚れて、読みにくくなりません?

% git log --graph --date=human --decorate=short --oneline
* e457ad1 (HEAD -> feature-02) Merge branch 'main' into feature-02 # <-- 見にくい!!
|\
| * f631f4a (main) Update hello.txt # <-- どっちが
* | 0a1680a Add feature2 # <-- どっちのブランチだっけ??
|/
* d2dd658 Initial commit

です。そんなときにこそ、rebase

% git checkout feature-02
% git rebase main

ほら、スッキリ!

% git log --graph --date=human --decorate=short --oneline
* 25ef0e1 (HEAD -> feature-02) Add feature2 # <-- ここがmainを新しいbaseにした状態
* f631f4a (main) Update hello.txt
* d2dd658 Initial commit

GitHubプルリクエストでも出てくるよ!

Rebaseに関連して、最近GitHubプルリクエストのマージボタンに選択肢が増えたの知ってますか?
こういうやつです⬇️

  • Create a merge commit
    一番ベーシックなやつで、普通にマージコミットを作るやりかたです。先程の例でもブランチが見にくくなるやつですね。
  • Squash and merge
    プルリクエストのコミットを1つにまとめて(squashして)から、マージします。この場合はマージコミットは作られず、マージ先のブランチにまとまったコミットが1つだけ加わります。
    マージ先のブランチがとても綺麗になりますが、個別の変更が分かりにくくなるデメリットがあります。
  • Rebase and merge
    これが先程のrebaseと同じことで、baseをマージ先のブランチに変更してからコミットをすべてマージ先のブランチに乗せます。
    1つ前のsquashとの違いはすべての変更がマージ先ブランチに乗るので、履歴が残ります。(ただ、"fix"とだけ書いたような汚いコミットとかもそのまま乗るので、そのデメリットはあります)

詳しくはGitHubの公式ドキュメントを是非ご参照してください⬇️
プルリクエストのマージについて - GitHub Docs

ちなみに昔は一番最初のCreate a merge commitしかなくて、次がSquash and mergeが追加され、最後に追加されたのがRebase and mergeです。
僕は前職でsquashがルールだったのですがボタンがなかったので、手動で

% git fetch
% git rebase -i origin/develop
# <-- ここで、手動でコミットをsquash
% git push -u origin develop

みたいにして、ブランチ(及びプルリクエスト)の中身を1つにして、直接マージ先のブランチにプッシュしていました。
メインブランチにそのままpushするかと今思うと、なかなか危険なことしてましたね 😆

余談:baseといえば、AYB

Baseと言えば、昔インターネットで「All your base are belong to us.」(通称AYB)って流行りましたね。

https://ja.wikipedia.org/wiki/All_your_base_are_belong_to_us

こういうことが二度と起きないよう、プログリットとシャドテンを広めていきたいな、と思います 😄

reflogの活用✨

個人的には今回の目玉機能だと思っているのがreflogです!
これを知っているだけで、きっとあなたは救われます!!

これも試しに分解してみると

reflog = 「ref(=reference、見ているブランチとかコミット)」+「log(ログ)」

となります。
つまり、 見ているブランチのログ になります。

試しにgit-internalsレポジトリで実行してみると…(自分の記録なので、人によって表示は異なります)

25ef0e1 (HEAD -> feature-02) HEAD@{0}: rebase (finish): returning to refs/heads/feature-02
25ef0e1 (HEAD -> feature-02) HEAD@{1}: rebase (pick): Add feature2
f631f4a (main) HEAD@{2}: rebase (start): checkout main
0a1680a HEAD@{3}: checkout: moving from 0a1680ae42a49c716635957f68bb4debc0c1df26 to feature-02
0a1680a HEAD@{4}: checkout: moving from e457ad1dc43342b992d8f23da1455f44e2ded72d to 0a1680
e457ad1 HEAD@{5}: reset: moving to feature-02
0a1680a HEAD@{6}: checkout: moving from feature-02 to 0a1680a
e457ad1 HEAD@{7}: merge main: Merge made by the 'ort' strategy.
0a1680a HEAD@{8}: checkout: moving from main to feature-02
f631f4a (main) HEAD@{9}: commit: Update hello.txt
d2dd658 (origin/main) HEAD@{10}: checkout: moving from feature-02 to main
0a1680a HEAD@{11}: commit: Add feature2
d2dd658 (origin/main) HEAD@{12}: checkout: moving from main to feature-02
d2dd658 (origin/main) HEAD@{13}: checkout: moving from feature-01 to main
a9f7614 (feature-01) HEAD@{14}: checkout: moving from main to feature-01

ちょっと見にくいのですが、だいたい僕がどのブランチに切り替えて、どのコミットをcheckout して、どれをrebaseしたか、とかすべて残っています。

ただの作業履歴に近いのですが、僕が思う一番のメリットは

一瞬でもcheckoutしたコミットがすべて記録されている

つまり、rebaseとかいろいろやっていて「あれ、あの変更のコミットがなくなったかも。5時間の作業が消えたーー」というときに

% git reflog

と叩くと、だいたい出てきます。
表示が読みにくいので探すのは大変かもしれませんが、5時間よりは短い時間で探せると思います😄

ファイルのこの部分だけコミットに入れたいなぁ

コミットを作成する際に、みなさんはどうしてますか?

% git add .
% git commit -m 'fix' # <- メッセージ短か!

みたいなコミット作ってませんか?

コミットはメンバーや将来の自分への「メッセージ」です!綺麗にしていきたいですね!

で、そんなときに使えるテクニックですが、

% git add -p -- ファイル名

です。これをやると、Gitがファイルの変更箇所ごとに「コミットに入れます?」みたいな聞いてきてくれます。
これを使えば、

% git add -p -- hello.txt
diff --git a/hello.txt b/hello.txt
index 1b97a5e..884a2af 100644
--- a/hello.txt
+++ b/hello.txt
@@ -1,2 +1,8 @@
Hello world
+
+Change in the middle # <-- 1つ目の部分
+
New commit on main
+
+
+Change at the end # <-- 2つ目の部分
(1/1) Stage this hunk [y,n,q,a,d,s,e,?]? s # <-- Gitから「どうする?」と聞かれて、 s (split) と回答。
Split into 2 hunks.
@@ -1,2 +1,5 @@
Hello world
+
+Change in the middle # <-- 1つ目の部分だけ出てくる
+
New commit on main
(1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? y # <-- 「(1つ目)どうする?」と聞かれるので、y (yes) でstageさせる。
@@ -2 +5,4 @@
New commit on main
+
+
+Change at the end # <-- 2つ目の部分だけ出てくる
(2/2) Stage this hunk [y,n,q,a,d,K,g,/,e,?]? n # <-- 「(2つ目)どうする?」と聞かれるので、n (no) でstageさせない。

この状態で 1つ目だけstage(コミット準備段階)になります。

% git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
(use "git push" to publish your local commits)

Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: hello.txt # <-- 1つ目の部分だけ追加されている。

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: hello.txt # <-- 2つ目の部分は追加されていない

こうすれば、必要な部分だけちゃんとメッセージを付けてコミットしやすいですね!
コードレビューする方にとってもそうですし、後になって振り返るときも分かりやすくなると思います。

ちなみにさりげに⬆️で初めて使った「--」ですが、「ここから先はオプションやフラグじゃなくてファイル名です」という宣言になります。
Gitのコマンドって、機能があんまりに多くて、サブコマンドやらファイル名やらを並べていってしまうと、見分けが付かなくなる場合もあり、Gitからエラー表示が出たり(Gitにとっても見分けが付かないことがあります)ので、ファイル名の前には付けることをオススメします!

あのときの、あのファイルが欲しい

いろいろブランチを変えている中、「あー、あのときの、あのファイルだけ使いたいんだよなぁ」ってことありますよね?

そのファイルがあったときのブランチやコミットが分かっていれば、以下のコマンドですぐに呼び戻すことができます!

% git checkout feature/issue-283 -- src/file01.js

その他のネタ

最後に、Gitに関する小ネタを紹介させてください。

マージのブランチ数に上限なんてない。

みなさんが知っているマージって、通常developmasterにマージとか、2ブランチ間のことが多いと思いますが、実際には「何ブランチ」でも行けるそうです。

多いものは Octopus Merge 🐙と呼ばれるらしいです。

過去に記録されている最大と思われるものはLinux Kernelにおける66個のマージだそうです!!

さすがのLinusも「これはOctopusじゃなくて、Cthulhuマージだ」と言ってました(Cthulhuは海外の小説に出てくる化け物だそうです)⬇️

'Re: [GIT PULL] regulator updates for v3.13-rc1' - MARC

It's pulled, and it's fine, but there's clearly a balance between
"octopus merges are fine" and "Christ, that's not an octopus, that's a
Cthulhu merge".

まとめ

いかがでしたでしょうか?
いろいろと書いてしまいました全部一気に使わなくても全然問題ないと思っています。
普段の開発の中でGitを使っていて「あ、そういえば、こういうのあったな」というときにちょっと使ってみて、便利そうなら、意識的に使うようにしていくうちにどんどん慣れていく形が良いと個人的には感じています。

さて、この話、実は社内の勉強会で僕が説明したものをベースにしています。実際の勉強会で実際にコマンドラインを見せながらやって、よりインタラクティブな感じでした。
普段からこういう話ができるような環境に入ってみたい方は是非、話を聞きに来てください!⬇️

株式会社プログリット's job postings
33 Likes
33 Likes

Weekly ranking

Show other rankings
Invitation from 株式会社プログリット
If this story triggered your interest, have a chat with the team?