跳至主要內容

版本控制

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

分頁能運作有兩個前提。伺服器必須是以專案目錄啟動的:先 cdui project init my-project,再 cdui start --project my-project。沒有專案目錄的伺服器會顯示「版本控制需要專案目錄。」並印出這兩行指令,因為專案是伺服器的啟動參數,瀏覽器裡的任何按鈕都補不上。專案是什麼請見專案目錄。另外,執行伺服器的那台電腦必須裝有 git 2.23 以上(不是開瀏覽器的那台):沒裝時分頁會說「伺服器電腦尚未安裝 git。」,版本太舊則是 git {version} 版本過舊,需要 2.23 以上。 專案目錄還不是儲存庫時,分頁上只有一個按鈕:初始化儲存庫。它會執行 git init,並寫入和 cdui project init 相同的 .gitignore(第一行是 .env)與 .gitattributes。已經有 .gitignore 但沒有忽略 .env 時,只會在後面加上那一行。已經有的 .gitattributes 則維持原樣,所以 2.6.0 之前寫出的檔案仍然缺少 *.json text eol=lf,請自行加上這一行(這個檔案的作用見專案目錄)。如果這個目錄位於另一個儲存庫裡,畫面會先說明:它位於另一個儲存庫({path})內;初始化會在此建立獨立的儲存庫。

伺服器是以它自己的作業系統使用者身分、用子行程執行 git,而且從不進入互動模式。所有詢問都被關掉了(GIT_TERMINAL_PROMPT=0、清空的 GIT_ASKPASS,以及 ssh -oBatchMode=yes;最後這項只在你沒有自行指定 ssh 執行方式時套用,也就是沒有設定 GIT_SSH_COMMAND 或 GIT_SSH,全域設定或這個儲存庫的設定裡也沒有 core.sshCommand),所以 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。伺服器大約每 30 秒會重新找一次 git,但只在它啟動時的 PATH 裡找。安裝程式新增了 PATH 項目時(Git for Windows 就會),請照畫面上說的(「安裝後請重新啟動伺服器。」)重新啟動伺服器:先 cdui stop,再從新開的終端機視窗執行 cdui start --project <dir>,新視窗才有新的 PATH。升級 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.json 與 layout/<name>.layout.json 會立刻出現在 變更 底下,不必等下一次輪詢。用列上的 暫存 或群組標題上的 全部暫存 把要進這次提交的檔案暫存起來,寫下訊息,再按 提交 或 Ctrl/Cmd + Enter。不想先暫存的話,按鈕旁邊箭頭底下的 全部提交(含新檔案) 會把所有變更(包括新檔案)暫存並一次提交。它旁邊的 修改上一個提交 是一個開關:勾選後,提交 旁會出現 修改中 標籤,下一次提交或全部提交會取代上一個提交,而不是新增一個;沒有暫存任何東西時,只會改寫提交訊息。那次提交完成後,它會自動關閉。分支還沒有任何提交時它是灰的;上一個提交已經在上游時,它會顯示「無法修改:上一個提交已推送」。

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

新檔案與修改過的檔案都列在 變更 裡。已暫存的變更 底下列上的 取消暫存,或該群組標題上的 全部取消暫存,會把檔案移出下一次提交。變更 底下列上的 捨棄變更 會先問捨棄 {name} 的變更?,然後從索引還原檔案,所以同時也已暫存的檔案會保留暫存的版本;對新檔案則會問刪除 {name}?此檔案未被 git 追蹤,無法復原。並刪除它。變更 標題上的 捨棄所有變更 會問捨棄 {changed} 個變更並刪除 {untracked} 個未追蹤檔案?此動作無法復原。,然後對所有檔案做這兩件事。被 git 忽略的檔案,例如 .env、虛擬環境或模型權重,永遠不會被動到。全部暫存、全部取消暫存與捨棄所有變更會略過路徑經過連結資料夾(符號連結或 junction)的檔案,並顯示已略過 {count} 個檔案(它們指向專案之外)。捨棄所有變更仍會刪除未追蹤的連結本身,但不會刪除它指向的檔案。全部暫存也會暫存處於衝突狀態的檔案,等於把它們以磁碟上的現狀(連同衝突標記)標記為已解決;合併進行中時,請改用「衝突」一節說明的 合併變更 列逐一處理。

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

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

