跳至主要内容

版本控制

版本控制分頁(側邊欄圖示列上的分支圖示)就是伺服器啟動時所指定的那個專案目錄的 git。它會顯示上一次提交之後改了哪些東西,讓你暫存並提交、建立與切換分支、新增遠端並推送、處理合併衝突、瀏覽歷史,也可以打開任何檔案的變更來看差異。圖表在磁碟上就是兩個純 JSON 檔,所以這裡做的都是對一般檔案執行的一般 git:這個分頁能做的事,你在同一個目錄用命令列一樣做得到;你在命令列做的事,檔案清單、分支列,以及每一個你展開著的清單,最多十五秒內也會跟上。

分頁能運作有兩個前提。伺服器必須是以專案目錄啟動的:先 cdui project init my-project,再 cdui start --project my-project。沒有專案目錄的伺服器會顯示「版本控制需要專案目錄。」並印出這兩行指令,因為專案是伺服器的啟動參數,瀏覽器裡的任何按鈕都補不上。專案是什麼請見專案目錄。另外,執行伺服器的那台電腦必須裝有 git 2.23 以上(不是開瀏覽器的那台):沒裝時分頁會說「伺服器電腦尚未安裝 git。」,版本太舊則是 git {version} 版本過舊,需要 2.23 以上。 專案目錄還不是儲存庫時,分頁上只有一個按鈕:初始化儲存庫

伺服器是以它自己的作業系統使用者身分、用子行程執行 git,而且從不進入互動模式。所有詢問都被關掉了(GIT_TERMINAL_PROMPT=0、清空的 GIT_ASKPASS,以及在你沒有自訂 GIT_SSH_COMMAND 時預設的 ssh -oBatchMode=yes),所以 git 一旦要求輸入密碼,就會在一秒內失敗,而不是一直卡到你關掉分頁;分頁會把它報成「伺服器電腦沒有此遠端的登入憑證。」CodefyUI 自己不保存任何 token、密碼或金鑰。每一次擷取與推送用的,都是那個使用者本來就有的憑證管理員與 SSH 金鑰,所以登入憑證只需要在那台機器的終端機裡設定一次,永遠不在應用程式裡設定。

設定 git 與 GitHub

每台要跑伺服器的電腦做一次就好。

安裝 git。 Windows 用 winget install Git.Git,Git for Windows 會一併安裝 Git Credential Manager,它就是負責記住 HTTPS 登入的那一塊。macOS 用 xcode-select --install 裝 Apple 的 git,或用 brew install git 裝新版。Ubuntu 與 Debian 用 sudo apt install git。伺服器大約每半分鐘會重新確認一次 git 在不在,所以新裝好的 git 通常會自己被認出來;畫面上那句「安裝後請重新啟動伺服器。」講得比實際需要的保守。

告訴 git 你是誰。 提交會帶上名稱與電子郵件,缺一個 git 就拒絕提交:

git config --global user.name "Your Name"
git config --global user.email "you@example.com"

分頁裡也做得到:更多動作 > 提交身分... 會開一個小表單,顯示 git 目前回答的內容,以及它是「來自全域 git 設定」還是「此專案專用」。從這個表單儲存會寫進這個儲存庫自己的設定,所以它適合用來為單一專案指定另一個地址,而不適合用來設定整台機器的預設值;後者請用上面那兩行指令。

