標準愚痴出力

個人的なIT作業ログです。もしかしたら一般的に参考になることが書いているかもしれません(弱気

マイクラデータのバックアップの落ち着きどころ

イクラデータのバックアップをしてみたところ二回目以降がかなり速いので、もうこれを採用することにした。その為の環境変数の設定や、バッチファイルを用意する。

.nyagos 追加行

nyagos.env.RESTIC_REPOSITORY=nyagos.pathjoin(nyagos.env.USERPROFILE,"OneDrive\\Documents\\minecraft-backups")
nyagos.env.RESTIC_PASSWORD="********"

環境変数の定義は、バッチファイルの方に書いてもよかったのだが、コマンドラインで直接 restic snapshots などでバックアップ一覧を見る場合もあるので、こちらに書いておくことにした。

Backup-minecraft.cmd

バックアップをとって、過去3回以前の差分は捨てるようにする

setlocal
    pushd "%APPDATA%\.minecraft"
        restic.exe backup saves
        restic.exe forget --keep-last 3 --prune
    popd
endlocal

念のためテスト

restic を疑っているわけではないが、万が一「全バックアップは初回のみで、以降は全て差分バックアップ。初回を削除すると差分が無意味になる」という方式だったりすると、目もあてられないので。

比較は拙作の verifyarc を使う。このツール、アーカイブファイル vs ファイルシステムという比較しかできないので、restic restore で展開したファイル一式を一旦tar化してから、比較する。

C:> cd ~/tmp
C:> restic.exe restore -t . 19f2d6d5
C:> tar cf - saves | verifyarc.exe -C %APPDATA%\.minecraft -

ARCHIVE: [OK] saves/TEST/icon.png
ARCHIVE: [OK] saves/TEST/level.dat
ARCHIVE: [OK] saves/TEST/level.dat_old
    :中略
ARCHIVE: [OK] saves/Demo_World/advancements/17a2da24-6445-38a2-96f6-e635896edf81.json
ARCHIVE: [OK] saves/Demo_World/advancements/e6f32b0a-08ea-4a32-9034-7eabfb444178.json
FILESYS: [OK] saves\TEST\DIM-1\data\raids.dat
FILESYS: [OK] saves\TEST\DIM-1\data\raids_nether.dat
FILESYS: [OK] saves\TEST\data\raids.dat
    :中略
FILESYS: [OK] saves\TEST\session.lock
FILESYS: [OK] saves\TEST\stats\e6f32b0a-08ea-4a32-9034-7eabfb444178.json

C:>

verifyarc のログを見る限りでは、ARCHIVE 側も FILESYS 側両方とも [OK] がそろっており、問題なさそうだ。

よしよし

バックアップツール restic おためし中

以前から、マインクラフトのゲームデータのバックアップをいろいろ試行錯誤してきたが

  1. tar → zstd → curlftp
    • tarのサイズが大きすぎて、verify 時にエラーが確認される
  2. マインクラフトの標準のバックアップ先 %APPDATA%\.minecraft\backups 以下を OneDrive の下へのジャンクション化
    • OneDrive の容量がギリギリで過去1回分しか取れない (2回分も残すつもりはないが、1回目のバックアップが終わるまで、その前のバックアップは残しておきたい)

と、いろいろとうまくいっていなかったため、restic を試してみた。 scoop install restic でインストールだ。

初回バックアップ

C:> cd "%APPDATA%\.minecraft"
C:> restic.exe --repo ~\OneDrive\Documents\minecraft-backups init
C:> restic.exe --repo ~\OneDrive\Documents\minecraft-backups backup saves

すごく重い。CPU負荷には余裕があるが、ディスク負荷が 100% に張りついている。これは圧縮をあんまりしてないっぽいなぁ

  • バックアップ元:7.2G %APPDATA%\.minecraft\saves\ (3681 files)
  • バックアップ先:5.7G ~\OneDrive\Documents\minecraft-backups (605 files)
  • 初回所要時間:23分(restic) + OneDrive 同期時間

2回目のバックアップ

C:> restic.exe --repo ~\OneDrive\Documents\minecraft-backups backup saves
enter password for repository:
repository a8ee745d opened (version 2, compression level auto)
using parent snapshot d88cd20f
[0:00] 66.67%  2 / 3 index files loaded

Files:           0 new,    87 changed,  3541 unmodified
Dirs:            0 new,    15 changed,    38 unmodified
Added to the repository: 149.510 MiB (132.221 MiB stored)

processed 3628 files, 6.713 GiB in 0:16
snapshot 86b8686f saved

C:> restic.exe --repo ~\OneDrive\Documents\minecraft-backups snapshots
enter password for repository:
repository a8ee745d opened (version 2, compression level auto)
ID        Time                 Host             Tags        Paths
------------------------------------------------------------------------------------------------------------
d88cd20f  2024-02-21 10:11:35  DESKTOP-NPOTG52              C:\Users\hymkor\AppData\Roaming\.minecraft\saves
86b8686f  2024-02-22 03:41:16  DESKTOP-NPOTG52              C:\Users\hymkor\AppData\Roaming\.minecraft\saves
------------------------------------------------------------------------------------------------------------
2 snapshots

16秒!? すごく速い。これならプレイ後、毎回バックアップ取れる。いいな、これ

nyagos を jj 管理にしたところ、jj log がすごく長くなってしまった

太古のブランチのコミットが出てる

jj log -h によると、

$ jj log -h
Show commit history
    :(中略)
Options:
  -r, --revisions <REVISIONS>  Which revisions to show. Defaults to the `revsets.log`
                               setting, or `@ | ancestors(immutable_heads().., 2) |
                               heads(immutable_heads())` if it is not set
:

デフォルトのログ出力の範囲は @ | ancestors(immutable_heads().., 2) | heads(immutable_heads()) という revsets で表される範囲らしい。この指定で、どうして、タグ: 4.2.5_1 〜 branch: r4.2@origin の中身が全部出てしまうのか、よく分からない。

うっとうしいので、4.4.1_0 以降だけを出すようにしよう。

$ jj config set --repo revsets.log "( @ | ancestors(immutable_heads().., 2) | heads(immutable_heads()) ) & 4.4.1_0::"

$ jj log
@  lzpznyvv iyahaya@nifty.com 2024-02-19 11:06:53.000 +09:00 32c463b7
│  (no description set)
◉  uqpslxtp iyahaya@nifty.com 2024-02-18 13:32:55.000 +09:00 master HEAD@git d416cca1
╷  .gitignore: add *.cmd and *.git
╷ ◉  pnltvyyq iyahaya@nifty.com 2021-07-03 11:45:40.000 +09:00 retrynetdrive@origin c7379b94
╭─╯  When changing drive to network drive failed, net use and retry
◉  nmsyzyuu iyahaya@nifty.com 2021-07-03 00:36:18.000 +09:00 4.4.10_1 7798575d
│  bump to 4.4.10_1
~

よしよし

既存パッケージ(uncozip) を Go1.22 の rangefunc 対応に

意外と簡単でした(Lua に慣れてる人は、コールバック関数を戻り値として返すというやり方はおなじみかも)

uncozip のパッケージは、ZIPファイルの各ファイルをリストアップする関数として (*CorruptedZip) Scan() bool というメソッドがあります。この実装は、内部の繰り返し関数 (*CorruptedZip) scan() error を次のようにラッピングしているだけの関数です。

// Scan advances the CorruptedZip to the next single file in a ZIP archive.
func (cz *CorruptedZip) Scan() bool {
    err := cz.scan()
    if err == io.EOF {
        cz.err = nil // means no error
    } else {
        cz.err = err
    }
    return err == nil
}

これをちょっと組み変えた関数 Each を用意します。(*CorruptedZip) Scan() は繰り返し要素のデータは元々自分自身のメソッドで提供していたので、Each は自分自身をコールバック関数に渡すという手抜きをしています。

( これ、将来的には、Scan は廃止して、各要素は本体のフィールドから外部に追い出すように最適化したいものですが、とりあえずは簡単対応で )

// Each is the function for rangefunc.
func (cz *CorruptedZip) Each(f func(*CorruptedZip) bool) {
    for {
        err := cz.scan()
        if err != nil {
            if err != io.EOF {
                cz.err = err
            }
            break
        }
        if !f(cz) {
            break
        }
    }
}

呼び出し側を少々書き変えます。

diff --git a/cmd/uncozip/main.go b/cmd/uncozip/main.go
index 40d1c6bd23...4f777afa23 100644
--- a/cmd/uncozip/main.go
+++ b/cmd/uncozip/main.go
@@ -165,13 +165,13 @@
        })
    }
 
