Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a2ed40d348 | |||
| 65f7d184bc | |||
| a7773b77d1 | |||
| ca1e90f130 | |||
| e63230af71 | |||
| b3cd896fd5 | |||
| 1858f3e5d0 | |||
| f77bf3bbfc | |||
| e80dfdf8e9 | |||
| 51ba410f15 | |||
| e7c975fdea | |||
| f51f8b743d | |||
| 785d07b08f | |||
| 9d1fd59644 | |||
| 8f970ed5b4 | |||
| 0702d27166 | |||
| 2c103f7f96 | |||
| 52a1054757 |
@@ -0,0 +1,59 @@
|
|||||||
|
name: SCIP Index
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['**']
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
index:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Clone clojure-tui (local dependency)
|
||||||
|
run: git clone https://git.ajet.fyi/ajet/clojure-tui.git ../clojure-tui
|
||||||
|
|
||||||
|
- name: Setup Java
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y openjdk-17-jdk
|
||||||
|
echo "JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Install Clojure CLI
|
||||||
|
run: curl -fsSL https://download.clojure.org/install/linux-install-1.12.0.1501.sh | sudo bash
|
||||||
|
|
||||||
|
- name: Install Babashka
|
||||||
|
run: |
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/babashka/babashka/master/install | sudo bash
|
||||||
|
|
||||||
|
- name: Install clojure-lsp
|
||||||
|
run: |
|
||||||
|
mkdir -p /usr/local/bin
|
||||||
|
curl -fsSL https://github.com/clojure-lsp/clojure-lsp/releases/latest/download/clojure-lsp-native-static-linux-amd64.zip -o /tmp/clojure-lsp.zip
|
||||||
|
unzip -o /tmp/clojure-lsp.zip -d /usr/local/bin/
|
||||||
|
chmod +x /usr/local/bin/clojure-lsp
|
||||||
|
|
||||||
|
- name: Clone and build scip-clojure
|
||||||
|
run: |
|
||||||
|
git clone https://git.ajet.fyi/ajet/scip-clojure.git /tmp/scip-clojure
|
||||||
|
cd /tmp/scip-clojure
|
||||||
|
clojure -T:build compile-java
|
||||||
|
|
||||||
|
- name: Generate SCIP index
|
||||||
|
run: |
|
||||||
|
cd /tmp/scip-clojure
|
||||||
|
clojure -M:run -p $GITHUB_WORKSPACE -o $GITHUB_WORKSPACE/index.scip -m $GITHUB_WORKSPACE/package-map.edn
|
||||||
|
env:
|
||||||
|
CLOJURE_LSP_PATH: /usr/local/bin/clojure-lsp
|
||||||
|
|
||||||
|
- name: Install Sourcegraph CLI
|
||||||
|
run: |
|
||||||
|
curl -fsSL https://sourcegraph.com/.api/src-cli/src_linux_amd64 -o /usr/local/bin/src
|
||||||
|
chmod +x /usr/local/bin/src
|
||||||
|
|
||||||
|
- name: Upload to Sourcegraph
|
||||||
|
run: src code-intel upload -file=index.scip
|
||||||
|
env:
|
||||||
|
SRC_ENDPOINT: ${{ secrets.SRC_ENDPOINT }}
|
||||||
|
SRC_ACCESS_TOKEN: ${{ secrets.SRC_ACCESS_TOKEN }}
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
.nrepl-port
|
.nrepl-port
|
||||||
target/
|
target/
|
||||||
*.jar
|
*.jar
|
||||||
|
.clj-kondo/
|
||||||
|
.lsp/
|
||||||
|
index.scip
|
||||||
|
|
||||||
# VHS test outputs (generated artifacts, not needed in repo)
|
# VHS test outputs (generated artifacts, not needed in repo)
|
||||||
test/e2e/output/
|
test/e2e/output/
|
||||||
|
|||||||
@@ -74,3 +74,5 @@ bb test:e2e # Run VHS tape tests
|
|||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,4 +12,10 @@
|
|||||||
e2e:setup {:doc "Setup standard e2e test repo"
|
e2e:setup {:doc "Setup standard e2e test repo"
|
||||||
:task (e2e/setup-test-repo)}
|
:task (e2e/setup-test-repo)}
|
||||||
e2e:setup-cursor {:doc "Setup cursor navigation test repo"
|
e2e:setup-cursor {:doc "Setup cursor navigation test repo"
|
||||||
:task (e2e/setup-cursor-test-repo)}}}
|
:task (e2e/setup-cursor-test-repo)}
|
||||||
|
e2e:setup-branch-order {:doc "Setup branch order test repo"
|
||||||
|
:task (e2e/setup-branch-order-test-repo)}
|
||||||
|
e2e:cleanup-branch-order {:doc "Cleanup branch order test repo"
|
||||||
|
:task (e2e/cleanup-branch-order-test-repo)}
|
||||||
|
e2e:cleanup {:doc "Cleanup all e2e test repos"
|
||||||
|
:task (e2e/cleanup-all)}}}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
;; Package map for cross-repository SCIP navigation
|
||||||
|
;; Maps namespace prefixes to package@version coordinates
|
||||||
|
;;
|
||||||
|
;; This enables Sourcegraph to resolve cross-repo references:
|
||||||
|
;; - "tui" prefix -> clojure-tui repository
|
||||||
|
;; - "lazygitclj" prefix -> this repository
|
||||||
|
|
||||||
|
{"tui" "io.github.ajet/clojure-tui@main"
|
||||||
|
"lazygitclj" "io.github.ajet/lazygitclj@main"}
|
||||||
+242
-277
@@ -1,9 +1,13 @@
|
|||||||
(ns lazygitclj.core
|
(ns lazygitclj.core
|
||||||
"lazygitclj - A lazygit-inspired TUI for git."
|
"lazygitclj - A lazygit-inspired TUI for git."
|
||||||
(:require [tui.core :as tui :refer [quit]]
|
(:require [tui.core :as tui]
|
||||||
|
[tui.events :as ev :refer [key=]]
|
||||||
|
[tui.terminal :as term]
|
||||||
[lazygitclj.git :as git]
|
[lazygitclj.git :as git]
|
||||||
[clojure.string :as str]))
|
[clojure.string :as str]))
|
||||||
|
|
||||||
|
(def temp tui/run)
|
||||||
|
|
||||||
;; === Model ===
|
;; === Model ===
|
||||||
|
|
||||||
(defn load-git-data []
|
(defn load-git-data []
|
||||||
@@ -53,33 +57,8 @@
|
|||||||
(str (subs s 0 max-len) "...")
|
(str (subs s 0 max-len) "...")
|
||||||
s))
|
s))
|
||||||
|
|
||||||
(defn visible-window
|
|
||||||
"Calculate the visible window of items for scrolling.
|
|
||||||
Returns {:items visible-items :start-idx start-index :total total-count}
|
|
||||||
Keeps cursor visible by scrolling the window."
|
|
||||||
[items cursor max-visible]
|
|
||||||
(let [total (count items)
|
|
||||||
max-visible (max 1 max-visible)]
|
|
||||||
(if (<= total max-visible)
|
|
||||||
;; All items fit, no scrolling needed
|
|
||||||
{:items items :start-idx 0 :total total}
|
|
||||||
;; Need to scroll - calculate window that keeps cursor visible
|
|
||||||
(let [;; Simple approach: cursor should be in the middle when possible
|
|
||||||
half (quot max-visible 2)
|
|
||||||
;; Calculate start index
|
|
||||||
start-idx (cond
|
|
||||||
;; Cursor near start - show from beginning
|
|
||||||
(<= cursor half) 0
|
|
||||||
;; Cursor near end - show end portion
|
|
||||||
(>= cursor (- total half)) (- total max-visible)
|
|
||||||
;; Cursor in middle - center it
|
|
||||||
:else (- cursor half))
|
|
||||||
start-idx (max 0 (min start-idx (- total max-visible)))
|
|
||||||
visible-items (subvec (vec items) start-idx (+ start-idx max-visible))]
|
|
||||||
{:items visible-items :start-idx start-idx :total total}))))
|
|
||||||
|
|
||||||
(defn get-current-diff
|
(defn get-current-diff
|
||||||
"Get the diff for the currently selected item based on panel and cursor."
|
"Change doc string for test"
|
||||||
[model]
|
[model]
|
||||||
(let [panel (:panel model)
|
(let [panel (:panel model)
|
||||||
items (current-items model)
|
items (current-items model)
|
||||||
@@ -113,7 +92,8 @@
|
|||||||
;; === Update ===
|
;; === Update ===
|
||||||
|
|
||||||
(defn update-diff [model]
|
(defn update-diff [model]
|
||||||
(assoc model :diff (get-current-diff model)))
|
;; never update lol
|
||||||
|
model)
|
||||||
|
|
||||||
(defn initial-model []
|
(defn initial-model []
|
||||||
(let [base (merge
|
(let [base (merge
|
||||||
@@ -137,496 +117,494 @@
|
|||||||
(assoc :message nil)
|
(assoc :message nil)
|
||||||
update-diff))
|
update-diff))
|
||||||
|
|
||||||
(defn update-files [model msg]
|
(defn update-files [{:keys [model event]}]
|
||||||
(let [items (file-items model)
|
(let [items (file-items model)
|
||||||
cursor (:cursor model)
|
cursor (:cursor model)
|
||||||
item (get items cursor)]
|
item (get items cursor)]
|
||||||
(cond
|
(cond
|
||||||
(tui/key= msg " ")
|
(key= event \space)
|
||||||
(if item
|
(if item
|
||||||
(do
|
(do
|
||||||
(if (= (:type item) :staged)
|
(if (= (:type item) :staged)
|
||||||
(git/unstage-file (:path item))
|
(git/unstage-file (:path item))
|
||||||
(git/stage-file (:path item)))
|
(git/stage-file (:path item)))
|
||||||
[(refresh model) nil])
|
{:model (refresh model)})
|
||||||
[model nil])
|
{:model model})
|
||||||
|
|
||||||
(tui/key= msg "a")
|
(key= event \a)
|
||||||
(if (and (seq (:staged model)) (empty? (:unstaged model)))
|
(if (and (seq (:staged model)) (empty? (:unstaged model)))
|
||||||
;; All files are staged - unstage all
|
;; All files are staged - unstage all
|
||||||
(do
|
(do
|
||||||
(git/reset-staged)
|
(git/reset-staged)
|
||||||
[(refresh model) nil])
|
{:model (refresh model)})
|
||||||
;; Some files are unstaged - stage all
|
;; Some files are unstaged - stage all
|
||||||
(do
|
(do
|
||||||
(git/stage-all)
|
(git/stage-all)
|
||||||
[(refresh model) nil]))
|
{:model (refresh model)}))
|
||||||
|
|
||||||
(tui/key= msg "d")
|
(key= event \d)
|
||||||
(if (and item (= (:type item) :unstaged) (not= (:index item) \?))
|
(if (and item (= (:type item) :unstaged) (not= (:index item) \?))
|
||||||
(do
|
(do
|
||||||
(git/discard-file (:path item))
|
(git/discard-file (:path item))
|
||||||
[(refresh model) nil])
|
{:model (refresh model)})
|
||||||
[(assoc model :message "Can only discard unstaged changes") nil])
|
{:model (assoc model :message "Can only discard unstaged changes")})
|
||||||
|
|
||||||
;; s: Quick stash all changes
|
;; s: Quick stash all changes
|
||||||
(tui/key= msg "s")
|
(key= event \s)
|
||||||
(do
|
(do
|
||||||
(git/stash-all)
|
(git/stash-all)
|
||||||
[(-> model refresh (assoc :message "Stashed all changes")) nil])
|
{:model (-> model refresh (assoc :message "Stashed all changes"))})
|
||||||
|
|
||||||
;; S: Stash options menu
|
;; S: Stash options menu
|
||||||
(tui/key= msg "S")
|
(key= event \s #{:shift})
|
||||||
[(assoc model :menu-mode :stash-options) nil]
|
{:model (assoc model :menu-mode :stash-options)}
|
||||||
|
|
||||||
:else
|
:else
|
||||||
[model nil])))
|
{:model model})))
|
||||||
|
|
||||||
(defn update-commits [model msg]
|
(defn update-commits [{:keys [model event]}]
|
||||||
(let [items (current-items model)
|
(let [items (current-items model)
|
||||||
cursor (:cursor model)
|
cursor (:cursor model)
|
||||||
item (get (vec items) cursor)
|
item (get (vec items) cursor)
|
||||||
sha (when item (:sha item))]
|
sha (when item (:sha item))]
|
||||||
(cond
|
(cond
|
||||||
;; Space: Checkout commit/reflog entry
|
;; Space: Checkout commit/reflog entry
|
||||||
(tui/key= msg " ")
|
(key= event \space)
|
||||||
(if sha
|
(if sha
|
||||||
(do
|
(do
|
||||||
(git/checkout-reflog-entry sha)
|
(git/checkout-reflog-entry sha)
|
||||||
[(-> model refresh (assoc :message (str "Checked out " sha))) nil])
|
{:model (-> model refresh (assoc :message (str "Checked out " sha)))})
|
||||||
[model nil])
|
{:model model})
|
||||||
|
|
||||||
;; g: Reset to this commit
|
;; g: Reset to this commit
|
||||||
(tui/key= msg "g")
|
(key= event \g)
|
||||||
(if sha
|
(if sha
|
||||||
(do
|
(do
|
||||||
(git/reset-to-reflog sha)
|
(git/reset-to-reflog sha)
|
||||||
[(-> model refresh (assoc :message (str "Reset to " sha))) nil])
|
{:model (-> model refresh (assoc :message (str "Reset to " sha)))})
|
||||||
[model nil])
|
{:model model})
|
||||||
|
|
||||||
;; C: Cherry-pick commit
|
;; C: Cherry-pick commit
|
||||||
(tui/key= msg "C")
|
(key= event \c #{:shift})
|
||||||
(if sha
|
(if sha
|
||||||
(do
|
(do
|
||||||
(git/cherry-pick-commit sha)
|
(git/cherry-pick-commit sha)
|
||||||
[(-> model refresh (assoc :message (str "Cherry-picked " sha))) nil])
|
{:model (-> model refresh (assoc :message (str "Cherry-picked " sha)))})
|
||||||
[model nil])
|
{:model model})
|
||||||
|
|
||||||
;; t: Revert commit
|
;; t: Revert commit
|
||||||
(tui/key= msg "t")
|
(key= event \t)
|
||||||
(if sha
|
(if sha
|
||||||
(do
|
(do
|
||||||
(git/revert-commit sha)
|
(git/revert-commit sha)
|
||||||
[(-> model refresh (assoc :message (str "Reverted " sha))) nil])
|
{:model (-> model refresh (assoc :message (str "Reverted " sha)))})
|
||||||
[model nil])
|
{:model model})
|
||||||
|
|
||||||
;; r: Reword commit (HEAD only)
|
;; r: Reword commit (HEAD only)
|
||||||
(tui/key= msg "r")
|
(key= event \r)
|
||||||
(if sha
|
(if sha
|
||||||
[(assoc model :input-mode :reword :input-buffer "" :input-context sha) nil]
|
{:model (assoc model :input-mode :reword :input-buffer "" :input-context sha)}
|
||||||
[model nil])
|
{:model model})
|
||||||
|
|
||||||
;; y: Copy SHA
|
;; y: Copy SHA
|
||||||
(tui/key= msg "y")
|
(key= event \y)
|
||||||
(if sha
|
(if sha
|
||||||
[(assoc model :message (str "SHA: " sha " (not copied - no clipboard support)")) nil]
|
{:model (assoc model :message (str "SHA: " sha " (not copied - no clipboard support)"))}
|
||||||
[model nil])
|
{:model model})
|
||||||
|
|
||||||
:else
|
:else
|
||||||
[model nil])))
|
{:model model})))
|
||||||
|
|
||||||
(defn update-branches [model msg]
|
(defn update-branches [{:keys [model event]}]
|
||||||
(let [items (current-items model)
|
(let [items (current-items model)
|
||||||
cursor (:cursor model)
|
cursor (:cursor model)
|
||||||
item (get (vec items) cursor)
|
item (get (vec items) cursor)
|
||||||
tab (:branches-tab model)]
|
tab (:branches-tab model)]
|
||||||
(cond
|
(cond
|
||||||
;; Enter: checkout branch/tag
|
;; Enter: checkout branch/tag
|
||||||
(tui/key= msg :enter)
|
(key= event :enter)
|
||||||
(cond
|
(cond
|
||||||
(and (= tab :local) item (not= item (:branch model)))
|
(and (= tab :local) item (not= item (:branch model)))
|
||||||
(do
|
(do
|
||||||
(git/checkout-branch item)
|
(git/checkout-branch item)
|
||||||
[(-> model refresh (assoc :message (str "Checked out " item))) nil])
|
{:model (-> model refresh (assoc :message (str "Checked out " item)))})
|
||||||
|
|
||||||
(and (= tab :tags) item)
|
(and (= tab :tags) item)
|
||||||
(do
|
(do
|
||||||
(git/checkout-tag item)
|
(git/checkout-tag item)
|
||||||
[(-> model refresh (assoc :message (str "Checked out tag " item))) nil])
|
{:model (-> model refresh (assoc :message (str "Checked out tag " item)))})
|
||||||
|
|
||||||
:else [model nil])
|
:else {:model model})
|
||||||
|
|
||||||
;; n: new branch (local tab only)
|
;; n: new branch (local tab only)
|
||||||
(and (tui/key= msg "n") (= tab :local))
|
(and (key= event \n) (= tab :local))
|
||||||
[(assoc model :input-mode :new-branch :input-buffer "") nil]
|
{:model (assoc model :input-mode :new-branch :input-buffer "")}
|
||||||
|
|
||||||
;; d: delete branch (local tab only)
|
;; d: delete branch (local tab only)
|
||||||
(and (tui/key= msg "d") (= tab :local) item (not= item (:branch model)))
|
(and (key= event \d) (= tab :local) item (not= item (:branch model)))
|
||||||
(do
|
(do
|
||||||
(git/delete-branch item)
|
(git/delete-branch item)
|
||||||
[(-> model refresh (assoc :message (str "Deleted branch " item))) nil])
|
{:model (-> model refresh (assoc :message (str "Deleted branch " item)))})
|
||||||
|
|
||||||
;; R: rename branch (local tab only)
|
;; R: rename branch (local tab only)
|
||||||
(and (tui/key= msg "R") (= tab :local) item)
|
(and (key= event \r #{:shift}) (= tab :local) item)
|
||||||
[(assoc model :input-mode :rename-branch :input-buffer "" :input-context item) nil]
|
{:model (assoc model :input-mode :rename-branch :input-buffer "" :input-context item)}
|
||||||
|
|
||||||
;; M: merge branch into current
|
;; M: merge branch into current
|
||||||
(and (tui/key= msg "M") (= tab :local) item (not= item (:branch model)))
|
(and (key= event \m #{:shift}) (= tab :local) item (not= item (:branch model)))
|
||||||
(do
|
(do
|
||||||
(git/merge-branch item)
|
(git/merge-branch item)
|
||||||
[(-> model refresh (assoc :message (str "Merged " item))) nil])
|
{:model (-> model refresh (assoc :message (str "Merged " item)))})
|
||||||
|
|
||||||
;; f: fast-forward branch
|
;; f: fast-forward branch
|
||||||
(and (tui/key= msg "f") (= tab :local) item)
|
(and (key= event \f) (= tab :local) item)
|
||||||
(do
|
(do
|
||||||
(git/fast-forward-branch item)
|
(git/fast-forward-branch item)
|
||||||
[(-> model refresh (assoc :message (str "Fast-forwarded " item))) nil])
|
{:model (-> model refresh (assoc :message (str "Fast-forwarded " item)))})
|
||||||
|
|
||||||
;; Tab switching with [ and ]
|
;; Tab switching with [ and ]
|
||||||
(tui/key= msg "[")
|
(key= event \[)
|
||||||
[(-> model
|
{:model (-> model
|
||||||
(update :branches-tab #(case % :local :tags :remotes :local :tags :remotes))
|
(update :branches-tab #(case % :local :tags :remotes :local :tags :remotes))
|
||||||
(assoc :cursor 0)
|
(assoc :cursor 0)
|
||||||
clamp-cursor) nil]
|
clamp-cursor)}
|
||||||
|
|
||||||
(tui/key= msg "]")
|
(key= event \])
|
||||||
[(-> model
|
{:model (-> model
|
||||||
(update :branches-tab #(case % :local :remotes :remotes :tags :tags :local))
|
(update :branches-tab #(case % :local :remotes :remotes :tags :tags :local))
|
||||||
(assoc :cursor 0)
|
(assoc :cursor 0)
|
||||||
clamp-cursor) nil]
|
clamp-cursor)}
|
||||||
|
|
||||||
:else
|
:else
|
||||||
[model nil])))
|
{:model model})))
|
||||||
|
|
||||||
(defn update-stash [model msg]
|
(defn update-stash [{:keys [model event]}]
|
||||||
(let [stashes (:stashes model)
|
(let [stashes (:stashes model)
|
||||||
cursor (:cursor model)
|
cursor (:cursor model)
|
||||||
stash (get (vec stashes) cursor)
|
stash (get (vec stashes) cursor)
|
||||||
ref (when stash (:ref stash))]
|
ref (when stash (:ref stash))]
|
||||||
(cond
|
(cond
|
||||||
;; Space: apply stash (keep in list)
|
;; Space: apply stash (keep in list)
|
||||||
(tui/key= msg " ")
|
(key= event \space)
|
||||||
(if ref
|
(if ref
|
||||||
(do
|
(do
|
||||||
(git/stash-apply ref)
|
(git/stash-apply ref)
|
||||||
[(-> model refresh (assoc :message (str "Applied " ref))) nil])
|
{:model (-> model refresh (assoc :message (str "Applied " ref)))})
|
||||||
[model nil])
|
{:model model})
|
||||||
|
|
||||||
;; g: pop stash (apply and remove)
|
;; g: pop stash (apply and remove)
|
||||||
(tui/key= msg "g")
|
(key= event \g)
|
||||||
(if ref
|
(if ref
|
||||||
(do
|
(do
|
||||||
(git/stash-pop ref)
|
(git/stash-pop ref)
|
||||||
[(-> model refresh (assoc :message (str "Popped " ref))) nil])
|
{:model (-> model refresh (assoc :message (str "Popped " ref)))})
|
||||||
[model nil])
|
{:model model})
|
||||||
|
|
||||||
;; d: drop stash
|
;; d: drop stash
|
||||||
(tui/key= msg "d")
|
(key= event \d)
|
||||||
(if ref
|
(if ref
|
||||||
(do
|
(do
|
||||||
(git/stash-drop ref)
|
(git/stash-drop ref)
|
||||||
[(-> model refresh (assoc :message (str "Dropped " ref))) nil])
|
{:model (-> model refresh (assoc :message (str "Dropped " ref)))})
|
||||||
[model nil])
|
{:model model})
|
||||||
|
|
||||||
;; n: new branch from stash
|
;; n: new branch from stash
|
||||||
(and (tui/key= msg "n") ref)
|
(and (key= event \n) ref)
|
||||||
[(assoc model :input-mode :stash-branch :input-buffer "" :input-context ref) nil]
|
{:model (assoc model :input-mode :stash-branch :input-buffer "" :input-context ref)}
|
||||||
|
|
||||||
:else
|
:else
|
||||||
[model nil])))
|
{:model model})))
|
||||||
|
|
||||||
(defn update-input-mode [model msg]
|
(defn update-input-mode [{:keys [model event]}]
|
||||||
(cond
|
(cond
|
||||||
(tui/key= msg :escape)
|
(key= event :escape)
|
||||||
[(assoc model :input-mode nil :input-buffer "" :input-context nil) nil]
|
{:model (assoc model :input-mode nil :input-buffer "" :input-context nil)}
|
||||||
|
|
||||||
(tui/key= msg :enter)
|
(key= event :enter)
|
||||||
(let [buf (:input-buffer model)
|
(let [buf (:input-buffer model)
|
||||||
ctx (:input-context model)]
|
ctx (:input-context model)]
|
||||||
(case (:input-mode model)
|
(case (:input-mode model)
|
||||||
:commit
|
:commit
|
||||||
(if (str/blank? buf)
|
(if (str/blank? buf)
|
||||||
[(assoc model :message "Commit message cannot be empty") nil]
|
{:model (assoc model :message "Commit message cannot be empty")}
|
||||||
(do
|
(do
|
||||||
(git/commit buf)
|
(git/commit buf)
|
||||||
[(-> model
|
{:model (-> model
|
||||||
(assoc :input-mode nil :input-buffer "" :input-context nil)
|
(assoc :input-mode nil :input-buffer "" :input-context nil)
|
||||||
refresh
|
refresh
|
||||||
(assoc :message "Committed!")) nil]))
|
(assoc :message "Committed!"))}))
|
||||||
|
|
||||||
:new-branch
|
:new-branch
|
||||||
(if (str/blank? buf)
|
(if (str/blank? buf)
|
||||||
[(assoc model :message "Branch name cannot be empty") nil]
|
{:model (assoc model :message "Branch name cannot be empty")}
|
||||||
(do
|
(do
|
||||||
(git/create-branch buf)
|
(git/create-branch buf)
|
||||||
[(-> model
|
{:model (-> model
|
||||||
(assoc :input-mode nil :input-buffer "" :input-context nil)
|
(assoc :input-mode nil :input-buffer "" :input-context nil)
|
||||||
refresh
|
refresh
|
||||||
(assoc :message (str "Created branch " buf))) nil]))
|
(assoc :message (str "Created branch " buf)))}))
|
||||||
|
|
||||||
:rename-branch
|
:rename-branch
|
||||||
(if (str/blank? buf)
|
(if (str/blank? buf)
|
||||||
[(assoc model :message "New name cannot be empty") nil]
|
{:model (assoc model :message "New name cannot be empty")}
|
||||||
(do
|
(do
|
||||||
(git/rename-branch ctx buf)
|
(git/rename-branch ctx buf)
|
||||||
[(-> model
|
{:model (-> model
|
||||||
(assoc :input-mode nil :input-buffer "" :input-context nil)
|
(assoc :input-mode nil :input-buffer "" :input-context nil)
|
||||||
refresh
|
refresh
|
||||||
(assoc :message (str "Renamed " ctx " to " buf))) nil]))
|
(assoc :message (str "Renamed " ctx " to " buf)))}))
|
||||||
|
|
||||||
:stash-branch
|
:stash-branch
|
||||||
(if (str/blank? buf)
|
(if (str/blank? buf)
|
||||||
[(assoc model :message "Branch name cannot be empty") nil]
|
{:model (assoc model :message "Branch name cannot be empty")}
|
||||||
(do
|
(do
|
||||||
(git/stash-branch buf ctx)
|
(git/stash-branch buf ctx)
|
||||||
[(-> model
|
{:model (-> model
|
||||||
(assoc :input-mode nil :input-buffer "" :input-context nil)
|
(assoc :input-mode nil :input-buffer "" :input-context nil)
|
||||||
refresh
|
refresh
|
||||||
(assoc :message (str "Created branch " buf " from stash"))) nil]))
|
(assoc :message (str "Created branch " buf " from stash")))}))
|
||||||
|
|
||||||
:reword
|
:reword
|
||||||
(if (str/blank? buf)
|
(if (str/blank? buf)
|
||||||
[(assoc model :message "Message cannot be empty") nil]
|
{:model (assoc model :message "Message cannot be empty")}
|
||||||
(if (git/reword-commit ctx buf)
|
(if (git/reword-commit ctx buf)
|
||||||
[(-> model
|
{:model (-> model
|
||||||
(assoc :input-mode nil :input-buffer "" :input-context nil)
|
(assoc :input-mode nil :input-buffer "" :input-context nil)
|
||||||
refresh
|
refresh
|
||||||
(assoc :message "Rewrote commit message")) nil]
|
(assoc :message "Rewrote commit message"))}
|
||||||
[(assoc model :input-mode nil :input-buffer "" :input-context nil
|
{:model (assoc model :input-mode nil :input-buffer "" :input-context nil
|
||||||
:message "Can only reword HEAD commit") nil]))
|
:message "Can only reword HEAD commit")}))
|
||||||
|
|
||||||
[model nil]))
|
{:model model}))
|
||||||
|
|
||||||
(tui/key= msg :backspace)
|
(key= event :backspace)
|
||||||
[(update model :input-buffer #(if (empty? %) % (subs % 0 (dec (count %))))) nil]
|
{:model (update model :input-buffer #(if (empty? %) % (subs % 0 (dec (count %)))))}
|
||||||
|
|
||||||
;; Character input - key format is [:key {:char \a}]
|
;; Character input - new event format {:type :key :key \a}
|
||||||
(and (= (first msg) :key)
|
(and (= (:type event) :key)
|
||||||
(map? (second msg))
|
(char? (:key event))
|
||||||
(:char (second msg))
|
(not (:modifiers event)))
|
||||||
(not (:ctrl (second msg)))
|
{:model (update model :input-buffer str (:key event))}
|
||||||
(not (:alt (second msg))))
|
|
||||||
[(update model :input-buffer str (:char (second msg))) nil]
|
|
||||||
|
|
||||||
:else
|
:else
|
||||||
[model nil]))
|
{:model model}))
|
||||||
|
|
||||||
(defn update-stash-menu [model msg]
|
(defn update-stash-menu [{:keys [model event]}]
|
||||||
(cond
|
(cond
|
||||||
(tui/key= msg :escape)
|
(key= event :escape)
|
||||||
[(assoc model :menu-mode nil) nil]
|
{:model (assoc model :menu-mode nil)}
|
||||||
|
|
||||||
;; a: Stash all changes
|
;; a: Stash all changes
|
||||||
(tui/key= msg "a")
|
(key= event \a)
|
||||||
(do
|
(do
|
||||||
(git/stash-all)
|
(git/stash-all)
|
||||||
[(-> model (assoc :menu-mode nil) refresh (assoc :message "Stashed all changes")) nil])
|
{:model (-> model (assoc :menu-mode nil) refresh (assoc :message "Stashed all changes"))})
|
||||||
|
|
||||||
;; i: Stash all changes and keep index
|
;; i: Stash all changes and keep index
|
||||||
(tui/key= msg "i")
|
(key= event \i)
|
||||||
(do
|
(do
|
||||||
(git/stash-keep-index)
|
(git/stash-keep-index)
|
||||||
[(-> model (assoc :menu-mode nil) refresh (assoc :message "Stashed all changes (kept index)")) nil])
|
{:model (-> model (assoc :menu-mode nil) refresh (assoc :message "Stashed all changes (kept index)"))})
|
||||||
|
|
||||||
;; U: Stash all including untracked files
|
;; U: Stash all including untracked files
|
||||||
(tui/key= msg "U")
|
(key= event \u #{:shift})
|
||||||
(do
|
(do
|
||||||
(git/stash-include-untracked)
|
(git/stash-include-untracked)
|
||||||
[(-> model (assoc :menu-mode nil) refresh (assoc :message "Stashed all changes (including untracked)")) nil])
|
{:model (-> model (assoc :menu-mode nil) refresh (assoc :message "Stashed all changes (including untracked)"))})
|
||||||
|
|
||||||
;; s: Stash staged changes only
|
;; s: Stash staged changes only
|
||||||
(tui/key= msg "s")
|
(key= event \s)
|
||||||
(do
|
(do
|
||||||
(git/stash-staged)
|
(git/stash-staged)
|
||||||
[(-> model (assoc :menu-mode nil) refresh (assoc :message "Stashed staged changes only")) nil])
|
{:model (-> model (assoc :menu-mode nil) refresh (assoc :message "Stashed staged changes only"))})
|
||||||
|
|
||||||
;; u: Stash unstaged changes only
|
;; u: Stash unstaged changes only
|
||||||
(tui/key= msg "u")
|
(key= event \u)
|
||||||
(do
|
(do
|
||||||
(git/stash-unstaged)
|
(git/stash-unstaged)
|
||||||
[(-> model (assoc :menu-mode nil) refresh (assoc :message "Stashed unstaged changes only")) nil])
|
{:model (-> model (assoc :menu-mode nil) refresh (assoc :message "Stashed unstaged changes only"))})
|
||||||
|
|
||||||
:else
|
:else
|
||||||
[model nil]))
|
{:model model}))
|
||||||
|
|
||||||
(defn update-reset-menu [model msg]
|
(defn update-reset-menu [{:keys [model event]}]
|
||||||
(cond
|
(cond
|
||||||
(tui/key= msg :escape)
|
(key= event :escape)
|
||||||
[(assoc model :menu-mode nil) nil]
|
{:model (assoc model :menu-mode nil)}
|
||||||
|
|
||||||
;; s: Soft reset (keep staged)
|
;; s: Soft reset (keep staged)
|
||||||
(tui/key= msg "s")
|
(key= event \s)
|
||||||
(do
|
(do
|
||||||
(git/reset-soft)
|
(git/reset-soft)
|
||||||
[(-> model (assoc :menu-mode nil) refresh (assoc :message "Soft reset to HEAD~1")) nil])
|
{:model (-> model (assoc :menu-mode nil) refresh (assoc :message "Soft reset to HEAD~1"))})
|
||||||
|
|
||||||
;; m: Mixed reset (unstage)
|
;; m: Mixed reset (unstage)
|
||||||
(tui/key= msg "m")
|
(key= event \m)
|
||||||
(do
|
(do
|
||||||
(git/reset-mixed)
|
(git/reset-mixed)
|
||||||
[(-> model (assoc :menu-mode nil) refresh (assoc :message "Mixed reset to HEAD~1")) nil])
|
{:model (-> model (assoc :menu-mode nil) refresh (assoc :message "Mixed reset to HEAD~1"))})
|
||||||
|
|
||||||
;; h: Hard reset (discard all)
|
;; h: Hard reset (discard all)
|
||||||
(tui/key= msg "h")
|
(key= event \h)
|
||||||
(do
|
(do
|
||||||
(git/reset-hard)
|
(git/reset-hard)
|
||||||
[(-> model (assoc :menu-mode nil) refresh (assoc :message "Hard reset to HEAD~1")) nil])
|
{:model (-> model (assoc :menu-mode nil) refresh (assoc :message "Hard reset to HEAD~1"))})
|
||||||
|
|
||||||
;; u: Unstage all
|
;; u: Unstage all
|
||||||
(tui/key= msg "u")
|
(key= event \u)
|
||||||
(do
|
(do
|
||||||
(git/reset-staged)
|
(git/reset-staged)
|
||||||
[(-> model (assoc :menu-mode nil) refresh (assoc :message "Unstaged all changes")) nil])
|
{:model (-> model (assoc :menu-mode nil) refresh (assoc :message "Unstaged all changes"))})
|
||||||
|
|
||||||
;; d: Discard unstaged changes
|
;; d: Discard unstaged changes
|
||||||
(tui/key= msg "d")
|
(key= event \d)
|
||||||
(do
|
(do
|
||||||
(git/discard-all-unstaged)
|
(git/discard-all-unstaged)
|
||||||
[(-> model (assoc :menu-mode nil) refresh (assoc :message "Discarded unstaged changes")) nil])
|
{:model (-> model (assoc :menu-mode nil) refresh (assoc :message "Discarded unstaged changes"))})
|
||||||
|
|
||||||
;; c: Clean untracked files
|
;; c: Clean untracked files
|
||||||
(tui/key= msg "c")
|
(key= event \c)
|
||||||
(do
|
(do
|
||||||
(git/clean-untracked)
|
(git/clean-untracked)
|
||||||
[(-> model (assoc :menu-mode nil) refresh (assoc :message "Cleaned untracked files")) nil])
|
{:model (-> model (assoc :menu-mode nil) refresh (assoc :message "Cleaned untracked files"))})
|
||||||
|
|
||||||
:else
|
:else
|
||||||
[model nil]))
|
{:model model}))
|
||||||
|
|
||||||
(defn update-help [model msg]
|
(defn update-help [{:keys [model event]}]
|
||||||
(cond
|
(cond
|
||||||
(or (tui/key= msg :escape) (tui/key= msg "?") (tui/key= msg "q"))
|
(or (key= event :escape) (key= event \?) (key= event \q))
|
||||||
[(assoc model :menu-mode nil) nil]
|
{:model (assoc model :menu-mode nil)}
|
||||||
|
|
||||||
:else
|
:else
|
||||||
[model nil]))
|
{:model model}))
|
||||||
|
|
||||||
(defn update-model [model msg]
|
(defn update-model [{:keys [model event] :as ctx}]
|
||||||
(cond
|
(cond
|
||||||
;; Menu modes take priority
|
;; Menu modes take priority
|
||||||
(= (:menu-mode model) :stash-options)
|
(= (:menu-mode model) :stash-options)
|
||||||
(update-stash-menu model msg)
|
(update-stash-menu ctx)
|
||||||
|
|
||||||
(= (:menu-mode model) :reset-options)
|
(= (:menu-mode model) :reset-options)
|
||||||
(update-reset-menu model msg)
|
(update-reset-menu ctx)
|
||||||
|
|
||||||
(= (:menu-mode model) :help)
|
(= (:menu-mode model) :help)
|
||||||
(update-help model msg)
|
(update-help ctx)
|
||||||
|
|
||||||
(:input-mode model)
|
(:input-mode model)
|
||||||
(update-input-mode model msg)
|
(update-input-mode ctx)
|
||||||
|
|
||||||
(or (tui/key= msg "q") (tui/key= msg [:ctrl \c]))
|
(or (key= event \q) (key= event \c #{:ctrl}))
|
||||||
[model quit]
|
{:model model :events [(ev/quit)]}
|
||||||
|
|
||||||
(tui/key= msg "r")
|
(key= event \r)
|
||||||
[(refresh model) nil]
|
{:model (refresh model)}
|
||||||
|
|
||||||
;; Help panel
|
;; Help panel
|
||||||
(tui/key= msg "?")
|
(key= event \?)
|
||||||
[(assoc model :menu-mode :help) nil]
|
{:model (assoc model :menu-mode :help)}
|
||||||
|
|
||||||
;; Reset options menu
|
;; Reset options menu
|
||||||
(tui/key= msg "D")
|
(key= event \d #{:shift})
|
||||||
[(assoc model :menu-mode :reset-options) nil]
|
{:model (assoc model :menu-mode :reset-options)}
|
||||||
|
|
||||||
;; Panel jump keys (matching lazygit: 2=Files, 3=Branches, 4=Commits, 5=Stash)
|
;; Panel jump keys (matching lazygit: 2=Files, 3=Branches, 4=Commits, 5=Stash)
|
||||||
(tui/key= msg "2")
|
(key= event \2)
|
||||||
[(-> model (assoc :panel :files :cursor 0) clamp-cursor update-diff) nil]
|
{:model (-> model (assoc :panel :files :cursor 0) clamp-cursor update-diff)}
|
||||||
|
|
||||||
(tui/key= msg "3")
|
(key= event \3)
|
||||||
[(-> model (assoc :panel :branches :cursor 0) clamp-cursor update-diff) nil]
|
{:model (-> model (assoc :panel :branches :cursor 0) clamp-cursor update-diff)}
|
||||||
|
|
||||||
(tui/key= msg "4")
|
(key= event \4)
|
||||||
[(-> model (assoc :panel :commits :cursor 0) clamp-cursor update-diff) nil]
|
{:model (-> model (assoc :panel :commits :cursor 0) clamp-cursor update-diff)}
|
||||||
|
|
||||||
(tui/key= msg "5")
|
(key= event \5)
|
||||||
[(-> model (assoc :panel :stash :cursor 0) clamp-cursor update-diff) nil]
|
{:model (-> model (assoc :panel :stash :cursor 0) clamp-cursor update-diff)}
|
||||||
|
|
||||||
;; h/l or Left/Right: move between panels (order: files → branches → commits → stash)
|
;; h/l or Left/Right: move between panels (order: files → branches → commits → stash)
|
||||||
(or (tui/key= msg "h") (tui/key= msg :left))
|
(or (key= event \h) (key= event :left))
|
||||||
[(-> model
|
{:model (-> model
|
||||||
(update :panel #(case % :files :stash :branches :files :commits :branches :stash :commits))
|
(update :panel #(case % :files :stash :branches :files :commits :branches :stash :commits))
|
||||||
(assoc :cursor 0)
|
(assoc :cursor 0)
|
||||||
clamp-cursor
|
clamp-cursor
|
||||||
update-diff) nil]
|
update-diff)}
|
||||||
|
|
||||||
(or (tui/key= msg "l") (tui/key= msg :right))
|
(or (key= event \l) (key= event :right))
|
||||||
[(-> model
|
{:model (-> model
|
||||||
(update :panel #(case % :files :branches :branches :commits :commits :stash :stash :files))
|
(update :panel #(case % :files :branches :branches :commits :commits :stash :stash :files))
|
||||||
(assoc :cursor 0)
|
(assoc :cursor 0)
|
||||||
clamp-cursor
|
clamp-cursor
|
||||||
update-diff) nil]
|
update-diff)}
|
||||||
|
|
||||||
(or (tui/key= msg :up) (tui/key= msg "k"))
|
(or (key= event :up) (key= event \k))
|
||||||
[(-> model (update :cursor dec) clamp-cursor update-diff) nil]
|
{:model (-> model (update :cursor dec) clamp-cursor update-diff)}
|
||||||
|
|
||||||
(or (tui/key= msg :down) (tui/key= msg "j"))
|
(or (key= event :down) (key= event \j))
|
||||||
[(-> model (update :cursor inc) clamp-cursor update-diff) nil]
|
{:model (-> model (update :cursor inc) clamp-cursor update-diff)}
|
||||||
|
|
||||||
(and (tui/key= msg "c") (= (:panel model) :files))
|
(and (key= event \c) (= (:panel model) :files))
|
||||||
(if (empty? (:staged model))
|
(if (empty? (:staged model))
|
||||||
[(assoc model :message "Nothing staged to commit") nil]
|
{:model (assoc model :message "Nothing staged to commit")}
|
||||||
[(assoc model :input-mode :commit :input-buffer "") nil])
|
{:model (assoc model :input-mode :commit :input-buffer "")})
|
||||||
|
|
||||||
(tui/key= msg "P")
|
(key= event \p #{:shift})
|
||||||
(do
|
(do
|
||||||
(git/push)
|
(git/push)
|
||||||
[(assoc model :message "Pushed!") nil])
|
{:model (assoc model :message "Pushed!")})
|
||||||
|
|
||||||
(tui/key= msg "p")
|
(key= event \p)
|
||||||
(do
|
(do
|
||||||
(git/pull)
|
(git/pull)
|
||||||
[(-> model refresh (assoc :message "Pulled!")) nil])
|
{:model (-> model refresh (assoc :message "Pulled!"))})
|
||||||
|
|
||||||
;; Tab switching with [ and ] (for commits panel)
|
;; Tab switching with [ and ] (for commits panel)
|
||||||
(and (tui/key= msg "[") (= (:panel model) :commits))
|
(and (key= event \[) (= (:panel model) :commits))
|
||||||
[(-> model
|
{:model (-> model
|
||||||
(update :commits-tab #(if (= % :reflog) :commits :reflog))
|
(update :commits-tab #(if (= % :reflog) :commits :reflog))
|
||||||
(assoc :cursor 0)
|
(assoc :cursor 0)
|
||||||
clamp-cursor) nil]
|
clamp-cursor)}
|
||||||
|
|
||||||
(and (tui/key= msg "]") (= (:panel model) :commits))
|
(and (key= event \]) (= (:panel model) :commits))
|
||||||
[(-> model
|
{:model (-> model
|
||||||
(update :commits-tab #(if (= % :commits) :reflog :commits))
|
(update :commits-tab #(if (= % :commits) :reflog :commits))
|
||||||
(assoc :cursor 0)
|
(assoc :cursor 0)
|
||||||
clamp-cursor) nil]
|
clamp-cursor)}
|
||||||
|
|
||||||
;; Global undo (z): Reset to previous reflog entry
|
;; Global undo (z): Reset to previous reflog entry
|
||||||
(tui/key= msg "z")
|
(key= event \z)
|
||||||
(let [reflog (:reflog model)
|
(let [reflog (:reflog model)
|
||||||
current-idx (or (:reflog-index model) 0)
|
current-idx (or (:reflog-index model) 0)
|
||||||
next-idx (inc current-idx)]
|
next-idx (inc current-idx)]
|
||||||
(if (< next-idx (count reflog))
|
(if (< next-idx (count reflog))
|
||||||
(let [entry (get (vec reflog) next-idx)]
|
(let [entry (get (vec reflog) next-idx)]
|
||||||
(git/reset-to-reflog (:ref entry))
|
(git/reset-to-reflog (:ref entry))
|
||||||
[(-> model
|
{:model (-> model
|
||||||
refresh
|
refresh
|
||||||
(assoc :reflog-index next-idx)
|
(assoc :reflog-index next-idx)
|
||||||
(assoc :message (str "Undo: reset to " (:ref entry)))) nil])
|
(assoc :message (str "Undo: reset to " (:ref entry))))})
|
||||||
[(assoc model :message "Nothing to undo") nil]))
|
{:model (assoc model :message "Nothing to undo")}))
|
||||||
|
|
||||||
;; Global redo (Z): Reset forward in reflog
|
;; Global redo (Z): Reset forward in reflog
|
||||||
(tui/key= msg "Z")
|
(key= event \z #{:shift})
|
||||||
(let [current-idx (or (:reflog-index model) 0)]
|
(let [current-idx (or (:reflog-index model) 0)]
|
||||||
(if (> current-idx 0)
|
(if (> current-idx 0)
|
||||||
(let [prev-idx (dec current-idx)
|
(let [prev-idx (dec current-idx)
|
||||||
reflog (:reflog model)
|
reflog (:reflog model)
|
||||||
entry (get (vec reflog) prev-idx)]
|
entry (get (vec reflog) prev-idx)]
|
||||||
(git/reset-to-reflog (:ref entry))
|
(git/reset-to-reflog (:ref entry))
|
||||||
[(-> model
|
{:model (-> model
|
||||||
refresh
|
refresh
|
||||||
(assoc :reflog-index prev-idx)
|
(assoc :reflog-index prev-idx)
|
||||||
(assoc :message (str "Redo: reset to " (:ref entry)))) nil])
|
(assoc :message (str "Redo: reset to " (:ref entry))))})
|
||||||
[(assoc model :message "Nothing to redo") nil]))
|
{:model (assoc model :message "Nothing to redo")}))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(case (:panel model)
|
(case (:panel model)
|
||||||
:files (update-files model msg)
|
:files (update-files ctx)
|
||||||
:commits (update-commits model msg)
|
:commits (update-commits ctx)
|
||||||
:branches (update-branches model msg)
|
:branches (update-branches ctx)
|
||||||
:stash (update-stash model msg)
|
:stash (update-stash ctx)
|
||||||
[model nil])))
|
{:model model})))
|
||||||
|
|
||||||
;; === Views ===
|
;; === Views ===
|
||||||
|
|
||||||
@@ -648,20 +626,19 @@
|
|||||||
(when sync-info [:text {:fg :cyan} sync-info])]))]))
|
(when sync-info [:text {:fg :cyan} sync-info])]))]))
|
||||||
|
|
||||||
;; Panel 2: Files
|
;; Panel 2: Files
|
||||||
(defn files-panel [{:keys [staged unstaged cursor panel]} max-visible]
|
(defn files-panel [{:keys [staged unstaged cursor panel]}]
|
||||||
(let [active? (= panel :files)
|
(let [active? (= panel :files)
|
||||||
all-files (into (mapv #(assoc % :section :staged) staged)
|
all-files (into (mapv #(assoc % :section :staged) staged)
|
||||||
(mapv #(assoc % :section :unstaged) unstaged))
|
(mapv #(assoc % :section :unstaged) unstaged))
|
||||||
{:keys [items start-idx total]} (visible-window all-files (if active? cursor 0) max-visible)]
|
total (count all-files)]
|
||||||
[:box {:border (if active? :double :single)
|
[:box {:border (if active? :double :single)
|
||||||
:title (str "2 Files (" total ")")
|
:title (str "2 Files (" total ")")
|
||||||
:padding [0 1] :width :fill :height :fill}
|
:padding [0 1] :width :fill :height :fill}
|
||||||
(if (empty? all-files)
|
(if (empty? all-files)
|
||||||
[:text {:fg :gray} "No changes"]
|
[:text {:fg :gray} "No changes"]
|
||||||
(into [:col]
|
(into [:scroll {:cursor (if active? cursor 0)}]
|
||||||
(for [[local-idx file] (map-indexed vector items)]
|
(for [[idx file] (map-indexed vector all-files)]
|
||||||
(let [global-idx (+ start-idx local-idx)
|
(let [is-cursor (and active? (= idx cursor))
|
||||||
is-cursor (and active? (= global-idx cursor))
|
|
||||||
color (if (= (:section file) :staged) :green :red)]
|
color (if (= (:section file) :staged) :green :red)]
|
||||||
[:row {:gap 1}
|
[:row {:gap 1}
|
||||||
[:text {:fg color} (file-status-char file)]
|
[:text {:fg color} (file-status-char file)]
|
||||||
@@ -669,60 +646,55 @@
|
|||||||
(truncate (:path file) 20)]]))))]))
|
(truncate (:path file) 20)]]))))]))
|
||||||
|
|
||||||
;; Panel 3: Branches
|
;; Panel 3: Branches
|
||||||
(defn branches-panel [{:keys [branches remote-branches tags branch branches-tab cursor panel]} max-visible]
|
(defn branches-panel [{:keys [branches remote-branches tags branch branches-tab cursor panel]}]
|
||||||
(let [active? (= panel :branches)
|
(let [active? (= panel :branches)
|
||||||
all-items (case branches-tab :local branches :remotes remote-branches :tags tags branches)
|
all-items (case branches-tab :local branches :remotes remote-branches :tags tags branches)
|
||||||
{:keys [items start-idx total]} (visible-window all-items (if active? cursor 0) max-visible)
|
|
||||||
tab-str (case branches-tab :local "[L] R T" :remotes "L [R] T" :tags "L R [T]")]
|
tab-str (case branches-tab :local "[L] R T" :remotes "L [R] T" :tags "L R [T]")]
|
||||||
[:box {:border (if active? :double :single)
|
[:box {:border (if active? :double :single)
|
||||||
:title (str "3 Branches " tab-str)
|
:title (str "3 Branches " tab-str)
|
||||||
:padding [0 1] :width :fill :height :fill}
|
:padding [0 1] :width :fill :height :fill}
|
||||||
(if (empty? all-items)
|
(if (empty? all-items)
|
||||||
[:text {:fg :gray} (str "No " (name branches-tab))]
|
[:text {:fg :gray} (str "No " (name branches-tab))]
|
||||||
(into [:col]
|
(into [:scroll {:cursor (if active? cursor 0)}]
|
||||||
(for [[local-idx item] (map-indexed vector items)]
|
(for [[idx item] (map-indexed vector all-items)]
|
||||||
(let [global-idx (+ start-idx local-idx)
|
(let [is-cursor (and active? (= idx cursor))
|
||||||
is-cursor (and active? (= global-idx cursor))
|
|
||||||
is-current (and (= branches-tab :local) (= item branch))
|
is-current (and (= branches-tab :local) (= item branch))
|
||||||
fg (cond is-current :green is-cursor :cyan :else :white)]
|
fg (cond is-current :green is-cursor :cyan :else :white)]
|
||||||
[:text {:fg fg :bold (or is-current is-cursor) :inverse is-cursor}
|
[:text {:fg fg :bold (or is-current is-cursor) :inverse is-cursor}
|
||||||
(str (if is-current "* " " ") (truncate item 22))]))))]))
|
(str (if is-current "* " " ") (truncate item 22))]))))]))
|
||||||
|
|
||||||
;; Panel 4: Commits
|
;; Panel 4: Commits
|
||||||
(defn commits-panel [{:keys [commits reflog commits-tab cursor panel]} max-visible]
|
(defn commits-panel [{:keys [commits reflog commits-tab cursor panel]}]
|
||||||
(let [active? (= panel :commits)
|
(let [active? (= panel :commits)
|
||||||
reflog? (= commits-tab :reflog)
|
reflog? (= commits-tab :reflog)
|
||||||
all-items (if reflog? reflog commits)
|
all-items (if reflog? reflog commits)]
|
||||||
{:keys [items start-idx total]} (visible-window all-items (if active? cursor 0) max-visible)]
|
|
||||||
[:box {:border (if active? :double :single)
|
[:box {:border (if active? :double :single)
|
||||||
:title (str "4 Commits " (if reflog? "C [R]" "[C] R"))
|
:title (str "4 Commits " (if reflog? "C [R]" "[C] R"))
|
||||||
:padding [0 1] :width :fill :height :fill}
|
:padding [0 1] :width :fill :height :fill}
|
||||||
(if (empty? all-items)
|
(if (empty? all-items)
|
||||||
[:text {:fg :gray} "No commits"]
|
[:text {:fg :gray} "No commits"]
|
||||||
(into [:col]
|
(into [:scroll {:cursor (if active? cursor 0)}]
|
||||||
(for [[local-idx {:keys [sha action subject]}] (map-indexed vector items)]
|
(for [[idx {:keys [sha action subject]}] (map-indexed vector all-items)]
|
||||||
(let [global-idx (+ start-idx local-idx)
|
(let [is-cursor (and active? (= idx cursor))]
|
||||||
is-cursor (and active? (= global-idx cursor))]
|
|
||||||
[:row {:gap 1}
|
[:row {:gap 1}
|
||||||
[:text {:fg :yellow} (or sha "?")]
|
[:text {:fg :yellow} (or sha "?")]
|
||||||
[:text {:fg (if is-cursor :cyan :white) :bold is-cursor :inverse is-cursor}
|
[:text {:fg (if is-cursor :cyan :white) :bold is-cursor :inverse is-cursor}
|
||||||
(truncate (if reflog? (str action ": " subject) (or subject "")) 14)]]))))]))
|
(truncate (if reflog? (str action ": " subject) (or subject "")) 14)]]))))]))
|
||||||
|
|
||||||
;; Panel 5: Stash
|
;; Panel 5: Stash
|
||||||
(defn stash-panel [{:keys [stashes cursor panel]} max-visible]
|
(defn stash-panel [{:keys [stashes cursor panel]}]
|
||||||
(let [active? (= panel :stash)
|
(let [active? (= panel :stash)
|
||||||
{:keys [items start-idx total]} (visible-window stashes (if active? cursor 0) max-visible)]
|
total (count stashes)]
|
||||||
[:box {:border (if active? :double :single)
|
[:box {:border (if active? :double :single)
|
||||||
:title (str "5 Stash (" total ")")
|
:title (str "5 Stash (" total ")")
|
||||||
:padding [0 1] :width :fill :height :fill}
|
:padding [0 1] :width :fill :height :fill}
|
||||||
(if (empty? stashes)
|
(if (empty? stashes)
|
||||||
[:text {:fg :gray} "No stashes"]
|
[:text {:fg :gray} "No stashes"]
|
||||||
(into [:col]
|
(into [:scroll {:cursor (if active? cursor 0)}]
|
||||||
(for [[local-idx {:keys [index message]}] (map-indexed vector items)]
|
(for [[idx {:keys [index message]}] (map-indexed vector stashes)]
|
||||||
(let [global-idx (+ start-idx local-idx)
|
(let [is-cursor (and active? (= idx cursor))]
|
||||||
is-cursor (and active? (= global-idx cursor))]
|
|
||||||
[:row {:gap 1}
|
[:row {:gap 1}
|
||||||
[:text {:fg :yellow} (str (or index global-idx))]
|
[:text {:fg :yellow} (str (or index idx))]
|
||||||
[:text {:fg (if is-cursor :cyan :white) :bold is-cursor :inverse is-cursor}
|
[:text {:fg (if is-cursor :cyan :white) :bold is-cursor :inverse is-cursor}
|
||||||
(truncate (or message "") 20)]]))))]))
|
(truncate (or message "") 20)]]))))]))
|
||||||
|
|
||||||
@@ -845,54 +817,42 @@
|
|||||||
|
|
||||||
(defn main-grid-view
|
(defn main-grid-view
|
||||||
"Render the main lazygit-style grid layout."
|
"Render the main lazygit-style grid layout."
|
||||||
[model width height]
|
[model]
|
||||||
(let [has-message? (some? (:message model))
|
(let [has-message? (some? (:message model))
|
||||||
|
{:keys [width]} (term/get-terminal-size)
|
||||||
narrow? (< width 70)
|
narrow? (< width 70)
|
||||||
;; Calculate available height for panels
|
|
||||||
;; Wide layout: left column has [3 :flex :flex :flex :flex]
|
|
||||||
;; So 4 flex panels share (height - status(3) - help-bar(1) - message?)
|
|
||||||
;; Each panel total height = remaining / 4
|
|
||||||
;; Inner height = panel total - 2 (borders)
|
|
||||||
remaining-height (- height 3 1 (if has-message? 1 0))
|
|
||||||
panel-total-height (quot remaining-height 4)
|
|
||||||
;; Inner height is total minus borders (2 rows)
|
|
||||||
panel-inner-height (if narrow?
|
|
||||||
;; Narrow: 6 sections share height
|
|
||||||
(max 1 (- (quot remaining-height 6) 2))
|
|
||||||
;; Wide: 4 panels share remaining height
|
|
||||||
(max 1 (- panel-total-height 2)))
|
|
||||||
content (if narrow?
|
content (if narrow?
|
||||||
;; NARROW: Single column stacked
|
;; NARROW: Single column stacked
|
||||||
[:col {:heights [3 :flex :flex :flex 3 :flex 4]}
|
[:col {:heights [3 :flex :flex :flex :flex :flex 4]}
|
||||||
(status-panel model)
|
(status-panel model)
|
||||||
(files-panel model panel-inner-height)
|
(files-panel model)
|
||||||
(branches-panel model panel-inner-height)
|
(branches-panel model)
|
||||||
(commits-panel model panel-inner-height)
|
(commits-panel model)
|
||||||
(stash-panel model panel-inner-height)
|
(stash-panel model)
|
||||||
(main-view-panel model)
|
(main-view-panel model)
|
||||||
(command-log-panel)]
|
(command-log-panel)]
|
||||||
;; WIDE: Two columns
|
;; WIDE: Two columns using row/col
|
||||||
[:row {:gap 1 :widths [30 :flex]}
|
[:row {:gap 1 :widths [30 :flex]}
|
||||||
[:col {:heights [3 :flex :flex :flex :flex]}
|
[:col {:heights [3 :flex :flex :flex :flex]}
|
||||||
(status-panel model)
|
(status-panel model)
|
||||||
(files-panel model panel-inner-height)
|
(files-panel model)
|
||||||
(branches-panel model panel-inner-height)
|
(branches-panel model)
|
||||||
(commits-panel model panel-inner-height)
|
(commits-panel model)
|
||||||
(stash-panel model panel-inner-height)]
|
(stash-panel model)]
|
||||||
[:col {:heights [:flex 4]}
|
[:col {:heights [:flex 4]}
|
||||||
(main-view-panel model)
|
(main-view-panel model)
|
||||||
(command-log-panel)]])]
|
(command-log-panel)]])]
|
||||||
(if has-message?
|
(if has-message?
|
||||||
[:col {:width width :height height :heights [1 :flex 1]}
|
[:col {:heights [1 :flex 1]}
|
||||||
(message-view model)
|
(message-view model)
|
||||||
content
|
content
|
||||||
(help-bar model)]
|
(help-bar model)]
|
||||||
[:col {:width width :height height :heights [:flex 1]}
|
[:col {:heights [:flex 1]}
|
||||||
content
|
content
|
||||||
(help-bar model)])))
|
(help-bar model)])))
|
||||||
|
|
||||||
(defn view [model {:keys [width height] :or {width 120 height 30}}]
|
(defn view [model]
|
||||||
(let [background (main-grid-view model width height)]
|
(let [background (main-grid-view model)]
|
||||||
(cond
|
(cond
|
||||||
;; Help menu modal overlay
|
;; Help menu modal overlay
|
||||||
(= (:menu-mode model) :help)
|
(= (:menu-mode model) :help)
|
||||||
@@ -922,15 +882,20 @@
|
|||||||
:else
|
:else
|
||||||
background)))
|
background)))
|
||||||
|
|
||||||
|
(defn test-view-2 [& args]
|
||||||
|
[:box "foo"])
|
||||||
|
|
||||||
;; === Main ===
|
;; === Main ===
|
||||||
|
|
||||||
|
(def temp2 get-current-diff)
|
||||||
|
|
||||||
(defn -main [& _args]
|
(defn -main [& _args]
|
||||||
(if (git/repo-root)
|
(if (git/repo-root)
|
||||||
(do
|
(do
|
||||||
(println "Starting lazygitclj...")
|
(println (str "String with initial model: " (initial-model)))
|
||||||
(tui/run {:init (initial-model)
|
(tui/run {:init (initial-model)
|
||||||
:update update-model
|
:update update-model
|
||||||
:view view})
|
:view test-view-2})
|
||||||
(println "Goodbye!"))
|
(println "Goodbye!"))
|
||||||
(do
|
(do
|
||||||
(println "Error: Not a git repository")
|
(println "Error: Not a git repository")
|
||||||
|
|||||||
+14
-4
@@ -86,13 +86,23 @@
|
|||||||
;; === Branches ===
|
;; === Branches ===
|
||||||
|
|
||||||
(defn branches
|
(defn branches
|
||||||
"Returns list of local branches."
|
"Returns list of local branches, sorted by last commit date (newest first).
|
||||||
|
This matches lazygit's default branch ordering."
|
||||||
[]
|
[]
|
||||||
(->> (sh "git" "branch" "--format=%(refname:short)")
|
(->> (sh "git" "for-each-ref"
|
||||||
|
"--sort=-committerdate"
|
||||||
|
"--format=%(refname:short)"
|
||||||
|
"refs/heads/")
|
||||||
lines))
|
lines))
|
||||||
|
|
||||||
(defn remote-branches []
|
(defn remote-branches
|
||||||
(->> (sh "git" "branch" "-r" "--format=%(refname:short)")
|
"Returns list of remote branches, sorted by last commit date (newest first).
|
||||||
|
This matches lazygit's default remote branch ordering."
|
||||||
|
[]
|
||||||
|
(->> (sh "git" "for-each-ref"
|
||||||
|
"--sort=-committerdate"
|
||||||
|
"--format=%(refname:short)"
|
||||||
|
"refs/remotes/")
|
||||||
lines))
|
lines))
|
||||||
|
|
||||||
;; === Actions ===
|
;; === Actions ===
|
||||||
|
|||||||
+116
@@ -75,6 +75,112 @@
|
|||||||
(println "Cursor test repo created at" repo-dir)
|
(println "Cursor test repo created at" repo-dir)
|
||||||
repo-dir))
|
repo-dir))
|
||||||
|
|
||||||
|
(defn setup-branch-order-test-repo
|
||||||
|
"Setup test repo with branches having different commit dates to test sorting.
|
||||||
|
Creates branches in alphabetical order but with commits in reverse order,
|
||||||
|
so date-sorted results should differ from alphabetical.
|
||||||
|
|
||||||
|
Commit dates:
|
||||||
|
- main: 2024-01-04 (newest, so it appears first)
|
||||||
|
- gamma-branch: 2024-01-03
|
||||||
|
- beta-branch: 2024-01-02
|
||||||
|
- alpha-branch: 2024-01-01 (oldest)
|
||||||
|
|
||||||
|
Expected date-sorted order: main, gamma-branch, beta-branch, alpha-branch
|
||||||
|
Alphabetical order would be: alpha-branch, beta-branch, gamma-branch, main"
|
||||||
|
([] (setup-branch-order-test-repo "/tmp/lazygitclj-e2e-branch-order"))
|
||||||
|
([repo-dir]
|
||||||
|
(fs/delete-tree repo-dir)
|
||||||
|
(fs/create-dirs repo-dir)
|
||||||
|
|
||||||
|
;; Initialize repo
|
||||||
|
(shell {:dir repo-dir} "git" "init" "-b" "main")
|
||||||
|
(shell {:dir repo-dir} "git" "config" "user.email" "test@example.com")
|
||||||
|
(shell {:dir repo-dir} "git" "config" "user.name" "Test User")
|
||||||
|
|
||||||
|
;; Initial commit on main (will be updated later with a specific date)
|
||||||
|
(spit (fs/file repo-dir "README.md") "# Branch Order Test\n")
|
||||||
|
(shell {:dir repo-dir} "git" "add" ".")
|
||||||
|
(shell {:dir repo-dir :extra-env {"GIT_COMMITTER_DATE" "2024-01-04T10:00:00"}}
|
||||||
|
"git" "commit" "-m" "Initial commit" "--date=2024-01-04T10:00:00")
|
||||||
|
|
||||||
|
;; Create branches with commits in specific order (oldest to newest):
|
||||||
|
;; alpha-branch (oldest) -> beta-branch -> gamma-branch -> main (newest)
|
||||||
|
;; This way, date-sorted order should be: main, gamma, beta, alpha
|
||||||
|
;; while alphabetical would be: alpha, beta, gamma, main
|
||||||
|
|
||||||
|
;; Create alpha-branch (oldest)
|
||||||
|
(shell {:dir repo-dir} "git" "checkout" "-b" "alpha-branch")
|
||||||
|
(spit (fs/file repo-dir "alpha.txt") "alpha content\n")
|
||||||
|
(shell {:dir repo-dir} "git" "add" ".")
|
||||||
|
;; Use GIT_COMMITTER_DATE to set specific commit times
|
||||||
|
(shell {:dir repo-dir :extra-env {"GIT_COMMITTER_DATE" "2024-01-01T10:00:00"}}
|
||||||
|
"git" "commit" "-m" "Add alpha" "--date=2024-01-01T10:00:00")
|
||||||
|
|
||||||
|
;; Create beta-branch (middle)
|
||||||
|
(shell {:dir repo-dir} "git" "checkout" "main")
|
||||||
|
(shell {:dir repo-dir} "git" "checkout" "-b" "beta-branch")
|
||||||
|
(spit (fs/file repo-dir "beta.txt") "beta content\n")
|
||||||
|
(shell {:dir repo-dir} "git" "add" ".")
|
||||||
|
(shell {:dir repo-dir :extra-env {"GIT_COMMITTER_DATE" "2024-01-02T10:00:00"}}
|
||||||
|
"git" "commit" "-m" "Add beta" "--date=2024-01-02T10:00:00")
|
||||||
|
|
||||||
|
;; Create gamma-branch
|
||||||
|
(shell {:dir repo-dir} "git" "checkout" "main")
|
||||||
|
(shell {:dir repo-dir} "git" "checkout" "-b" "gamma-branch")
|
||||||
|
(spit (fs/file repo-dir "gamma.txt") "gamma content\n")
|
||||||
|
(shell {:dir repo-dir} "git" "add" ".")
|
||||||
|
(shell {:dir repo-dir :extra-env {"GIT_COMMITTER_DATE" "2024-01-03T10:00:00"}}
|
||||||
|
"git" "commit" "-m" "Add gamma" "--date=2024-01-03T10:00:00")
|
||||||
|
|
||||||
|
;; Return to main
|
||||||
|
(shell {:dir repo-dir} "git" "checkout" "main")
|
||||||
|
|
||||||
|
(println "Branch order test repo created at" repo-dir)
|
||||||
|
(println "Expected date-sorted order: main, gamma-branch, beta-branch, alpha-branch")
|
||||||
|
repo-dir))
|
||||||
|
|
||||||
|
(defn cleanup-branch-order-test-repo
|
||||||
|
"Clean up the branch order test repo."
|
||||||
|
([] (cleanup-branch-order-test-repo "/tmp/lazygitclj-e2e-branch-order"))
|
||||||
|
([repo-dir]
|
||||||
|
(fs/delete-tree repo-dir)
|
||||||
|
(println "Cleaned up" repo-dir)))
|
||||||
|
|
||||||
|
(def test-repo-paths
|
||||||
|
"All test repo paths created by e2e tests."
|
||||||
|
["/tmp/lazygitclj-e2e-test"
|
||||||
|
"/tmp/lazygitclj-e2e-nav"
|
||||||
|
"/tmp/lazygitclj-e2e-stage"
|
||||||
|
"/tmp/lazygitclj-e2e-commit"
|
||||||
|
"/tmp/lazygitclj-e2e-commit-verify"
|
||||||
|
"/tmp/lazygitclj-e2e-branch"
|
||||||
|
"/tmp/lazygitclj-e2e-branches-tabs"
|
||||||
|
"/tmp/lazygitclj-e2e-commits-tabs"
|
||||||
|
"/tmp/lazygitclj-e2e-stash"
|
||||||
|
"/tmp/lazygitclj-e2e-stash-menu"
|
||||||
|
"/tmp/lazygitclj-e2e-help"
|
||||||
|
"/tmp/lazygitclj-e2e-reset"
|
||||||
|
"/tmp/lazygitclj-e2e-undo"
|
||||||
|
"/tmp/lazygitclj-e2e-cursor"
|
||||||
|
"/tmp/lazygitclj-e2e-branch-order"
|
||||||
|
;; Additional test repos from older/manual tests
|
||||||
|
"/tmp/lazygitclj-e2e-modal"
|
||||||
|
"/tmp/lazygitclj-e2e-modal-large"
|
||||||
|
"/tmp/lazygitclj-e2e-modal-narrow"
|
||||||
|
"/tmp/lazygitclj-e2e-modal-small"
|
||||||
|
"/tmp/lazygitclj-e2e-scroll"])
|
||||||
|
|
||||||
|
(defn cleanup-all
|
||||||
|
"Clean up all test repos."
|
||||||
|
[]
|
||||||
|
(println "Cleaning up e2e test repos...")
|
||||||
|
(doseq [path test-repo-paths]
|
||||||
|
(when (fs/exists? path)
|
||||||
|
(fs/delete-tree path)
|
||||||
|
(println " Removed" path)))
|
||||||
|
(println "Cleanup complete."))
|
||||||
|
|
||||||
(def tests
|
(def tests
|
||||||
"List of e2e test tapes."
|
"List of e2e test tapes."
|
||||||
["test/e2e/navigation.tape"
|
["test/e2e/navigation.tape"
|
||||||
@@ -85,6 +191,7 @@
|
|||||||
"test/e2e/branches.tape"
|
"test/e2e/branches.tape"
|
||||||
"test/e2e/branch-operations.tape"
|
"test/e2e/branch-operations.tape"
|
||||||
"test/e2e/branches-tabs.tape"
|
"test/e2e/branches-tabs.tape"
|
||||||
|
"test/e2e/branch-order.tape"
|
||||||
"test/e2e/commits-tabs.tape"
|
"test/e2e/commits-tabs.tape"
|
||||||
"test/e2e/stash-operations.tape"
|
"test/e2e/stash-operations.tape"
|
||||||
"test/e2e/stash-menu.tape"
|
"test/e2e/stash-menu.tape"
|
||||||
@@ -109,6 +216,9 @@
|
|||||||
(println "Running lazygitclj VHS e2e tests...")
|
(println "Running lazygitclj VHS e2e tests...")
|
||||||
(println "=================================")
|
(println "=================================")
|
||||||
|
|
||||||
|
;; Clean up any leftover test repos before starting
|
||||||
|
(cleanup-all)
|
||||||
|
|
||||||
(let [results (map run-tape tests)
|
(let [results (map run-tape tests)
|
||||||
passed (count (filter true? results))
|
passed (count (filter true? results))
|
||||||
failed (count (filter false? results))]
|
failed (count (filter false? results))]
|
||||||
@@ -118,6 +228,9 @@
|
|||||||
(println (str "Results: " passed " passed, " failed " failed"))
|
(println (str "Results: " passed " passed, " failed " failed"))
|
||||||
(println "=================================")
|
(println "=================================")
|
||||||
|
|
||||||
|
;; Clean up after tests complete
|
||||||
|
(cleanup-all)
|
||||||
|
|
||||||
(when (pos? failed)
|
(when (pos? failed)
|
||||||
(System/exit 1))))
|
(System/exit 1))))
|
||||||
|
|
||||||
@@ -127,5 +240,8 @@
|
|||||||
(case (first args)
|
(case (first args)
|
||||||
"setup" (setup-test-repo (or (second args) "/tmp/lazygitclj-e2e-test"))
|
"setup" (setup-test-repo (or (second args) "/tmp/lazygitclj-e2e-test"))
|
||||||
"setup-cursor" (setup-cursor-test-repo (or (second args) "/tmp/lazygitclj-e2e-cursor"))
|
"setup-cursor" (setup-cursor-test-repo (or (second args) "/tmp/lazygitclj-e2e-cursor"))
|
||||||
|
"setup-branch-order" (setup-branch-order-test-repo (or (second args) "/tmp/lazygitclj-e2e-branch-order"))
|
||||||
|
"cleanup-branch-order" (cleanup-branch-order-test-repo (or (second args) "/tmp/lazygitclj-e2e-branch-order"))
|
||||||
|
"cleanup" (cleanup-all)
|
||||||
"run" (run-all)
|
"run" (run-all)
|
||||||
(run-all))))
|
(run-all))))
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Set Height 600
|
|||||||
Set Framerate 10
|
Set Framerate 10
|
||||||
|
|
||||||
# Setup test repo and run lazygitclj
|
# Setup test repo and run lazygitclj
|
||||||
Type "./test/e2e/setup-test-repo.sh /tmp/lazygitclj-e2e-branch && cd /tmp/lazygitclj-e2e-branch && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
Type "cd /home/ajet/repos/lazygitclj && bb e2e:setup /tmp/lazygitclj-e2e-branch && cd /tmp/lazygitclj-e2e-branch && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
||||||
Enter
|
Enter
|
||||||
Sleep 2s
|
Sleep 2s
|
||||||
|
|
||||||
@@ -69,3 +69,8 @@ Sleep 500ms
|
|||||||
# Quit
|
# Quit
|
||||||
Type "q"
|
Type "q"
|
||||||
Sleep 1s
|
Sleep 1s
|
||||||
|
|
||||||
|
# Cleanup test repo
|
||||||
|
Type "rm -rf /tmp/lazygitclj-e2e-branch"
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# VHS E2E test for lazygitclj - Branch ordering by commit date
|
||||||
|
# Tests that branches are sorted by commit date (newest first) like lazygit
|
||||||
|
#
|
||||||
|
# Setup creates branches with specific commit dates:
|
||||||
|
# - main: 2024-01-04 (newest)
|
||||||
|
# - gamma-branch: 2024-01-03
|
||||||
|
# - beta-branch: 2024-01-02
|
||||||
|
# - alpha-branch: 2024-01-01 (oldest)
|
||||||
|
#
|
||||||
|
# Expected order in UI (by date): main, gamma-branch, beta-branch, alpha-branch
|
||||||
|
# Alphabetical order would be: alpha-branch, beta-branch, gamma-branch, main
|
||||||
|
|
||||||
|
Output test/e2e/output/branch-order.gif
|
||||||
|
Output test/e2e/output/branch-order.ascii
|
||||||
|
|
||||||
|
Require bb
|
||||||
|
|
||||||
|
Set Shell "bash"
|
||||||
|
Set FontSize 14
|
||||||
|
Set Width 1000
|
||||||
|
Set Height 600
|
||||||
|
Set Framerate 10
|
||||||
|
|
||||||
|
# Setup test repo with branches having different commit dates
|
||||||
|
Type "cd /home/ajet/repos/lazygitclj && bb e2e:setup-branch-order"
|
||||||
|
Enter
|
||||||
|
Sleep 1s
|
||||||
|
|
||||||
|
# Run lazygitclj in the test repo
|
||||||
|
Type "cd /tmp/lazygitclj-e2e-branch-order && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
||||||
|
Enter
|
||||||
|
Sleep 2s
|
||||||
|
|
||||||
|
# Switch to Branches panel (panel 3)
|
||||||
|
Type "3"
|
||||||
|
Sleep 500ms
|
||||||
|
|
||||||
|
# The branches panel should show branches in date order:
|
||||||
|
# gamma-branch (newest), beta-branch, alpha-branch, main (oldest)
|
||||||
|
# Take a moment to verify the order visually
|
||||||
|
Sleep 2s
|
||||||
|
|
||||||
|
# Navigate down through the branches to verify order
|
||||||
|
Type "j"
|
||||||
|
Sleep 300ms
|
||||||
|
Type "j"
|
||||||
|
Sleep 300ms
|
||||||
|
Type "j"
|
||||||
|
Sleep 300ms
|
||||||
|
|
||||||
|
# Navigate back up
|
||||||
|
Type "k"
|
||||||
|
Sleep 300ms
|
||||||
|
Type "k"
|
||||||
|
Sleep 300ms
|
||||||
|
Type "k"
|
||||||
|
Sleep 300ms
|
||||||
|
|
||||||
|
# Quit
|
||||||
|
Type "q"
|
||||||
|
Sleep 1s
|
||||||
|
|
||||||
|
# Cleanup test repo
|
||||||
|
Type "cd /home/ajet/repos/lazygitclj && bb e2e:cleanup-branch-order"
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
@@ -13,7 +13,7 @@ Set Height 600
|
|||||||
Set Framerate 10
|
Set Framerate 10
|
||||||
|
|
||||||
# Setup test repo with tags and run lazygitclj
|
# Setup test repo with tags and run lazygitclj
|
||||||
Type "./test/e2e/setup-test-repo.sh /tmp/lazygitclj-e2e-branches-tabs && cd /tmp/lazygitclj-e2e-branches-tabs && git tag v1.0.0 && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
Type "cd /home/ajet/repos/lazygitclj && bb e2e:setup /tmp/lazygitclj-e2e-branches-tabs && cd /tmp/lazygitclj-e2e-branches-tabs && git tag v1.0.0 && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
||||||
Enter
|
Enter
|
||||||
Sleep 2s
|
Sleep 2s
|
||||||
|
|
||||||
@@ -46,3 +46,8 @@ Sleep 500ms
|
|||||||
# Quit
|
# Quit
|
||||||
Type "q"
|
Type "q"
|
||||||
Sleep 1s
|
Sleep 1s
|
||||||
|
|
||||||
|
# Cleanup test repo
|
||||||
|
Type "rm -rf /tmp/lazygitclj-e2e-branches-tabs"
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ Set Width 1000
|
|||||||
Set Height 600
|
Set Height 600
|
||||||
Set Framerate 10
|
Set Framerate 10
|
||||||
|
|
||||||
# Setup test repo (clean working tree) and run lazygitclj
|
# Setup test repo and run lazygitclj
|
||||||
Type "cd /tmp && rm -rf lazygitclj-e2e-branch && git clone /tmp/lazygitclj-e2e-nav lazygitclj-e2e-branch 2>/dev/null && cd lazygitclj-e2e-branch && git checkout main && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
Type "cd /home/ajet/repos/lazygitclj && bb e2e:setup /tmp/lazygitclj-e2e-branch && cd /tmp/lazygitclj-e2e-branch && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
||||||
Enter
|
Enter
|
||||||
Sleep 2s
|
Sleep 2s
|
||||||
|
|
||||||
@@ -37,3 +37,8 @@ Sleep 500ms
|
|||||||
# Quit
|
# Quit
|
||||||
Type "q"
|
Type "q"
|
||||||
Sleep 1s
|
Sleep 1s
|
||||||
|
|
||||||
|
# Cleanup test repo
|
||||||
|
Type "rm -rf /tmp/lazygitclj-e2e-branch"
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
|||||||
@@ -83,3 +83,8 @@ Sleep 500ms
|
|||||||
Type "git show --stat HEAD"
|
Type "git show --stat HEAD"
|
||||||
Enter
|
Enter
|
||||||
Sleep 1s
|
Sleep 1s
|
||||||
|
|
||||||
|
# Cleanup test repo
|
||||||
|
Type "rm -rf /tmp/lazygitclj-e2e-commit-verify"
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Set Height 600
|
|||||||
Set Framerate 10
|
Set Framerate 10
|
||||||
|
|
||||||
# Setup test repo and run lazygitclj
|
# Setup test repo and run lazygitclj
|
||||||
Type "./test/e2e/setup-test-repo.sh /tmp/lazygitclj-e2e-commit && cd /tmp/lazygitclj-e2e-commit && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
Type "cd /home/ajet/repos/lazygitclj && bb e2e:setup /tmp/lazygitclj-e2e-commit && cd /tmp/lazygitclj-e2e-commit && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
||||||
Enter
|
Enter
|
||||||
Sleep 2s
|
Sleep 2s
|
||||||
|
|
||||||
@@ -48,3 +48,8 @@ Sleep 500ms
|
|||||||
# Quit
|
# Quit
|
||||||
Type "q"
|
Type "q"
|
||||||
Sleep 1s
|
Sleep 1s
|
||||||
|
|
||||||
|
# Cleanup test repo
|
||||||
|
Type "rm -rf /tmp/lazygitclj-e2e-commit"
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Set Height 600
|
|||||||
Set Framerate 10
|
Set Framerate 10
|
||||||
|
|
||||||
# Setup test repo and run lazygitclj
|
# Setup test repo and run lazygitclj
|
||||||
Type "./test/e2e/setup-test-repo.sh /tmp/lazygitclj-e2e-commits-tabs && cd /tmp/lazygitclj-e2e-commits-tabs && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
Type "cd /home/ajet/repos/lazygitclj && bb e2e:setup /tmp/lazygitclj-e2e-commits-tabs && cd /tmp/lazygitclj-e2e-commits-tabs && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
||||||
Enter
|
Enter
|
||||||
Sleep 2s
|
Sleep 2s
|
||||||
|
|
||||||
@@ -44,3 +44,8 @@ Sleep 500ms
|
|||||||
# Quit
|
# Quit
|
||||||
Type "q"
|
Type "q"
|
||||||
Sleep 1s
|
Sleep 1s
|
||||||
|
|
||||||
|
# Cleanup test repo
|
||||||
|
Type "rm -rf /tmp/lazygitclj-e2e-commits-tabs"
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ Set Width 1000
|
|||||||
Set Height 600
|
Set Height 600
|
||||||
Set Framerate 10
|
Set Framerate 10
|
||||||
|
|
||||||
# Setup test repo with many files using setup script
|
# Setup test repo with many files
|
||||||
Type "./test/e2e/setup-cursor-test.sh && cd /tmp/lazygitclj-e2e-cursor && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
Type "cd /home/ajet/repos/lazygitclj && bb e2e:setup-cursor && cd /tmp/lazygitclj-e2e-cursor && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
||||||
Enter
|
Enter
|
||||||
Sleep 3s
|
Sleep 3s
|
||||||
|
|
||||||
@@ -50,3 +50,8 @@ Sleep 500ms
|
|||||||
# Quit
|
# Quit
|
||||||
Type "q"
|
Type "q"
|
||||||
Sleep 1s
|
Sleep 1s
|
||||||
|
|
||||||
|
# Cleanup test repo
|
||||||
|
Type "rm -rf /tmp/lazygitclj-e2e-cursor"
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Set Height 600
|
|||||||
Set Framerate 10
|
Set Framerate 10
|
||||||
|
|
||||||
# Setup test repo and run lazygitclj
|
# Setup test repo and run lazygitclj
|
||||||
Type "./test/e2e/setup-test-repo.sh /tmp/lazygitclj-e2e-help && cd /tmp/lazygitclj-e2e-help && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
Type "cd /home/ajet/repos/lazygitclj && bb e2e:setup /tmp/lazygitclj-e2e-help && cd /tmp/lazygitclj-e2e-help && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
||||||
Enter
|
Enter
|
||||||
Sleep 2s
|
Sleep 2s
|
||||||
|
|
||||||
@@ -36,3 +36,8 @@ Sleep 1s
|
|||||||
# Quit the app
|
# Quit the app
|
||||||
Type "q"
|
Type "q"
|
||||||
Sleep 1s
|
Sleep 1s
|
||||||
|
|
||||||
|
# Cleanup test repo
|
||||||
|
Type "rm -rf /tmp/lazygitclj-e2e-help"
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Set Height 600
|
|||||||
Set Framerate 10
|
Set Framerate 10
|
||||||
|
|
||||||
# Setup test repo and run lazygitclj
|
# Setup test repo and run lazygitclj
|
||||||
Type "./test/e2e/setup-test-repo.sh /tmp/lazygitclj-e2e-nav && cd /tmp/lazygitclj-e2e-nav && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
Type "cd /home/ajet/repos/lazygitclj && bb e2e:setup /tmp/lazygitclj-e2e-nav && cd /tmp/lazygitclj-e2e-nav && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
||||||
Enter
|
Enter
|
||||||
Sleep 2s
|
Sleep 2s
|
||||||
|
|
||||||
@@ -77,3 +77,8 @@ Sleep 300ms
|
|||||||
# Quit with q
|
# Quit with q
|
||||||
Type "q"
|
Type "q"
|
||||||
Sleep 1s
|
Sleep 1s
|
||||||
|
|
||||||
|
# Cleanup test repo
|
||||||
|
Type "rm -rf /tmp/lazygitclj-e2e-nav"
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Set Height 600
|
|||||||
Set Framerate 10
|
Set Framerate 10
|
||||||
|
|
||||||
# Setup test repo with changes and run lazygitclj
|
# Setup test repo with changes and run lazygitclj
|
||||||
Type "./test/e2e/setup-test-repo.sh /tmp/lazygitclj-e2e-reset && cd /tmp/lazygitclj-e2e-reset && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
Type "cd /home/ajet/repos/lazygitclj && bb e2e:setup /tmp/lazygitclj-e2e-reset && cd /tmp/lazygitclj-e2e-reset && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
||||||
Enter
|
Enter
|
||||||
Sleep 2s
|
Sleep 2s
|
||||||
|
|
||||||
@@ -55,3 +55,8 @@ Sleep 1s
|
|||||||
# Quit
|
# Quit
|
||||||
Type "q"
|
Type "q"
|
||||||
Sleep 1s
|
Sleep 1s
|
||||||
|
|
||||||
|
# Cleanup test repo
|
||||||
|
Type "rm -rf /tmp/lazygitclj-e2e-reset"
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Set Height 600
|
|||||||
Set Framerate 10
|
Set Framerate 10
|
||||||
|
|
||||||
# Setup test repo and run lazygitclj
|
# Setup test repo and run lazygitclj
|
||||||
Type "./test/e2e/setup-test-repo.sh /tmp/lazygitclj-e2e-stage && cd /tmp/lazygitclj-e2e-stage && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
Type "cd /home/ajet/repos/lazygitclj && bb e2e:setup /tmp/lazygitclj-e2e-stage && cd /tmp/lazygitclj-e2e-stage && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
||||||
Enter
|
Enter
|
||||||
Sleep 2s
|
Sleep 2s
|
||||||
|
|
||||||
@@ -43,3 +43,8 @@ Sleep 500ms
|
|||||||
# Quit
|
# Quit
|
||||||
Type "q"
|
Type "q"
|
||||||
Sleep 1s
|
Sleep 1s
|
||||||
|
|
||||||
|
# Cleanup test repo
|
||||||
|
Type "rm -rf /tmp/lazygitclj-e2e-stage"
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Set Height 600
|
|||||||
Set Framerate 10
|
Set Framerate 10
|
||||||
|
|
||||||
# Setup test repo with changes and run lazygitclj
|
# Setup test repo with changes and run lazygitclj
|
||||||
Type "./test/e2e/setup-test-repo.sh /tmp/lazygitclj-e2e-stash-menu && cd /tmp/lazygitclj-e2e-stash-menu && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
Type "cd /home/ajet/repos/lazygitclj && bb e2e:setup /tmp/lazygitclj-e2e-stash-menu && cd /tmp/lazygitclj-e2e-stash-menu && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
||||||
Enter
|
Enter
|
||||||
Sleep 2s
|
Sleep 2s
|
||||||
|
|
||||||
@@ -48,3 +48,8 @@ Sleep 500ms
|
|||||||
# Quit
|
# Quit
|
||||||
Type "q"
|
Type "q"
|
||||||
Sleep 1s
|
Sleep 1s
|
||||||
|
|
||||||
|
# Cleanup test repo
|
||||||
|
Type "rm -rf /tmp/lazygitclj-e2e-stash-menu"
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Set Height 600
|
|||||||
Set Framerate 10
|
Set Framerate 10
|
||||||
|
|
||||||
# Setup test repo and run lazygitclj
|
# Setup test repo and run lazygitclj
|
||||||
Type "./test/e2e/setup-test-repo.sh /tmp/lazygitclj-e2e-stash && cd /tmp/lazygitclj-e2e-stash && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
Type "cd /home/ajet/repos/lazygitclj && bb e2e:setup /tmp/lazygitclj-e2e-stash && cd /tmp/lazygitclj-e2e-stash && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
||||||
Enter
|
Enter
|
||||||
Sleep 2s
|
Sleep 2s
|
||||||
|
|
||||||
@@ -57,3 +57,8 @@ Sleep 1s
|
|||||||
# Quit
|
# Quit
|
||||||
Type "q"
|
Type "q"
|
||||||
Sleep 1s
|
Sleep 1s
|
||||||
|
|
||||||
|
# Cleanup test repo
|
||||||
|
Type "rm -rf /tmp/lazygitclj-e2e-stash"
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Set Height 600
|
|||||||
Set Framerate 10
|
Set Framerate 10
|
||||||
|
|
||||||
# Setup test repo and make some changes
|
# Setup test repo and make some changes
|
||||||
Type "./test/e2e/setup-test-repo.sh /tmp/lazygitclj-e2e-undo && cd /tmp/lazygitclj-e2e-undo && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
Type "cd /home/ajet/repos/lazygitclj && bb e2e:setup /tmp/lazygitclj-e2e-undo && cd /tmp/lazygitclj-e2e-undo && bb --config /home/ajet/repos/lazygitclj/bb.edn start"
|
||||||
Enter
|
Enter
|
||||||
Sleep 2s
|
Sleep 2s
|
||||||
|
|
||||||
@@ -57,3 +57,8 @@ Sleep 1s
|
|||||||
# Quit
|
# Quit
|
||||||
Type "q"
|
Type "q"
|
||||||
Sleep 1s
|
Sleep 1s
|
||||||
|
|
||||||
|
# Cleanup test repo
|
||||||
|
Type "rm -rf /tmp/lazygitclj-e2e-undo"
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
(ns lazygitclj.core-test
|
(ns lazygitclj.core-test
|
||||||
"Unit tests for lazygitclj.core namespace - model and update functions"
|
"Unit tests for lazygitclj.core namespace - model and update functions"
|
||||||
(:require [clojure.test :refer [deftest testing is]]
|
(:require [clojure.test :refer [deftest testing is]]
|
||||||
[lazygitclj.core :as core]))
|
[lazygitclj.core :as core]
|
||||||
|
[tui.events :as ev]))
|
||||||
|
|
||||||
;; Helper to create key messages in the format the TUI library uses
|
;; Helper to create key events in the new format
|
||||||
(defn key-msg [k]
|
(defn key-event
|
||||||
|
([k]
|
||||||
(cond
|
(cond
|
||||||
(char? k) [:key {:char k}]
|
(char? k) {:type :key :key k}
|
||||||
(keyword? k) [:key k]
|
(keyword? k) {:type :key :key k}
|
||||||
(and (vector? k) (= :ctrl (first k))) [:key {:ctrl true :char (second k)}]
|
:else {:type :key :key k}))
|
||||||
:else [:key k]))
|
([k modifiers]
|
||||||
|
{:type :key :key k :modifiers modifiers}))
|
||||||
|
|
||||||
;; === Model Tests ===
|
;; === Model Tests ===
|
||||||
|
|
||||||
@@ -108,178 +111,207 @@
|
|||||||
;; === Update Tests ===
|
;; === Update Tests ===
|
||||||
|
|
||||||
(deftest test-update-model-quit
|
(deftest test-update-model-quit
|
||||||
(testing "q returns quit command"
|
(testing "q returns quit event"
|
||||||
(let [[model cmd] (core/update-model {} (key-msg \q))]
|
(let [{:keys [events]} (core/update-model {:model {} :event (key-event \q)})]
|
||||||
(is (= [:quit] cmd))))
|
(is (= [{:type :quit}] events))))
|
||||||
|
|
||||||
(testing "ctrl-c returns quit command"
|
(testing "ctrl-c returns quit event"
|
||||||
(let [[model cmd] (core/update-model {} [:key {:ctrl true :char \c}])]
|
(let [{:keys [events]} (core/update-model {:model {} :event (key-event \c #{:ctrl})})]
|
||||||
(is (= [:quit] cmd)))))
|
(is (= [{:type :quit}] events)))))
|
||||||
|
|
||||||
(deftest test-update-model-panel-switch
|
(deftest test-update-model-panel-switch
|
||||||
(testing "number keys switch panels (2-5 matching lazygit)"
|
(testing "number keys switch panels (2-5 matching lazygit)"
|
||||||
(let [[model _] (core/update-model {:panel :commits :cursor 0
|
(let [{:keys [model]} (core/update-model {:model {:panel :commits :cursor 0
|
||||||
:staged [] :unstaged []} (key-msg \2))]
|
:staged [] :unstaged []}
|
||||||
|
:event (key-event \2)})]
|
||||||
(is (= :files (:panel model))))
|
(is (= :files (:panel model))))
|
||||||
|
|
||||||
(let [[model _] (core/update-model {:panel :files :cursor 0
|
(let [{:keys [model]} (core/update-model {:model {:panel :files :cursor 0
|
||||||
:branches-tab :local
|
:branches-tab :local
|
||||||
:branches [] :remote-branches []
|
:branches [] :remote-branches []
|
||||||
:tags []} (key-msg \3))]
|
:tags []}
|
||||||
|
:event (key-event \3)})]
|
||||||
(is (= :branches (:panel model))))
|
(is (= :branches (:panel model))))
|
||||||
|
|
||||||
(let [[model _] (core/update-model {:panel :files :cursor 0
|
(let [{:keys [model]} (core/update-model {:model {:panel :files :cursor 0
|
||||||
:commits-tab :commits
|
:commits-tab :commits
|
||||||
:commits [] :reflog []} (key-msg \4))]
|
:commits [] :reflog []}
|
||||||
|
:event (key-event \4)})]
|
||||||
(is (= :commits (:panel model))))
|
(is (= :commits (:panel model))))
|
||||||
|
|
||||||
(let [[model _] (core/update-model {:panel :files :cursor 0
|
(let [{:keys [model]} (core/update-model {:model {:panel :files :cursor 0
|
||||||
:stashes []} (key-msg \5))]
|
:stashes []}
|
||||||
|
:event (key-event \5)})]
|
||||||
(is (= :stash (:panel model)))))
|
(is (= :stash (:panel model)))))
|
||||||
|
|
||||||
(testing "l key cycles panels right (files → branches → commits → stash → files)"
|
(testing "l key cycles panels right (files → branches → commits → stash → files)"
|
||||||
(let [[model _] (core/update-model {:panel :files :cursor 0
|
(let [{:keys [model]} (core/update-model {:model {:panel :files :cursor 0
|
||||||
:branches-tab :local
|
:branches-tab :local
|
||||||
:branches [] :remote-branches []
|
:branches [] :remote-branches []
|
||||||
:tags []} (key-msg \l))]
|
:tags []}
|
||||||
|
:event (key-event \l)})]
|
||||||
(is (= :branches (:panel model))))
|
(is (= :branches (:panel model))))
|
||||||
|
|
||||||
(let [[model _] (core/update-model {:panel :branches :cursor 0
|
(let [{:keys [model]} (core/update-model {:model {:panel :branches :cursor 0
|
||||||
:commits-tab :commits
|
:commits-tab :commits
|
||||||
:commits [] :reflog []} (key-msg \l))]
|
:commits [] :reflog []}
|
||||||
|
:event (key-event \l)})]
|
||||||
(is (= :commits (:panel model))))
|
(is (= :commits (:panel model))))
|
||||||
|
|
||||||
(let [[model _] (core/update-model {:panel :commits :cursor 0
|
(let [{:keys [model]} (core/update-model {:model {:panel :commits :cursor 0
|
||||||
:stashes []} (key-msg \l))]
|
:stashes []}
|
||||||
|
:event (key-event \l)})]
|
||||||
(is (= :stash (:panel model))))
|
(is (= :stash (:panel model))))
|
||||||
|
|
||||||
(let [[model _] (core/update-model {:panel :stash :cursor 0
|
(let [{:keys [model]} (core/update-model {:model {:panel :stash :cursor 0
|
||||||
:staged [] :unstaged []} (key-msg \l))]
|
:staged [] :unstaged []}
|
||||||
|
:event (key-event \l)})]
|
||||||
(is (= :files (:panel model))))))
|
(is (= :files (:panel model))))))
|
||||||
|
|
||||||
(deftest test-update-model-cursor-movement
|
(deftest test-update-model-cursor-movement
|
||||||
(testing "j moves cursor down"
|
(testing "j moves cursor down"
|
||||||
(let [[model _] (core/update-model {:panel :files :cursor 0
|
(let [{:keys [model]} (core/update-model {:model {:panel :files :cursor 0
|
||||||
:staged [{:path "a"} {:path "b"}]
|
:staged [{:path "a"} {:path "b"}]
|
||||||
:unstaged []} (key-msg \j))]
|
:unstaged []}
|
||||||
|
:event (key-event \j)})]
|
||||||
(is (= 1 (:cursor model)))))
|
(is (= 1 (:cursor model)))))
|
||||||
|
|
||||||
(testing "k moves cursor up"
|
(testing "k moves cursor up"
|
||||||
(let [[model _] (core/update-model {:panel :files :cursor 1
|
(let [{:keys [model]} (core/update-model {:model {:panel :files :cursor 1
|
||||||
:staged [{:path "a"} {:path "b"}]
|
:staged [{:path "a"} {:path "b"}]
|
||||||
:unstaged []} (key-msg \k))]
|
:unstaged []}
|
||||||
|
:event (key-event \k)})]
|
||||||
(is (= 0 (:cursor model)))))
|
(is (= 0 (:cursor model)))))
|
||||||
|
|
||||||
(testing "down arrow moves cursor down"
|
(testing "down arrow moves cursor down"
|
||||||
(let [[model _] (core/update-model {:panel :files :cursor 0
|
(let [{:keys [model]} (core/update-model {:model {:panel :files :cursor 0
|
||||||
:staged [{:path "a"} {:path "b"}]
|
:staged [{:path "a"} {:path "b"}]
|
||||||
:unstaged []} (key-msg :down))]
|
:unstaged []}
|
||||||
|
:event (key-event :down)})]
|
||||||
(is (= 1 (:cursor model)))))
|
(is (= 1 (:cursor model)))))
|
||||||
|
|
||||||
(testing "up arrow moves cursor up"
|
(testing "up arrow moves cursor up"
|
||||||
(let [[model _] (core/update-model {:panel :files :cursor 1
|
(let [{:keys [model]} (core/update-model {:model {:panel :files :cursor 1
|
||||||
:staged [{:path "a"} {:path "b"}]
|
:staged [{:path "a"} {:path "b"}]
|
||||||
:unstaged []} (key-msg :up))]
|
:unstaged []}
|
||||||
|
:event (key-event :up)})]
|
||||||
(is (= 0 (:cursor model))))))
|
(is (= 0 (:cursor model))))))
|
||||||
|
|
||||||
(deftest test-update-model-help-menu
|
(deftest test-update-model-help-menu
|
||||||
(testing "? opens help menu"
|
(testing "? opens help menu"
|
||||||
(let [[model _] (core/update-model {:menu-mode nil} (key-msg \?))]
|
(let [{:keys [model]} (core/update-model {:model {:menu-mode nil}
|
||||||
|
:event (key-event \?)})]
|
||||||
(is (= :help (:menu-mode model))))))
|
(is (= :help (:menu-mode model))))))
|
||||||
|
|
||||||
(deftest test-update-model-reset-menu
|
(deftest test-update-model-reset-menu
|
||||||
(testing "D opens reset options menu"
|
(testing "D opens reset options menu"
|
||||||
(let [[model _] (core/update-model {:menu-mode nil} (key-msg \D))]
|
(let [{:keys [model]} (core/update-model {:model {:menu-mode nil}
|
||||||
|
:event (key-event \d #{:shift})})]
|
||||||
(is (= :reset-options (:menu-mode model))))))
|
(is (= :reset-options (:menu-mode model))))))
|
||||||
|
|
||||||
(deftest test-update-model-input-mode
|
(deftest test-update-model-input-mode
|
||||||
(testing "c in files panel opens commit input when staged files exist"
|
(testing "c in files panel opens commit input when staged files exist"
|
||||||
(let [[model _] (core/update-model {:panel :files
|
(let [{:keys [model]} (core/update-model {:model {:panel :files
|
||||||
:staged [{:path "a.txt"}]
|
:staged [{:path "a.txt"}]
|
||||||
:unstaged []} (key-msg \c))]
|
:unstaged []}
|
||||||
|
:event (key-event \c)})]
|
||||||
(is (= :commit (:input-mode model)))))
|
(is (= :commit (:input-mode model)))))
|
||||||
|
|
||||||
(testing "c in files panel shows message when no staged files"
|
(testing "c in files panel shows message when no staged files"
|
||||||
(let [[model _] (core/update-model {:panel :files
|
(let [{:keys [model]} (core/update-model {:model {:panel :files
|
||||||
:staged []
|
:staged []
|
||||||
:unstaged []} (key-msg \c))]
|
:unstaged []}
|
||||||
|
:event (key-event \c)})]
|
||||||
(is (= "Nothing staged to commit" (:message model))))))
|
(is (= "Nothing staged to commit" (:message model))))))
|
||||||
|
|
||||||
(deftest test-update-input-mode
|
(deftest test-update-input-mode
|
||||||
(testing "escape cancels input mode"
|
(testing "escape cancels input mode"
|
||||||
(let [[model _] (core/update-input-mode {:input-mode :commit
|
(let [{:keys [model]} (core/update-input-mode {:model {:input-mode :commit
|
||||||
:input-buffer "test"
|
:input-buffer "test"
|
||||||
:input-context nil} (key-msg :escape))]
|
:input-context nil}
|
||||||
|
:event (key-event :escape)})]
|
||||||
(is (nil? (:input-mode model)))
|
(is (nil? (:input-mode model)))
|
||||||
(is (= "" (:input-buffer model)))))
|
(is (= "" (:input-buffer model)))))
|
||||||
|
|
||||||
(testing "backspace removes last character"
|
(testing "backspace removes last character"
|
||||||
(let [[model _] (core/update-input-mode {:input-mode :commit
|
(let [{:keys [model]} (core/update-input-mode {:model {:input-mode :commit
|
||||||
:input-buffer "abc"
|
:input-buffer "abc"
|
||||||
:input-context nil} (key-msg :backspace))]
|
:input-context nil}
|
||||||
|
:event (key-event :backspace)})]
|
||||||
(is (= "ab" (:input-buffer model))))))
|
(is (= "ab" (:input-buffer model))))))
|
||||||
|
|
||||||
(deftest test-update-commits-tabs
|
(deftest test-update-commits-tabs
|
||||||
(testing "] switches to reflog tab in commits panel"
|
(testing "] switches to reflog tab in commits panel"
|
||||||
(let [[model _] (core/update-model {:panel :commits
|
(let [{:keys [model]} (core/update-model {:model {:panel :commits
|
||||||
:commits-tab :commits
|
:commits-tab :commits
|
||||||
:cursor 0
|
:cursor 0
|
||||||
:commits []
|
:commits []
|
||||||
:reflog []} (key-msg \]))]
|
:reflog []}
|
||||||
|
:event (key-event \])})]
|
||||||
(is (= :reflog (:commits-tab model)))))
|
(is (= :reflog (:commits-tab model)))))
|
||||||
|
|
||||||
(testing "[ switches to commits tab in commits panel"
|
(testing "[ switches to commits tab in commits panel"
|
||||||
(let [[model _] (core/update-model {:panel :commits
|
(let [{:keys [model]} (core/update-model {:model {:panel :commits
|
||||||
:commits-tab :reflog
|
:commits-tab :reflog
|
||||||
:cursor 0
|
:cursor 0
|
||||||
:commits []
|
:commits []
|
||||||
:reflog []} (key-msg \[))]
|
:reflog []}
|
||||||
|
:event (key-event \[)})]
|
||||||
(is (= :commits (:commits-tab model))))))
|
(is (= :commits (:commits-tab model))))))
|
||||||
|
|
||||||
(deftest test-update-branches-tabs
|
(deftest test-update-branches-tabs
|
||||||
(testing "] cycles branches tabs forward"
|
(testing "] cycles branches tabs forward"
|
||||||
(let [[model _] (core/update-branches {:branches-tab :local
|
(let [{:keys [model]} (core/update-branches {:model {:branches-tab :local
|
||||||
:cursor 0
|
:cursor 0
|
||||||
:branches []} (key-msg \]))]
|
:branches []}
|
||||||
|
:event (key-event \])})]
|
||||||
(is (= :remotes (:branches-tab model))))
|
(is (= :remotes (:branches-tab model))))
|
||||||
|
|
||||||
(let [[model _] (core/update-branches {:branches-tab :remotes
|
(let [{:keys [model]} (core/update-branches {:model {:branches-tab :remotes
|
||||||
:cursor 0
|
:cursor 0
|
||||||
:remote-branches []} (key-msg \]))]
|
:remote-branches []}
|
||||||
|
:event (key-event \])})]
|
||||||
(is (= :tags (:branches-tab model))))
|
(is (= :tags (:branches-tab model))))
|
||||||
|
|
||||||
(let [[model _] (core/update-branches {:branches-tab :tags
|
(let [{:keys [model]} (core/update-branches {:model {:branches-tab :tags
|
||||||
:cursor 0
|
:cursor 0
|
||||||
:tags []} (key-msg \]))]
|
:tags []}
|
||||||
|
:event (key-event \])})]
|
||||||
(is (= :local (:branches-tab model)))))
|
(is (= :local (:branches-tab model)))))
|
||||||
|
|
||||||
(testing "[ cycles branches tabs backward"
|
(testing "[ cycles branches tabs backward"
|
||||||
(let [[model _] (core/update-branches {:branches-tab :local
|
(let [{:keys [model]} (core/update-branches {:model {:branches-tab :local
|
||||||
:cursor 0
|
:cursor 0
|
||||||
:tags []} (key-msg \[))]
|
:tags []}
|
||||||
|
:event (key-event \[)})]
|
||||||
(is (= :tags (:branches-tab model))))))
|
(is (= :tags (:branches-tab model))))))
|
||||||
|
|
||||||
(deftest test-update-stash-menu
|
(deftest test-update-stash-menu
|
||||||
(testing "escape closes stash menu"
|
(testing "escape closes stash menu"
|
||||||
(let [[model _] (core/update-stash-menu {:menu-mode :stash-options} (key-msg :escape))]
|
(let [{:keys [model]} (core/update-stash-menu {:model {:menu-mode :stash-options}
|
||||||
|
:event (key-event :escape)})]
|
||||||
(is (nil? (:menu-mode model))))))
|
(is (nil? (:menu-mode model))))))
|
||||||
|
|
||||||
(deftest test-update-reset-menu
|
(deftest test-update-reset-menu
|
||||||
(testing "escape closes reset menu"
|
(testing "escape closes reset menu"
|
||||||
(let [[model _] (core/update-reset-menu {:menu-mode :reset-options} (key-msg :escape))]
|
(let [{:keys [model]} (core/update-reset-menu {:model {:menu-mode :reset-options}
|
||||||
|
:event (key-event :escape)})]
|
||||||
(is (nil? (:menu-mode model))))))
|
(is (nil? (:menu-mode model))))))
|
||||||
|
|
||||||
(deftest test-update-help
|
(deftest test-update-help
|
||||||
(testing "escape closes help"
|
(testing "escape closes help"
|
||||||
(let [[model _] (core/update-help {:menu-mode :help} (key-msg :escape))]
|
(let [{:keys [model]} (core/update-help {:model {:menu-mode :help}
|
||||||
|
:event (key-event :escape)})]
|
||||||
(is (nil? (:menu-mode model)))))
|
(is (nil? (:menu-mode model)))))
|
||||||
|
|
||||||
(testing "q closes help"
|
(testing "q closes help"
|
||||||
(let [[model _] (core/update-help {:menu-mode :help} (key-msg \q))]
|
(let [{:keys [model]} (core/update-help {:model {:menu-mode :help}
|
||||||
|
:event (key-event \q)})]
|
||||||
(is (nil? (:menu-mode model)))))
|
(is (nil? (:menu-mode model)))))
|
||||||
|
|
||||||
(testing "? closes help"
|
(testing "? closes help"
|
||||||
(let [[model _] (core/update-help {:menu-mode :help} (key-msg \?))]
|
(let [{:keys [model]} (core/update-help {:model {:menu-mode :help}
|
||||||
|
:event (key-event \?)})]
|
||||||
(is (nil? (:menu-mode model))))))
|
(is (nil? (:menu-mode model))))))
|
||||||
|
|
||||||
;; Run tests when executed directly
|
;; Run tests when executed directly
|
||||||
|
|||||||
Reference in New Issue
Block a user