讓伺服器有辦法通過認證。 第一次取得憑證一定要在伺服器電腦的終端機裡完成,因為伺服器自己永遠不會發問。以下三條路擇一即可:

  • 用 GitHub CLI 走 HTTPS。 Windows 用 winget install GitHub.cli,macOS 用 brew install gh;Ubuntu 的預設套件庫不一定有 gh,請照 GitHub 在 cli.github.com 上的 apt 說明安裝。接著執行 gh auth login;當它問要不要用你的 GitHub 帳號設定 git 認證時回答要。它會把憑證輔助程式寫進你的全域 git 設定,之後每次推送都不會再問。
  • 走 HTTPS,但自己先推一次。 在專案目錄的終端機裡 clone 或推送一次。憑證管理員會記住你輸入的內容,之後伺服器的推送就找得到它。
  • 改用 SSH。 執行 ssh-keygen -t ed25519 -C "you@example.com",把公鑰加到 GitHub 的 Settings > SSH and GPG keys,然後在終端機執行一次 ssh -T git@github.com。最後這步不能省:第一次連線要先確認主機金鑰,而伺服器是用批次模式跑 ssh 的,問不出口,所以由伺服器發起的第一次連線只會直接失敗。新增遠端時請用 git@github.com:owner/repo.git 這種寫法。