-   for cz.Scan() {
+   for entry := range cz.Each {
        var err error
        var checksum uint32
        if *flagTest {
-           checksum, err = testEntry(cz, patterns)
+           checksum, err = testEntry(entry, patterns)
        } else {
-           checksum, err = extractEntry(cz, patterns)
+           checksum, err = extractEntry(entry, patterns)
        }
        if err == errSkipEntry {
            continue
@@ -179,17 +179,17 @@
        if err != nil {
            return err
        }
-       if checksum != cz.CRC32() {
+       if checksum != entry.CRC32() {
            if *flagStrict {
                return fmt.Errorf("%s: CRC32 is expected %X in header, but %X",
-                   cz.Name(), cz.CRC32(), checksum)
+                   entry.Name(), entry.CRC32(), checksum)
            }
            fmt.Fprintf(os.Stderr,
                "NG:   %s: CRC32 is expected %X in header, but %X\n",
-               cz.Name(), cz.CRC32(), checksum)
+               entry.Name(), entry.CRC32(), checksum)
        } else if *flagDebug {
            fmt.Fprintf(os.Stderr, "%s: CRC32: header=%X , body=%X\n",
-               cz.Name(), cz.CRC32(), checksum)
+               entry.Name(), entry.CRC32(), checksum)
        }
    }
    if err := cz.Err(); err != io.EOF {

最後に Makefile を修正します。

--- a/Makefile
+++ b/Makefile
@@ -14,7 +14,7 @@
 all:
        go fmt
        $(SET) "CGO_ENABLED=0" && go build $(GOOPT)
-       cd "cmd/uncozip" && go fmt && $(SET) "CGO_ENABLED=0" && go build -o ../../$(NAME)$(EXE)
$(GOOPT)
+       cd "cmd/uncozip" && go fmt && $(SET) "CGO_ENABLED=0" && $(SET) "GOEXPERIMENT=rangefunc"
& go build -o ../../$(NAME)$(EXE) $(GOOPT)

 _dist:
        $(MAKE) all

ね、簡単でしょ? (そりゃ、手抜きだし…)

この対応では、実行ファイル作成部分では GOEXPERIMENT=rangefunc を指定していますが、パッケージ部分では rangefunc が要求する型の関数を用意しているだけで、言語の拡張部分は使っていません。それゆえ、Go 1.22以前のコードからでも従来どおり普通に import して使えます。

GNU Cash で家計簿をつけてるつもりが、ただのライフログになってた

よく間違えて「GNU Cache」と typo しがちな「GNU Cash」で家計簿つけてるけど、いまいちうまくいかない。

手入力するためのフィールドがもっと欲しいなー

  • レシートについている現時点での電子マネーの残額(検算用)
  • インボイスの登録番号(働いていない今はいいけど、将来使うかもしれない)
  • 時刻(自分がいつどこで行動したか、あとから追いやすい)1

でも、レシートの中の細かい明細まで入力していない。ぜんぶひっくるめて「食費」あつかいとか、そんな感じ。そして、月単位での集計もしていない。

結果として、ただのライフログになってしまっている。

Amazon の明細と、手入力のメモがうまくマッチしない

そして、試験的に先月からアマゾンで電子書籍を買った時に、日時と価格と書名を毎回記録していたんだけど、請求書と全然マッチしない。

どうも、請求書では、価格がポイントを引いた額になっているのと、日付が時差の関係か知らんけど1日前になっているためっぽい。

うーん。うまいこといかんなぁ…

なんか用語難しいよね!

複式簿記の「借方」と「貸方」という単語。どうも、自分の直感を逆に感じられてシックリこないんだよな…

英語の debit(借方),credit(貸方) の方がまだ分かりやすいなぁ。デビットカード・クレジットカードの動きを連想できるから

GNU Cash むずかしい

使い方のページとか見てるけど、ガチすぎて「いや、そこまでしたくないんですけど」という感じだ。自前で Excel か、ツールでも手作りして、入力・集計した方がいいのかなぁ


  1. 殺人事件が起きた時、アリバイを説明しやすい (証明できるとは言っていない)

その後 Jujutsu で分かったこと、失敗したことなど

2月3日の公開後も、ちまちま更新を入れてます。以下の体験とかも反映しています。

( でも、ページ構成に合わせたうまい挿入場所がなくて… 以下に関してはまとまったエピソードとして書いている本ブログの方が分かりやすいかも。Book の方は書く場所が分散してしまっているので )

A Git co-located repository (← よい日本語訳がない)

当初は、jj でタグが打てないのがネックになって移行できなかったんですが、7章に書いたとおり

  • 既存のローカルの Git レポジトリで、jj git init --git-repo=. で初期化

を行えば、Git と jj が共存状態のレポジトリ( a Git co-located repository )となります。これで、ネイティブgitの方のgit tag とか git describe が使えるようです。

そうすると何か問題やら制限があるのではないかと考えるべきですが

  • 普通はブランチを指しているはずの git の HEAD が、コミットを直接指すようになる
    = カレントブランチがなくなるため、一部の git コマンドは発行できなくなる

といったところが確認できています。たとえば、GitHub よりコミットが進んだ状態で、git push を実行しようとすると

$ git push
fatal: You are not currently on a branch.
To push the history leading to the current (detached HEAD)
state now, use

    git push origin HEAD:<name-of-remote-branch>

というエラーになります。ただし、これはjj branch track master@origin を行えば、以後 jj git push が出来るので実害はありません。jj でコミットを進めると自動で git 側の HEAD も移動してもらえるようで、jj log をとると

$ jj log
@  uqpslxtp iyahaya@nifty.com 2024-02-18 12:43:20.000 +09:00 469c6103
│  (no description set)
◉  ysyyypoo iyahaya@nifty.com 2024-02-18 12:43:20.000 +09:00 HEAD@git 53b3435c
│  docs/release_note*.md: wrote about removing ezoe.lua
◉  ksuxxukq iyahaya@nifty.com 2024-02-18 12:40:37.000 +09:00 b9f43fa4
│  Remove nyagos.d\catalog\ezoe.lua (joke script)
◉  wsxquoxy iyahaya@nifty.com 2024-02-14 20:31:23.000 +09:00 master master@origin 69b80e1
2

HEAD@git というマークで確認できます。

ネイティブな git が併用できるようになった結果、自分のタグ運用に問題がなくなりました。jj が普段使いできるようになったので、しばらく使い続けて、さらなる問題点などないか検証をつづけたいと思います。

バイナリの実行ファイルをコミットに入れてしまった

ちなみに、今まで出た一番まずい失敗は、Linux のバイナリの実行ファイルがうっかりコミットに入ったのに気付かずに jj branch set -r @- masterjj git push してしまったことです。

jj では jj git push したコミットは immutable なフラグがついて、編集不可能になってしまいます。つまり、ローカルレポジトリからも問題のコミットを消すことができなくなってしまいました。この immutable なフラグをオフにして、git push -f に相当する操作をしたかったのですが、その操作がよく分かりません。結局:

  1. 別のディレクトリで git clone
  2. そちらで git reset @~, git push -f し、問題のコミットを消す
  3. 元のローカルレポジトリで、jj git fetch し、GitHub の変更を取り込む
  4. immutable ではないコミットを jj edit, jj restore を繰り返して消す

とネイティブGit側の力技で逃げてしまいました。本当はどうすべきだったんでしょう。

こうなった一番の原因は Linux のバイナリをビルドした時に 3MB くらいになったのですが、この時 jj の「コミットに取り込めない」というエラーを

$ jj config set --repo snapshot.max-new-file-size 5MiB

で回避してしまったことです。ここはレポジトリに置けるファイルの上限を上げるのではなく、.gitignore に実行ファイル名を登録して、とりこむファイルの対象外にすべきでした。

これ、Windows だったら、共通 .gitignore に *.exe と設定しており、個別に実行する必要もなかったんですが、Linux だと実行ファイルは拡張子なしですから汎用的にマッチするパターンがないんですよね。個別に名前指定するのを厭ったのが失敗でした。

考えてみると、snapshot.max-new-file-size のデフォルト値が 1MB と低くおさえられている理由は、こういうバイナリファイルの誤コミットを防ぐためだったのかもしれません。さすがに、今は 1MiB に戻しています。

Jujutsu レポジトリで GitHub CLI (ghコマンド) を使う

(2024-02-19:追記あり)

Using GitHub CLI によると

$ GIT_DIR=.jj/repo/store/git gh issue list

のように、環境変数 GIT_DIR に .jj ディレクトリ内の git ディレクトリを指定すればよいようです。

nyagos で、gh コマンドを使う時、この指定を自動的に行うようラッパーを定義しておきましょう。~/.nyagos を以下のエイリアス定義を追加すれば Ok です。

nyagos.alias.gh = function(args)
    local wd = nyagos.getwd()
    local newgitdir = nyagos.pathjoin(wd,".jj\\repo\\store\\git")
    args[0] = "gh.exe" -- replace 'gh' to 'gh.exe'
    if nyagos.access(newgitdir,0) then
        local oldgitdir = nyagos.env.GIT_DIR
        nyagos.env.GIT_DIR = newgitdir
        nyagos.rawexec(args)
        nyagos.env.GIT_DIR = oldgitdir
    else
        nyagos.rawexec(args)
    end
end

bash 環境の場合は、本家によると direnv コマンドを導入して .envrc に次のように書いておけばよいそうです。

export GIT_DIR=$PWD/.jj/repo/store/git

2024-02-19:追記

にも書いたのですが、jj git init --git-repo=. で既存の Git レポジトリを jj 化して、両方使える状態にすると、gh コマンドのエイリアスは必要ではなくなります。

とはいえ、nyagos でコマンドのエイリアスの書き方のサンプルとしては良いので、本記事はそのままにしておきます。