Githubに2000万Commitをpushしてみる

warning

Githubに大量のCommitをすることは利用規約等で禁止された行為ではありませんが、アカウントが停止されるリスクも0ではありません。

また本記事はそう言った行為を推奨するものではありません。

はじめに

Githubを使っていれば一度は謎に大量のコミットをして、草を生やしまくっている人を見たことがあるだろう。

ほとんどの人はその草にちゃんとした意味がある、一つ一つがちゃんと意味を持っているのだ


だが、ごく一部意味を持たない草を持っている人がいる、そういう草は見ればすぐわかる

意味を持たない草の画像

info

上のコミットは最小限なので本当に何もコミットされてませんが、多くの場合現在の時刻などの情報がコミットされています


今回はそんな草について書こうと思う


Githubに溢れる草生成ツール


また、githubでfake commitと調べると、偽のコミットを生成する多くのリポジトリが見つかる


大抵の場合そのようなツールはgitを呼び出している。

例えばjmfayard/timetravelerは過去に遡って、恰も毎日コミットしているように見せかけるシェルスクリプトだが、内部ではgit commitコマンドを使用してコミットされている


git commitコマンドはいつくかの引数を持っている


--date コミットを特定の日時でする

--allow-empty 空のコミットを許容する

--allow-empty-message 空のコミットメッセージを許容する


上がコミットを生成するツールによく使われる引数だ

これさえ知っていれば上記のようなツールが作成できる



これを見た多くの人がコミット数を青天井に増やし、世界一を目指したいと思うがここで一つの問題が発生する

git commitコマンドは遅いのだ


git commitの速度


git commitを打つだけでcommitしてくれる便利なコマンドだ


速度を見てみると1時間に20万のペースでコミットできていたが一つ問題がある

githubで一番コミット数が多いリポジトリは61Mだ


info

61Mコミットを持つリポジトリはNathanwoodburn/test-commitだが、現在は削除されている

その次に多いのはBullshitDays/race-pi-maxio-1で14Mだ


一時間で30万コミットのペースで6100万にしようと思ったら単純計算で203時間もかかる

実際には対数的?なのでそれ以上の時間がかかる、とてもじゃないがそんなに待てない


info

線形的な気がするが、コミット数が増えると速度が微妙に低下してる気がする


思った。

そうだRustを使おう、Rustを使って自力でCommitを生成すればいいものができるに決まってる


RustでCommitする


gitは.gitフォルダーに全ての情報を保存している

つまり、このフォルダーをいじれば、任意に情報を書き換えられるわけだ


info

この記事ではgitの仕組みについて詳しくは書かない、公式が簡単な説明をしているのでそちらを見ることを強くお勧めする


gitがコミットする流れ


実際にコミットする時のgitの動きとしては以下のようになっている


info

今回は空のコミットをする前提で書いている


1

コミットのヘッダーとデータが作成される

2

1で作成された物のsha-1がidとなる

3

.git/objects直下にidの最初の二文字がディレクトリ名に、最後の38文字がファイル名となってり、zlibで圧縮され保存される

4

.git/logs/HEAD.git/refs/heads/<ブランチ名>にログが書き込まれる

5

.git/refs/heads/<ブランチ名>に最終コミットのidが保存される


つまり、上のことを全てRustでやれば、擬似的にコミットを生成できることになる


Rustで書いてみる


1. コミットを作る

コミットは以下のようになっている

本来はもっと複雑だが、今回はコミットの数を稼ぐだけなので最小限の構造にしている

1

2

3

4

5

6

tree <tree>
parent <parent>
author applemango <[email protected]> 1708735321 +0900
committer applemango <[email protected]> 1708735321 +0900


コードにするとこんな感じだ

1

2

3

4

5

6

7

8

9

pub fn create_base_header(tree: String, parent: String) -> String {
    let email = "[email protected]";
    let name = "applemango";
    let time = "1708735321 +0900";
    let author = format!("{} <{}> {}", name, email, time);
    let committer = author.clone();
    let f = format!("tree {}\nparent {}\nauthor {}\ncommitter {}\n\n", tree, parent, author, committer);
    f
}