更多動作 裡的 隱藏版面檔案 會把 layout/*.layout.json 從 變更 清單中拿掉,因為每次儲存都會寫出一個。開啟時,這一項名稱後面會加上(已隱藏 {count} 個),而且這個選擇會記在這個瀏覽器裡。已暫存的變更、全部暫存與捨棄所有變更仍然包含這些檔案。選單最後一項 設定教學 會開啟這一頁的英文版。

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

分支、遠端與儲藏​

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

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

遠端 會列出每個遠端,以及 git 擷取時用的 URL,另有 變更 URL 與 移除;新增遠端... 就是標題上的那顆 +。含有憑證的 URL 會以遮蔽後的形式顯示(https://alice:***@host/r.git),而 變更 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.json 與 layout/<name>.layout.json,patch 上方會有一段簡短的摘要,由差異的兩面讀出來:

  • 新增與移除的節點,算的是畫布上的節點:拖進一個預設組合區塊算一個節點,而不是它一併帶進檔案的那份定義裡的六個;
  • 類型改變的節點;
  • 值有變動的參數,掛在節點的標籤底下;沒有標籤時,id 在 16 個字元以內就用 id,否則用類型加上 id 的前八個字元(被編輯的預設組合裡的節點寫成 <preset>/<node>)。預設組合節點自己的設定寫成 <inner node>.<parameter>,掛在該節點底下。值會截短到 40 個字元,某一面沒有的參數以 - 表示;
  • 新增與移除的連線,比較的是端點與 handle 而不是 id,因為複製貼上會重新產生 id;
  • 移動過的節點位置,這一項只會出現在 .layout.json 的差異裡,因為 .graph.json 裡根本沒有座標。

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

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

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

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

安全性​

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

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

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

LLM 節點上的 API 金鑰欄位是機密參數:打進去的金鑰在儲存時會從圖表中清空,所以不會進到任何提交,但重新載入後也就不見了。改用環境變數的作法見版本控管你的 graphs。

限制​

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

疑難排解​

分頁顯示的訊息意思該怎麼做
「此分支尚未發佈。」分支還沒有上游,所以沒有東西可以拉、也沒有地方可以推。按 發佈分支。狀態允許時,這個拒絕訊息旁邊就會帶著那顆按鈕。
「本機與遠端分支已分歧。」兩邊都有對方沒有的提交,拉取與同步所嘗試的快轉無法進行。按訊息旁的 合併遠端變更,處理掉衝突,再推送。
「遠端有你尚未取得的提交,請先拉取。」推送時發現遠端有這個分支沒有的提交。按 同步,或先 拉取 再 推送。
「伺服器電腦沒有此遠端的登入憑證。」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 branch -vv 會顯示上游分支。
git {version} 版本過舊,需要 2.23 以上。伺服器電腦上的 git 比這個分頁要用的 git restore 與 git switch 還舊。版本顯示成 ? 表示讀不到 git --version 的結果。在那台電腦上升級 git,然後重新啟動伺服器:每個伺服器行程只讀一次版本。
「不是 git 儲存庫。」專案目錄裡沒有 .git。在分頁的空狀態畫面上按 初始化儲存庫,或改用你真正要的目錄啟動伺服器。
「此檔案被 git 忽略。」你打開的是 git 忽略的路徑,或是伺服器永不提供的 .env 類檔案的變更。不用處理;這個檔案就是刻意不讓編輯器讀的。
「另一個 git 動作仍在執行。」兩個寫入同時發生,或在一個長操作進行中又寫入。等標題底下的進度條跑完。
git 在 {seconds} 秒內未完成。指令超過了它的期限:狀態 10 秒、讀取 20 秒、本機寫入 30 秒、走網路的 130 秒(git 自己的 120 秒加上請求的寬限)。通常是索引鎖被另一個行程佔著。關掉那個目錄裡其他正在跑 git 的東西,再按 重新整理。

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