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

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。把檔案視為真相,並不只是在儲存時原封不動地寫入內容。
當檔案在我們不知情的情況下被變更時,也不能錯過那項變化。