さらにそのサイズを計算して、それを入れる

1

2

3

4

5

6

pub fn create_header(tree: String, parent: String) -> String {
    let header = create_base_header(tree, parent);
    let bytes = header.as_bytes().len();
    let f = format!("commit {}\0{}", bytes, header);
    f
}

warning

\nと\0が混在してるのは意図的です

最終的には以下のようになる

1

2

3

4

5

6

7

commit 214
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent ae01c88095bd0e4002905ce95f799d82d8242260
author applemango <[email protected]> 1708735321 +0900
committer applemango <[email protected]> 1708735321 +0900


2. ハッシュを生成する

hashがkeyとして使用されるので、ハッシュを生成する

1

2

3

4

5

6

7

8

pub fn generate_hash(header: String) -> String {
    let store = header;
    let mut hasher = Sha1::new();
    hasher.update(store.clone());
    let sha1 = hasher.finalize();
    let sha1_str = format!("{:x}", sha1);
    sha1_str
}

3. ファイルとして保存する

コミットはzlibで圧縮され保存されるので、まずは中身を圧縮する

header.as_bytes()を渡せば良い

1

2

3

4

5

6

pub fn zlib_deflect(bytes: &[u8]) -> Vec<u8> {
    let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
    encoder.write_all(&bytes).unwrap();
    let bytes = encoder.finish().unwrap();
    bytes
}

次に圧縮したものを保存する

これは簡単で、適当にファイルのパスを作って、保存するだけで良い

1

2

3

4

5

6

7

8

9

10

11

12

13

14

pub fn create_path(base: String, hash: String) -> String {
    let directory = hash[0..2].to_string();
    let file = hash[2..].to_string();
    let directory_path = format!("{}/{}", base, directory);
    let p = format!("{}/{}", directory_path.clone(), file);
    if !directory_is_exists(directory_path.clone()) {
        let _ = create_dir(directory_path).unwrap();
    }
    p
}
pub fn write_bytes(path: String, bytes: Vec<u8>) {
    let mut file = File::create(path).unwrap();
    file.write_all(&bytes).unwrap();
}

4. logsに書き込む

logs/refs/headslogs/HEADも以下のように保存されている

1

2

0000000000000000000000000000000000000000 ae01c88095bd0e4002905ce95f799d82d8242260 applemango <[email protected]> 1708735269 +0900	commit (initial):
ae01c88095bd0e4002905ce95f799d82d8242260 848a86c39afbd6dac9cedde748b1b22ef69d68e9 applemango <[email protected]> 1708735321 +0900	commit:

つまり、下のように書けば良い

1

2

3

4

5

6

7

8

9

10

pub fn create_head_message(last_commit_id: String, commit_id: String) -> String {
    let email = "[email protected]";
    let name = "applemango";
    let time = "1708735321 +0900";
    format!("{} {} {} <{}> {}	commit:\n", last_commit_id, commit_id, name, email, time)
}
pub fn append_head(path: String, last_commit_id: String, id: String) {
    let mut f = File::options().append(true).open(path).unwrap();
    write!(&mut f, "{}", create_head_message(last_commit_id.clone(), id.clone()));
}

5. 適当に最終コミットを書き換える

.git/refs/heads/<ブランチ名>を変えるだけで終わる

1

2

3

pub fn update_refs_heads(path: String, last_commit_id: String) {
    std::fs::write(Path::new(&path), format!("{}\n", last_commit_id)).unwrap();
}

動かしてみる

あとは上のコードを適当に変えるだけです

合わせるだけですが、長くなりそうなので、詳しくは下のリポジトリを見てください


実際に動かすとgitコマンドの50倍の速度でコミットを生成していることがわかります


終わりに

一番コミット数が多い人になれたので満足しました。

( RustじゃなくてC++で作ればよかったな、と少し思った )

このドキュメントどう?

emoji
emoji
emoji
emoji