接上儲存庫。 在 GitHub 上建一個空的儲存庫,不要 README、不要 .gitignore、不要授權檔,讓它的歷史是空的,你第一次推送才不會變成合併。接著在分頁裡展開遠端,按標題上的 +新增遠端...):名稱除非另有理由,否則就用 origin,URL 用 GitHub 顯示的那一個(https://github.com/owner/repo.git,或 SSH 的寫法)。標題下方那一列接著會出現 發佈分支,它會推送目前的分支並讓它追蹤遠端的分支。從此之後,那個位置會改成 同步(先拉取再推送) 按鈕。

日常流程

儲存圖表(Ctrl/Cmd + S)。它寫出的兩個檔案 graphs/<name>.graph.jsonlayout/<name>.layout.json 會立刻出現在 變更 底下,不必等下一次輪詢。用列上的 暫存 或群組標題上的 全部暫存 把要進這次提交的檔案暫存起來,寫下訊息,再按 提交Ctrl/Cmd + Enter。不想先暫存的話,按鈕旁邊的箭頭底下有 全部提交(暫存所有變更,含新檔案),以及 修改上一個提交;這一列一直都在,不能用的時候是停用而不是消失:分支還沒有任何提交時它是灰的,上一個提交已經推送時它會改成顯示「無法修改:上一個提交已推送」。

提交 按鈕不會只是變灰,它會說出原因:「請輸入訊息」或「沒有已暫存的變更」,游標停留時看得到,螢幕閱讀器也讀得到。

標題下方的分支列說明分支的狀態。↑2 ↓1 表示有兩個提交待推送、一個待拉取;同一件事給螢幕閱讀器與提示文字的完整說法是「2 個待推送,1 個待拉取」,而與上游一致的分支則什麼都不畫。「尚未發佈」表示這個分支還沒有上游,「上游已刪除」表示它曾經有、但遠端已經沒有了,「尚無提交」是空的儲存庫,「分離的 HEAD」則是不在任何分支上的檢出狀態。

同步(先拉取再推送) 是日常用的按鈕。想只做其中一半時,擷取拉取推送 都在 更多動作 選單裡;這裡的拉取只做快轉,所以分歧會被回報出來,而不是默默幫你合併。按不下去的項目不會只是變灰,而會在原地說明原因:「尚無遠端。」、「尚未發佈」、「尚無提交」、「分離的 HEAD」,或是正在執行中的那個操作。網路操作有自己的執行通道,所以一個很慢的擷取不會擋住你正要做的提交,而且兩者都不會被重複啟動。

標題列的 重新整理 會重讀畫面上的所有東西:狀態、每一個展開中的區塊,以及只要還留著頁面的歷史——收合「歷史」不會把你載入過的頁面丟掉,所以它們一樣會被更新。底下什麼都沒有的收合區塊則不會被讀取。

分支、遠端與儲藏

檔案群組底下有四個可收合的清單:這一節說明前三個,第四個「歷史」另有一節。每一個都會記住上次的展開狀態、在展開時才讀取,收合著就完全不花成本。

分支 會列出本機分支,以及每一個離它追蹤的目標有多遠;分支名稱本身就是切換過去的按鈕,你目前所在的那一列則顯示「目前」。標題上的 新增分支... 會建立分支並切換過去。每一列的選單裡有 重新命名刪除;目前所在的分支沒有刪除,因為 git 本來就拒絕。刪除一個提交不在任何其他分支上的分支時,會用 git 自己的說法再問一次:{name} 有尚未合併的提交,仍要刪除? 本機分支底下的 遠端分支 列出遠端有哪些分支,按一下就會建立一個追蹤它的本機分支。

遠端 會列出每個遠端,以及 git 擷取時用的 URL,另有 變更 URL移除新增遠端... 就是標題上的那顆 +。只接受 https://ssh://file:// 以及 git@host:owner/repo.git 這種寫法,而且伺服器只允許 git 使用這三種協定,那些「URL 其實是一行要執行的指令」的傳輸方式根本設定不上去。

儲藏 是儲藏堆疊,最新的在最上面,每一列顯示你給的說明、它是在哪個分支上做的,以及多久以前。更多動作 選單裡的 儲藏變更... 會先問說明,並且會把未追蹤的檔案一起收走,這樣的儲藏才足以把工作區清出來給檢出用;說明留空的話,git 會自己寫一句主旨。每一列提供 取出套用刪除,只有刪除會先問,因為只有它會把工作丟掉。

衝突

合併因衝突停下來時,檔案群組上方會出現 合併變更 群組,並帶著「合併進行中:處理每個檔案後再提交。」這行說明。每一列提供 git 的三種答案:保留我的 用你這邊覆寫檔案,採用遠端的 用對方那邊覆寫,標記為已解決 則是把磁碟上現在的內容原樣暫存起來,也就是你自己把衝突標記處理掉之後(在編輯器裡或任何其他工具裡)要按的那一個。這些列上沒有捨棄:git 拒絕捨棄處於衝突狀態的路徑,所以那顆按鈕能做的只有顯示錯誤。

群組標題上的 中止合併 會把工作區還原成合併之前的樣子,按下去會先問「中止合併並捨棄目前已處理的內容?」另一條出路是提交,而且即使把每個檔案都處理成「我的」而導致沒有任何東西被暫存,提交 按鈕仍然是可以按的:那樣確實沒有改到任何檔案,但那正是 git 仍然要求你提交的那種合併。

推送被「本機與遠端分支已分歧。」擋下來時,錯誤訊息旁邊除了關閉,還會有一顆 合併遠端變更:它會以合併的方式拉取,也就是快轉式拉取拒絕做的那一步。凡是帶著 git 原始輸出的拒絕,都有一個 詳細資料 可以展開,裡面是原始的訊息尾巴,且已經把看起來像憑證的內容遮蔽掉。

歷史與差異檢視

歷史 是第四個區塊,位在儲藏底下,預設是收合的。展開時會讀進最新的三十個提交,每個提交一列,顯示短提交 ID、主旨、多久以前,以及作者。展開其中一列會列出那個提交改動的檔案,列選單裡的 複製提交 ID 會把完整的雜湊值放到剪貼簿。伺服器說還有下一頁時,底下會出現 載入更多

歷史刻意不加入十五秒的輪詢,因為輪詢會把你已經載入的那幾頁丟掉。會重新讀取它的是 重新整理、一次提交或修改提交,以及任何會移動分支的動作:拉取、推送、同步、切換分支、建立或刪除分支。

分頁裡任何一列檔案都可以打開該檔案的變更:變更 底下的列顯示尚未暫存的那一面,已暫存的變更 底下的列顯示下一次提交會包含的內容,而歷史裡某個提交底下的檔案列,顯示的是那個提交對它做了什麼。檢視會蓋在編輯器上開啟,標題是路徑,底下是範圍(「未暫存的變更」、「已暫存的變更」或 提交 {sha}),用 關閉 或 Escape 收起來。

單欄並排 是讀這份 patch 的兩種方式。並排是從同一份 patch 推導出來的,不是從整個檔案,所以它顯示的是有變更的段落與其上下文,左右對齊排好;處於衝突狀態的檔案只提供單欄,因為滿是衝突標記的檔案沒有「兩面」可以配對——git 對未合併的路徑會一次給出所有的面,視窗畫的就是它,連衝突標記一起。關於整份 patch,有三件事是用一句話說明而不是用差異行呈現:二進位檔案會顯示「二進位檔案,無文字差異。」;被伺服器在 1 MiB 處截斷的 patch,會在已顯示的內容上方帶著 差異已在 {kb} KB 處截斷。;文字上沒有差異的檔案則顯示「沒有變更」。patch 裡面還有兩句:git 自己的「檔案結尾沒有換行」會標在它所指的那一行旁邊,否則那種變更就是兩行一模一樣的文字;超過兩千行之後,最後一行畫出來的內容底下會有一句說明,因為畫更多會讓分頁凍住一秒,而側邊面板裡也沒有人會讀那麼多。

圖表到底改了什麼

一份已存圖表的 JSON 差異是一整片括號,「我把 k 從 5 改成 7」淹沒在裡面看不出來。所以對 graphs/<name>.graph.jsonlayout/<name>.layout.json,patch 上方會有一段簡短的摘要,由差異的兩面讀出來:

  • 新增與移除的節點,算的是畫布上的節點:拖進一個預設組合區塊算一個節點,而不是它一併帶進檔案的那份定義裡的六個;
  • 類型改變的節點;
  • 值有變動的參數,掛在節點的標籤底下(沒有標籤時用它的 id),過長的值會被截短;
  • 新增與移除的連線,比較的是端點與 handle 而不是 id,因為複製貼上會重新產生 id;
  • 移動過的節點位置,這一項只會出現在 .layout.json 的差異裡,因為 .graph.json 裡根本沒有座標。

最多顯示八行,其餘的以「還有 3 項」帶過。「邏輯無變更」表示兩面說的是同一件事,差別只在文字:鍵的順序、空白、陣列換了寫法、重新產生的 id。「無法解析為圖表」表示其中一面不是可解析的該類 JSON。

有些真正的變更目前還沒有對應的行,這時摘要是空的,而不是說沒事:段落群組、註記的幾何、子圖定義、預設組合定義的出現或消失(畫布上的那個實例節點仍然會被算進去)、圖表的名稱或描述,以及預設組合實例的個別覆寫值。這些都不會被說成「邏輯無變更」,下方的 patch 才是顯示它們的地方。至於非專案模式存成單一 <name>.json 的圖表,完全不會有摘要。

圖表開著時,磁碟上的檔案被改動了

拉取、切換分支、取出儲藏、捨棄、中止合併或處理衝突,都可能讓你開著的圖表在磁碟上變成另一份內容。分頁不會在你背後重新載入它,而是彈出一個常駐的提示,說明這次寫入影響到幾個開啟中的圖表,並附上 重新載入 按鈕;按下去會為全部這些分頁一次確認(「這些分頁未儲存的編輯會遺失。」),然後逐一從磁碟重讀。如果某個分頁的檔案在你切換過去的分支上並不存在,該分頁會保留目前顯示的內容(那已經是它唯一的一份),並說明這件事。

安全性

每一個寫入都會帶上編輯器的工作階段 token,和應用程式裡其他會改變狀態的呼叫一樣:初始化、暫存、取消暫存、捨棄、提交、提交身分、分支、遠端、儲藏、合併、解決衝突,以及所有和遠端往來的操作。讀取(狀態、歷史、某個提交的檔案清單、差異、某個 ref 上的檔案、設定與三個清單)則和 API 其他讀取一樣,是不帶認證的 GET。

和套件安裝不同,這些路由沒有只允許本機連線的限制。刻意把服務開放到區域網路的機器,其存取控制是部署方 IT 的責任:請把它放在會做認證的東西後面,做法見放在反向代理後面。畢竟這裡跑的是伺服器使用者自己的 git,對象是他自己打開的目錄,用的也是他本來就有的憑證。

有兩條規則在 API 底層強制執行,關不掉。任何名為 .env.env.<任何內容> 的檔案,在任何 ref 上都會被拒絕,而且是在啟動 git 之前就拒絕:差異與檔案讀取都一樣,工作區、索引與任何提交都一樣,所以不小心提交進去的 dotenv 沒辦法透過編輯器讀回來。.env.example 是例外,因為它本來就是給人看的。另外,git 的錯誤輸出在送出前會先被遮蔽一次,因為 https://user:token@github.com/... 這種 URL 真的會有人貼進遠端設定裡。

也請把機密留在圖表之外:打進 LLM 節點參數欄的 API 金鑰,會原樣存進圖表的 JSON。改用環境變數的作法見版本控管你的 graphs

限制

  • 沒有合併編輯器。 衝突只能整面採用其中一邊,或是自己把檔案改好之後按 標記為已解決
  • 沒有強制推送、沒有 rebase、沒有 cherry-pick、沒有標籤、沒有 clone。 那些是終端機的工作;這個分頁做的是 git 的日常部分,不是全部。
  • 一台伺服器一個專案。 專案目錄在伺服器啟動時就決定了,編輯器裡沒有辦法換到另一個。
  • 不能只暫存部分區塊。 暫存的單位是檔案。
  • 不支援提交簽章。 設定了 commit.gpgsign = true 的儲存庫會以「應用程式不支援提交簽章。」失敗,因為在沒有終端機的行程裡,gpg 沒辦法要求輸入密語。請為這個儲存庫關掉簽章,或改在終端機做那次提交。
  • 差異就是 patch。 並排檢視配對的是 patch 裡的段落;把兩個版本的整份檔案拿來比對是後續的工作。
  • 歷史以位移分頁。 一次三十個提交,所以在兩頁之間新增的提交會讓視窗位移;重新整理 會從第一頁開始,把你已經載入的那幾頁重讀一次。

疑難排解

分頁顯示的訊息意思該怎麼做
「此分支尚未發佈。」分支還沒有上游,所以沒有東西可以拉、也沒有地方可以推。發佈分支。狀態允許時,這個拒絕訊息旁邊就會帶著那顆按鈕。
「本機與遠端分支已分歧。」兩邊都有對方沒有的提交,無法快轉。按訊息旁的 合併遠端變更,處理掉衝突,再推送。
「伺服器電腦沒有此遠端的登入憑證。」git 要求輸入密碼,而伺服器沒有辦法回答。在伺服器電腦的終端機裡:HTTPS 用 gh auth login,或設定 SSH 金鑰並執行一次 ssh -T git@github.com,然後重試。
「git 的推送設定拒絕了這次推送(push.default 或上游分支名稱)。」是 git 自己的 push.default 規則擋的,不是遠端擋的。展開 詳細資料 看 git 的原句;通常 git config --global push.default current 或讓上游分支同名就能解決。
git {version} 版本過舊,需要 2.23 以上。伺服器電腦上的 git 比這個分頁讀取的狀態格式還舊。在那台電腦上升級 git。
「不是 git 儲存庫。」專案目錄裡沒有 .git在分頁的空狀態畫面上按 初始化儲存庫,或改用你真正要的目錄啟動伺服器。
「此檔案被 git 忽略。」你打開的是 git 忽略的路徑,或是伺服器永不提供的 .env 類檔案的變更。不用處理;這個檔案就是刻意不讓編輯器讀的。
「另一個 git 動作仍在執行。」兩個寫入同時發生,或在一個長操作進行中又寫入。等標題底下的進度條跑完。
git 在 {seconds} 秒內未完成。指令超過了它的期限:狀態 10 秒、讀取 20 秒、本機寫入 30 秒、走網路的 130 秒(git 自己的 120 秒加上請求的寬限)。通常是索引鎖被另一個行程佔著。關掉那個目錄裡其他正在跑 git 的東西,再按 重新整理

至於更大的圖像(什麼該進儲存庫、什麼絕對不能提交,以及怎麼在 CI 裡驗證每一張圖),請見版本控管你的 graphs專案目錄