功能價格文件部落格
取得 mote
← 部落格
工程·2026年9月7日·閱讀時間 4 分鐘

為了避免 git checkout 後文件變空,我們不看時間,而是比較位元組

切換分支後,曾遇過已開啟的文件變成空白畫面嗎?我們在拍攝要放上部落格的螢幕截圖時,在自己的應用程式裡看到了這個情況,也趁著修正它,一併抓出了一個更悄無聲息的錯誤。

m
mote
編輯中的文件在外部被變更時顯示的橫幅。使用者可以選擇重新載入或繼續編輯,而狀態列顯示還有尚未儲存的 13 B。
編輯中的文件在外部被變更時顯示的橫幅。使用者可以選擇重新載入或繼續編輯,而狀態列顯示還有尚未儲存的 13 B。

mote 完全信任檔案。畫面上的內容與磁碟中的位元組相同,儲存時也會原封不動地寫入那些位元組。

但如果檔案在應用程式外部被變更了呢?如果 mote 沒有察覺,下一次儲存時就可能覆寫其他人寫入的內容。

這個問題是在拍攝螢幕截圖時發現的。我們來回切換分支,準備「變動中的位元組」畫面時,文件偶爾會停在空白畫面。

藏在空白畫面背後的兩個問題

git checkout 不會一次完成檔案的變更。它會刪除原有檔案、建立新檔案,然後寫入內容。

檔案監視器會以多個事件通知這個過程。但 mote 在第一個事件一到時,就立刻讀取檔案。因為內容還沒填入完成,有時會讀到空白檔案。

重新讀取檔案時,也重新建立了監視器。在這段期間,舊監視器中尚未處理的事件消失了,畫面便一直停留在空白狀態。

還有一個更危險的問題。

mote 會把儲存後 1.5 秒內收到的事件視為「我造成的變更」並忽略。但在這段時間內,格式化工具或儲存掛鉤也可能再次修改檔案。

實際上,如果在儲存後 0.3 秒修改檔案,mote 完全不會顯示任何變化。使用者再次儲存時,外部變更的內容就會悄悄消失。

一個看得見,另一個看不見。原因卻相同。

我們是依據時間,而不是內容來判斷檔案的變化。

我們改成比較內容,而不是時間

現在收到檔案事件時,不會立刻讀取。我們會等待 200ms,而且每當收到新事件,就重新開始計算等待時間。

也就是等刪除、建立及寫入檔案的過程結束後,只讀取一次最後的狀態。舊計時器會透過世代編號失效。我們也沒有建立在無事發生時仍會持續運作的計時器。

讀取到的內容會與緩衝區最後一次看到的磁碟位元組比較。

如果相同,表示是應用程式儲存的內容,或相同內容被再次寫入。如果不同,才是真正的外部變更。儲存後經過的是 0.1 秒還是 1 秒並不重要。

再次讀取同一個檔案時,也會保留監視器。這樣就不會在更換監視器的期間遺失事件。

只在編輯中詢問

發現外部變更後,會先查看使用者的狀態。

如果有尚未儲存的編輯,就會顯示橫幅。使用者可以選擇重新載入已變更的檔案,或繼續編輯目前的內容。無論選擇哪一邊,都有可能失去內容,因此應用程式不會代替使用者決定。

如果不在編輯中,就會立刻載入新內容。不顯示橫幅,並維持游標和捲動位置不變。因為在來回切換分支閱讀文件時,比起顯示通知,保留原本閱讀的位置更加自然。

未在編輯中時於外部被變更的檔案。內容在沒有橫幅的情況下更新了,標題上的編輯標記和狀態列中等待寫入的位元組也消失了。
未在編輯中時於外部被變更的檔案。內容在沒有橫幅的情況下更新了,標題上的編輯標記和狀態列中等待寫入的位元組也消失了。

檔案消失時,也不會立刻做出判斷。因為其他編輯器可能正在重新命名檔案,所以會在 300ms 後再次確認。

如果那時檔案仍不存在,就會通知使用者。正在撰寫的內容會保留在緩衝區中,再次儲存時會建立檔案。

在實際環境中確認六種情況

這個問題很難只靠單元測試確認。因為它是在檔案監視器、實際磁碟及視窗管理員一起運作時發生的。

因此,我們建立了在實際環境中執行的檢查。

我們會確認即使切換分支十次,緩衝區是否仍會跟上檔案;即使在儲存 300ms 後修改檔案,變更是否仍會反映。我們也會檢查應用程式自行儲存時是否不會顯示通知,以及編輯期間發生外部變更時,是否會保護已撰寫的內容並顯示橫幅。

總共有六種。每次發布時都會執行。

在實體顯示器上執行 tools/e2e/external.sh 的輸出。六行全部都是 ok,最後一行是 PASS=6 FAIL=0。
在實體顯示器上執行 tools/e2e/external.sh 的輸出。六行全部都是 ok,最後一行是 PASS=6 FAIL=0

把檔案視為真相,並不只是在儲存時原封不動地寫入內容。

當檔案在我們不知情的情況下被變更時,也不能錯過那項變化。

← 較舊即使儲存途中關閉,也能避免檔案損毀的方法
透過 RSS 持續關注。
只留下文字。
繁體中文
© 2026 mote