mirror of
https://github.com/logseq/logseq.git
synced 2026-05-28 22:49:53 +00:00
PR summary
This commit is contained in:
@@ -252,6 +252,7 @@
|
||||
[:map
|
||||
[:title {:optional true} :string]
|
||||
[:body {:optional true} :string]
|
||||
[:commit-message {:optional true} :string]
|
||||
[:head-branch {:optional true} :string]
|
||||
[:base-branch {:optional true} :string]
|
||||
[:create-pr {:optional true} :boolean]
|
||||
|
||||
@@ -537,14 +537,25 @@
|
||||
[^js self current-session body user-id repo-url head-branch force? create-pr?]
|
||||
(p/let [pr-token (source-control/pr-token (.-env self))
|
||||
requested-base-branch (source-control/sanitize-branch-name (:base-branch body))
|
||||
default-base (source-control/sanitize-branch-name (default-base-branch (.-env self)))
|
||||
detected-base-branch (when (and (nil? requested-base-branch)
|
||||
(string? pr-token))
|
||||
(source-control/<default-branch! (.-env self)
|
||||
pr-token
|
||||
repo-url))
|
||||
detected-base-branch (source-control/sanitize-branch-name detected-base-branch)
|
||||
base-branch (or requested-base-branch
|
||||
detected-base-branch
|
||||
(default-base-branch (.-env self)))]
|
||||
default-base)
|
||||
base-branch (if (= base-branch head-branch)
|
||||
(or (some (fn [candidate]
|
||||
(let [candidate (source-control/sanitize-branch-name candidate)]
|
||||
(when (and (string? candidate)
|
||||
(not= candidate head-branch))
|
||||
candidate)))
|
||||
[default-base "main" "master"])
|
||||
base-branch)
|
||||
base-branch)]
|
||||
(cond
|
||||
(false? create-pr?)
|
||||
(http/json-response :sessions/pr
|
||||
@@ -587,6 +598,7 @@
|
||||
[^js self current-session body user-id repo-url runtime force? create-pr? push-branch-fn]
|
||||
(let [provider (runtime-provider/resolve-provider (.-env self) runtime)
|
||||
push-token (source-control/push-token (.-env self))
|
||||
commit-message (some-> (:commit-message body) str string/trim not-empty)
|
||||
head-branch (source-control/resolve-head-branch (:head-branch body)
|
||||
(generated-head-branch (:id current-session)))]
|
||||
(if-not (string? head-branch)
|
||||
@@ -597,11 +609,12 @@
|
||||
:force force?})
|
||||
push-result (push-branch-fn provider
|
||||
runtime
|
||||
{:session-id (:id current-session)
|
||||
:repo-url repo-url
|
||||
:head-branch head-branch
|
||||
:force force?
|
||||
:push-token push-token})
|
||||
(cond-> {:session-id (:id current-session)
|
||||
:repo-url repo-url
|
||||
:head-branch head-branch
|
||||
:force force?
|
||||
:push-token push-token}
|
||||
commit-message (assoc :commit-message commit-message)))
|
||||
_ (<append-publish-event! self "git.push.succeeded"
|
||||
{:by user-id
|
||||
:head-branch head-branch
|
||||
|
||||
@@ -430,7 +430,10 @@
|
||||
(defn- push-command
|
||||
[{:keys [repo-dir remote-url head-branch] :as opts}]
|
||||
(let [force? (true? (:force opts))
|
||||
commit-message (or (some-> (:commit-message opts) string/trim not-empty)
|
||||
"chore(agent): update files")
|
||||
branch (escape-shell-single head-branch)
|
||||
commit-message (escape-shell-single commit-message)
|
||||
remote (escape-shell-single remote-url)]
|
||||
(str "set -e; "
|
||||
"cd '" (escape-shell-single repo-dir) "'; "
|
||||
@@ -439,7 +442,7 @@
|
||||
"git add -A; "
|
||||
"if git diff --cached --quiet; then true; else "
|
||||
"git -c user.name='Logseq Agent' -c user.email='agent@logseq.local' "
|
||||
"commit -m 'chore(agent): update files'; "
|
||||
"commit -m '" commit-message "'; "
|
||||
"fi; "
|
||||
"git push '" remote "' HEAD:refs/heads/" branch
|
||||
(when force? " --force"))))
|
||||
@@ -956,6 +959,7 @@
|
||||
(let [script (push-command {:repo-dir repo-dir
|
||||
:remote-url remote-url
|
||||
:head-branch head-branch
|
||||
:commit-message (:commit-message opts)
|
||||
:force force?})]
|
||||
|
||||
(-> (p/let [result (sprites-exec-post! env name ["bash" "-lc" script])
|
||||
@@ -1119,6 +1123,7 @@
|
||||
script (push-command {:repo-dir repo-dir
|
||||
:remote-url remote-url
|
||||
:head-branch head-branch
|
||||
:commit-message (:commit-message opts)
|
||||
:force force?})]
|
||||
(-> (p/let [result (<cloudflare-exec! sandbox script)
|
||||
{:keys [stdout stderr exit-code success]} (cloudflare-exec-output result)]
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
(:require [cljs.test :refer [async deftest is testing]]
|
||||
[clojure.string :as string]
|
||||
[logseq.db-sync.worker.agent.do :as agent-do]
|
||||
[logseq.db-sync.worker.agent.runtime-provider :as runtime-provider]))
|
||||
[logseq.db-sync.worker.agent.runtime-provider :as runtime-provider]
|
||||
[logseq.db-sync.worker.agent.source-control :as source-control]))
|
||||
|
||||
(defn- make-agent-storage []
|
||||
(let [data (js/Map.)]
|
||||
@@ -618,7 +619,8 @@
|
||||
(deftest pr-endpoint-push-only-success-test
|
||||
(testing "session publish endpoint supports push-only flow"
|
||||
(async done
|
||||
(let [env #js {"AGENT_RUNTIME_PROVIDER" "local-dev"}
|
||||
(let [push-calls (atom [])
|
||||
env #js {"AGENT_RUNTIME_PROVIDER" "local-dev"}
|
||||
self (make-self env)
|
||||
headers {"content-type" "application/json"
|
||||
"x-user-id" "user-1"}]
|
||||
@@ -635,6 +637,7 @@
|
||||
(.then (fn [_]
|
||||
(with-redefs [runtime-provider/<push-branch!
|
||||
(fn [_provider _runtime opts]
|
||||
(swap! push-calls conj opts)
|
||||
(js/Promise.resolve
|
||||
{:head-branch (:head-branch opts)
|
||||
:repo-url (:repo-url opts)
|
||||
@@ -645,6 +648,7 @@
|
||||
"POST"
|
||||
{:create-pr false
|
||||
:head-branch "m14/push-only"
|
||||
:commit-message "feat: summarize PR changes"
|
||||
:force true}
|
||||
headers)))))
|
||||
(.then (fn [resp]
|
||||
@@ -654,6 +658,57 @@
|
||||
(is (= "pushed" (:status body)))
|
||||
(is (= "m14/push-only" (:head-branch body)))
|
||||
(is (= true (:force body)))
|
||||
(is (= "feat: summarize PR changes"
|
||||
(:commit-message (first @push-calls))))
|
||||
(done)))))
|
||||
(.catch (fn [error]
|
||||
(is false (str "unexpected error: " error))
|
||||
(done))))))))
|
||||
|
||||
(deftest pr-endpoint-fallbacks-base-branch-when-equal-to-head-test
|
||||
(testing "session publish endpoint avoids head==base branch when resolving base branch"
|
||||
(async done
|
||||
(let [env #js {"AGENT_RUNTIME_PROVIDER" "local-dev"}
|
||||
self (make-self env)
|
||||
headers {"content-type" "application/json"
|
||||
"x-user-id" "user-1"}
|
||||
same-branch "logseq-agent/same-branch"]
|
||||
(-> (.put (.-storage self)
|
||||
"session"
|
||||
(clj->js {:id "sess-pr-branch-fallback"
|
||||
:status "running"
|
||||
:task {:project {:repo-url "https://github.com/example/repo"}}
|
||||
:runtime {:provider "local-dev"
|
||||
:session-id "sess-pr-branch-fallback"}
|
||||
:audit {}
|
||||
:created-at 0
|
||||
:updated-at 0}))
|
||||
(.then (fn [_]
|
||||
(with-redefs [runtime-provider/<push-branch!
|
||||
(fn [_provider _runtime opts]
|
||||
(js/Promise.resolve
|
||||
{:head-branch (:head-branch opts)
|
||||
:repo-url (:repo-url opts)
|
||||
:force (:force opts)
|
||||
:remote "origin"}))
|
||||
source-control/pr-token
|
||||
(fn [_env] "token-1")
|
||||
source-control/<default-branch!
|
||||
(fn [_env _token _repo-url]
|
||||
(js/Promise.resolve same-branch))]
|
||||
(agent-do/handle-fetch self
|
||||
(json-request "http://db-sync.local/__session__/pr"
|
||||
"POST"
|
||||
{:create-pr false
|
||||
:head-branch same-branch}
|
||||
headers)))))
|
||||
(.then (fn [resp]
|
||||
(is (= 200 (.-status resp)))
|
||||
(.then (<json resp)
|
||||
(fn [body]
|
||||
(is (= "pushed" (:status body)))
|
||||
(is (= same-branch (:head-branch body)))
|
||||
(is (= "main" (:base-branch body)))
|
||||
(done)))))
|
||||
(.catch (fn [error]
|
||||
(is false (str "unexpected error: " error))
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
(testing "accepts sessions/pr request payload"
|
||||
(let [body {:title "feat: add m14 publish"
|
||||
:body "This change enables push + optional PR."
|
||||
:commit-message "feat: summarize m14 publish changes"
|
||||
:head-branch "m14/publish"
|
||||
:base-branch "main"
|
||||
:create-pr true
|
||||
@@ -64,4 +65,5 @@
|
||||
(testing "rejects invalid sessions/pr payload"
|
||||
(is (nil? (http/coerce-http-request :sessions/pr {:create-pr "yes"})))
|
||||
(is (nil? (http/coerce-http-request :sessions/pr {:force "no"})))
|
||||
(is (nil? (http/coerce-http-request :sessions/pr {:commit-message 100})))
|
||||
(is (nil? (http/coerce-http-request :sessions/pr {:head-branch 42})))))
|
||||
|
||||
@@ -244,6 +244,7 @@
|
||||
{:session-id "sess-push"
|
||||
:repo-url "https://github.com/example/repo"
|
||||
:head-branch "feature/m14"
|
||||
:commit-message "feat: summarize PR updates"
|
||||
:force true
|
||||
:push-token "token-1"})
|
||||
(.then (fn [result]
|
||||
@@ -253,6 +254,7 @@
|
||||
(is (= true (:force result)))
|
||||
(is (string/includes? (:script @captured) "git push"))
|
||||
(is (string/includes? (:script @captured) "feature/m14"))
|
||||
(is (string/includes? (:script @captured) "commit -m 'feat: summarize PR updates'"))
|
||||
(is (string/includes? (:script @captured) "x-access-token:token-1"))
|
||||
(done)))
|
||||
(.catch (fn [error]
|
||||
|
||||
@@ -352,6 +352,24 @@
|
||||
[message]
|
||||
(count (pr-str (select-keys message [:id :role :parts]))))
|
||||
|
||||
(def ^:private commit-message-max-len 120)
|
||||
|
||||
(defn- latest-assistant-summary
|
||||
[messages]
|
||||
(some->> messages
|
||||
reverse
|
||||
(keep (fn [message]
|
||||
(when (= "assistant" (normalize-role (:role message)))
|
||||
(ui-message-text message))))
|
||||
first
|
||||
normalized-text))
|
||||
|
||||
(defn- summary->commit-message
|
||||
[summary]
|
||||
(when-let [summary (normalized-text summary)]
|
||||
(let [single-line (string/replace summary #"\s+" " ")]
|
||||
(subs single-line 0 (min commit-message-max-len (count single-line))))))
|
||||
|
||||
(defn- session-messages-need-sync?
|
||||
[session-messages ui-messages]
|
||||
(let [ui-by-id (into {} (map (juxt :id identity) ui-messages))]
|
||||
@@ -418,9 +436,14 @@
|
||||
publish! (fn [create-pr?]
|
||||
(when (and base session-id (not publish-disabled?))
|
||||
(set-publish-mode! (if create-pr? :pr :push))
|
||||
(-> (agent-handler/<publish-session! block {:create-pr? create-pr?})
|
||||
(p/catch (fn [_] nil))
|
||||
(p/finally (fn [] (set-publish-mode! nil))))))]
|
||||
(let [summary (latest-assistant-summary session-chat-messages)
|
||||
commit-message (summary->commit-message summary)
|
||||
opts (cond-> {:create-pr? create-pr?}
|
||||
(string? summary) (assoc :body summary)
|
||||
(string? commit-message) (assoc :commit-message commit-message))]
|
||||
(-> (agent-handler/<publish-session! block opts)
|
||||
(p/catch (fn [_] nil))
|
||||
(p/finally (fn [] (set-publish-mode! nil)))))))]
|
||||
(hooks/use-effect!
|
||||
(fn []
|
||||
(when (agent-handler/task-ready? block)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
(:require [clojure.string :as string]
|
||||
[frontend.db :as db]
|
||||
[frontend.handler.db-based.sync :as db-sync]
|
||||
[frontend.handler.editor :as editor-handler]
|
||||
[frontend.handler.notification :as notification]
|
||||
[frontend.handler.property :as property-handler]
|
||||
[frontend.handler.property.util :as pu]
|
||||
@@ -403,14 +404,28 @@
|
||||
resp))))))
|
||||
|
||||
(defn- publish-request-body
|
||||
[{:keys [title body head-branch base-branch create-pr? force?]}]
|
||||
[{:keys [title body commit-message head-branch base-branch create-pr? force?]}]
|
||||
(cond-> {:create-pr (if (nil? create-pr?) true (true? create-pr?))
|
||||
:force (true? force?)}
|
||||
(string? (blank->nil title)) (assoc :title (blank->nil title))
|
||||
(string? (blank->nil body)) (assoc :body (blank->nil body))
|
||||
(string? (blank->nil commit-message)) (assoc :commit-message (blank->nil commit-message))
|
||||
(string? (blank->nil head-branch)) (assoc :head-branch (blank->nil head-branch))
|
||||
(string? (blank->nil base-branch)) (assoc :base-branch (blank->nil base-branch))))
|
||||
|
||||
(defn- maybe-insert-pr-sibling-blocks!
|
||||
[block-uuid resp summary]
|
||||
(when-let [url (or (blank->nil (:pr-url resp))
|
||||
(blank->nil (:manual-pr-url resp)))]
|
||||
(p/let [block (editor-handler/api-insert-new-block! (str "PR URL: " url)
|
||||
{:block-uuid block-uuid
|
||||
:sibling? false})]
|
||||
|
||||
(when-let [summary (blank->nil summary)]
|
||||
(editor-handler/api-insert-new-block! (str "PR Summary: " summary)
|
||||
{:block-uuid (:block/uuid block)
|
||||
:sibling? true})))))
|
||||
|
||||
(defn- publish-status-message
|
||||
[resp]
|
||||
(or (:message resp)
|
||||
@@ -466,6 +481,7 @@
|
||||
(p/then (fn [resp]
|
||||
(update-session-state! block-uuid {:last-publish resp
|
||||
:last-publish-at (util/time-ms)})
|
||||
(maybe-insert-pr-sibling-blocks! block-uuid resp (:body raw-body))
|
||||
(notification/show! (publish-status-message resp)
|
||||
(if (= "manual-pr-required" (:status resp))
|
||||
:warning
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
[clojure.string :as string]
|
||||
[frontend.handler.agent :as agent-handler]
|
||||
[frontend.handler.db-based.sync :as db-sync]
|
||||
[frontend.handler.editor :as editor-handler]
|
||||
[frontend.handler.notification :as notification]
|
||||
[frontend.handler.user :as user-handler]
|
||||
[frontend.state :as state]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(deftest start-session-sends-initial-message-test
|
||||
@@ -86,3 +89,129 @@
|
||||
(p/catch (fn [e]
|
||||
(is false (str e))
|
||||
(done)))))))
|
||||
|
||||
(deftest publish-session-pr-created-inserts-url-and-summary-sibling-blocks-test
|
||||
(async done
|
||||
(let [insert-calls (atom [])
|
||||
fetch-calls (atom [])
|
||||
block {:block/uuid #uuid "cccccccc-cccc-cccc-cccc-cccccccccccc"}
|
||||
prev-state @state/state]
|
||||
(swap! state/state assoc :agent/sessions {(str (:block/uuid block)) {:session-id "sess-pr-1"}})
|
||||
(-> (p/with-redefs [db-sync/http-base (fn [] "http://base")
|
||||
db-sync/fetch-json (fn [url opts _]
|
||||
(swap! fetch-calls conj {:url url :opts opts})
|
||||
(if (string/includes? url "/events")
|
||||
(p/resolved {:events []})
|
||||
(p/resolved {:status "pr-created"
|
||||
:pr-url "https://github.com/example/repo/pull/123"})))
|
||||
user-handler/task--ensure-id&access-token (fn [resolve _reject]
|
||||
(resolve true))
|
||||
agent-handler/task-ready? (fn [_] true)
|
||||
notification/show! (fn [& _] nil)
|
||||
editor-handler/api-insert-new-block! (fn [content opts]
|
||||
(swap! insert-calls conj {:content content :opts opts}))]
|
||||
(p/let [_ (agent-handler/<publish-session! block {:create-pr? true
|
||||
:body "Agent summary for PR"})
|
||||
[first-call] @fetch-calls
|
||||
_ (reset! state/state prev-state)]
|
||||
(is (= "http://base/sessions/sess-pr-1/pr" (:url first-call)))
|
||||
(is (= 2 (count @insert-calls)))
|
||||
(is (= "PR URL: https://github.com/example/repo/pull/123"
|
||||
(:content (first @insert-calls))))
|
||||
(is (= "PR Summary: Agent summary for PR"
|
||||
(:content (second @insert-calls))))
|
||||
(is (every? #(= true (get-in % [:opts :sibling?])) @insert-calls))
|
||||
(is (every? #(= (:block/uuid block) (get-in % [:opts :block-uuid])) @insert-calls))
|
||||
(done)))
|
||||
(p/catch (fn [e]
|
||||
(reset! state/state prev-state)
|
||||
(is false (str e))
|
||||
(done)))))))
|
||||
|
||||
(deftest publish-session-manual-pr-inserts-manual-url-and-summary-sibling-blocks-test
|
||||
(async done
|
||||
(let [insert-calls (atom [])
|
||||
block {:block/uuid #uuid "dddddddd-dddd-dddd-dddd-dddddddddddd"}
|
||||
prev-state @state/state]
|
||||
(swap! state/state assoc :agent/sessions {(str (:block/uuid block)) {:session-id "sess-pr-2"}})
|
||||
(-> (p/with-redefs [db-sync/http-base (fn [] "http://base")
|
||||
db-sync/fetch-json (fn [url _opts _]
|
||||
(if (string/includes? url "/events")
|
||||
(p/resolved {:events []})
|
||||
(p/resolved {:status "manual-pr-required"
|
||||
:manual-pr-url "https://github.com/example/repo/compare/a...b"})))
|
||||
user-handler/task--ensure-id&access-token (fn [resolve _reject]
|
||||
(resolve true))
|
||||
agent-handler/task-ready? (fn [_] true)
|
||||
notification/show! (fn [& _] nil)
|
||||
editor-handler/api-insert-new-block! (fn [content opts]
|
||||
(swap! insert-calls conj {:content content :opts opts}))]
|
||||
(p/let [_ (agent-handler/<publish-session! block {:create-pr? true
|
||||
:body "Agent summary for manual PR"})
|
||||
_ (reset! state/state prev-state)]
|
||||
(is (= 2 (count @insert-calls)))
|
||||
(is (= "PR URL: https://github.com/example/repo/compare/a...b"
|
||||
(:content (first @insert-calls))))
|
||||
(is (= "PR Summary: Agent summary for manual PR"
|
||||
(:content (second @insert-calls))))
|
||||
(done)))
|
||||
(p/catch (fn [e]
|
||||
(reset! state/state prev-state)
|
||||
(is false (str e))
|
||||
(done)))))))
|
||||
|
||||
(deftest publish-session-push-only-does-not-insert-pr-sibling-blocks-test
|
||||
(async done
|
||||
(let [insert-calls (atom [])
|
||||
block {:block/uuid #uuid "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"}
|
||||
prev-state @state/state]
|
||||
(swap! state/state assoc :agent/sessions {(str (:block/uuid block)) {:session-id "sess-pr-3"}})
|
||||
(-> (p/with-redefs [db-sync/http-base (fn [] "http://base")
|
||||
db-sync/fetch-json (fn [url _opts _]
|
||||
(if (string/includes? url "/events")
|
||||
(p/resolved {:events []})
|
||||
(p/resolved {:status "pushed"})))
|
||||
user-handler/task--ensure-id&access-token (fn [resolve _reject]
|
||||
(resolve true))
|
||||
agent-handler/task-ready? (fn [_] true)
|
||||
notification/show! (fn [& _] nil)
|
||||
editor-handler/api-insert-new-block! (fn [content opts]
|
||||
(swap! insert-calls conj {:content content :opts opts}))]
|
||||
(p/let [_ (agent-handler/<publish-session! block {:create-pr? false
|
||||
:body "Summary should be ignored"})
|
||||
_ (reset! state/state prev-state)]
|
||||
(is (empty? @insert-calls))
|
||||
(done)))
|
||||
(p/catch (fn [e]
|
||||
(reset! state/state prev-state)
|
||||
(is false (str e))
|
||||
(done)))))))
|
||||
|
||||
(deftest publish-session-sends-commit-message-test
|
||||
(async done
|
||||
(let [request-bodies (atom [])
|
||||
block {:block/uuid #uuid "ffffffff-ffff-ffff-ffff-ffffffffffff"}
|
||||
prev-state @state/state]
|
||||
(swap! state/state assoc :agent/sessions {(str (:block/uuid block)) {:session-id "sess-pr-4"}})
|
||||
(-> (p/with-redefs [db-sync/http-base (fn [] "http://base")
|
||||
db-sync/fetch-json (fn [url opts _]
|
||||
(if (string/includes? url "/events")
|
||||
(p/resolved {:events []})
|
||||
(do
|
||||
(swap! request-bodies conj (js->clj (js/JSON.parse (:body opts))
|
||||
:keywordize-keys true))
|
||||
(p/resolved {:status "pushed"}))))
|
||||
user-handler/task--ensure-id&access-token (fn [resolve _reject]
|
||||
(resolve true))
|
||||
agent-handler/task-ready? (fn [_] true)
|
||||
notification/show! (fn [& _] nil)]
|
||||
(p/let [_ (agent-handler/<publish-session! block {:create-pr? false
|
||||
:commit-message "feat: summarize PR changes"})
|
||||
_ (reset! state/state prev-state)]
|
||||
(is (= "feat: summarize PR changes"
|
||||
(:commit-message (first @request-bodies))))
|
||||
(done)))
|
||||
(p/catch (fn [e]
|
||||
(reset! state/state prev-state)
|
||||
(is false (str e))
|
||||
(done)))))))
|
||||
|
||||
Reference in New Issue
Block a user