Skip to content

Commit 8193c93

Browse files
authored
simplify + update merge.clj (#390)
1 parent 235bc3b commit 8193c93

File tree

3 files changed

+180
-104
lines changed

3 files changed

+180
-104
lines changed

script/merge.clj

Lines changed: 155 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
[babashka.cli :as cli]
44
[babashka.process :as p]
55
[cheshire.core :as json]
6-
[clojure.pprint :as pp]
76
[clojure.string :as str]
87
[ice.core :as ice]
98
[util :as u]))
@@ -17,97 +16,167 @@
1716
:source-branch {:ref "<source-branch>"
1817
:desc "The source branch of the triggering PR."
1918
:alias :r
20-
:require true}
21-
:dry-run {:desc "If set, will not execute the command, just print it out."
22-
:coerce :boolean
23-
:default false}}
19+
:require true}}
2420
:error-fn u/cli-error-fn})
2521

26-
(defn source+target-branch->pr-number [source-branch target-branch]
27-
(let [head-ref-name (str source-branch "->" target-branch)
28-
_ (prn {:source-branch source-branch
22+
(defn- find-pr-list [source-branch target-branch]
23+
(let [pr (-> (p/sh "gh" "pr" "list"
24+
"--head" (u/head-ref-name source-branch target-branch)
25+
"--json" "number,headRefName")
26+
:out
27+
(json/parse-string true)
28+
first)]
29+
(if pr
30+
(do (ice/p [:green "Found PR #" (:number pr)])
31+
(:number pr))
32+
(throw (ex-info
33+
(str "No PR found for " (u/head-ref-name source-branch target-branch))
34+
{:source-branch source-branch
2935
:target-branch target-branch
30-
:head-ref-name head-ref-name})
31-
prs (-> (p/shell {:out :string}
32-
"gh" "pr" "list"
33-
"--limit" "1000"
34-
"--repo" "metabase/docs.metabase.github.io"
35-
"--json" "number,headRefName")
36-
:out
37-
(json/parse-string true)
38-
(into []))
39-
_ (println "→ Open PR count: " (count prs))
40-
_ (println "→ Open PRs: \n"
41-
(str/join "\n"
42-
(map #(str " " %)
43-
(str/split-lines (with-out-str (pp/pprint prs))))))
44-
_ (ice/p "See: " [:bold "https://github.com/metabase/docs.metabase.github.io/pulls"] " for more details")
45-
_ (println "→ Looking for PR with headRefName:" head-ref-name)
46-
pr-to-merge (first (filter #(= (:headRefName %) head-ref-name) prs))]
47-
(println "Found PR: " (pr-str pr-to-merge))
48-
(:number pr-to-merge)))
49-
50-
(defn update-pr-branch
51-
"Update the PR branch to include latest changes from base branch, resolving conflicts by taking incoming changes"
52-
[{:keys [dry-run? pr-number head-ref-name]}]
53-
(ice/p [:blue "Updating PR branch to latest master..."])
54-
(if-not dry-run?
55-
;; First try the API approach (clean merge)
56-
(let [{:keys [exit]} (p/sh "gh" "api"
57-
"--method" "PUT"
58-
(str "/repos/metabase/docs.metabase.github.io/pulls/" pr-number "/update-branch"))]
59-
(if (zero? exit)
60-
(ice/p [:green "✓ PR branch updated successfully via API"])
61-
(do
62-
(ice/p [:yellow "API update failed, likely due to conflict, trying git-based resolution..."])
63-
;; If API fails due to conflicts, resolve manually
64-
(try
65-
;; Fetch latest and checkout the PR branch
66-
(p/sh "git" "fetch" "origin")
67-
(p/sh "git" "checkout" head-ref-name)
68-
(prn (p/sh "git" "status"))
69-
70-
;; Try to merge master - this will show conflicts
71-
(let [merge-result (p/shell {:continue true} "git" "merge" "origin/master")]
72-
(if (= 0 (:exit merge-result))
73-
(ice/p [:green "✓ Clean merge successful"])
74-
(do
75-
;; Resolve conflicts by taking all changes from The PR Branch
76-
(ice/p [:blue "Resolving conflicts by preferring our changes..."])
77-
(p/sh "git" "checkout" "--ours" ".")
78-
(p/sh "git" "add" ".")
79-
(p/sh "git" "commit" "--no-edit" "-m" (str "Merge master, preferring changes from PR #" pr-number))
80-
(ice/p [:green "✓ Conflicts resolved, preferring PR branch's changes"]))))
81-
82-
;; Push the updated branch
83-
(p/sh "git" "push" "origin" head-ref-name)
84-
(ice/p [:green "✓ PR branch updated via git"])
85-
86-
(catch Exception git-e
87-
(ice/p [:red "Git-based update also failed: " (.getMessage git-e)]))))))
88-
(println "Dry run mode: would update PR branch, resolving conflicts by preferring incoming changes")))
89-
90-
(defn- gh-pr-merge [dry-run? pr-number]
91-
(let [cmd ["gh" "pr" "merge" pr-number "--squash" "--delete-branch"]]
92-
(if dry-run?
93-
(ice/p [:yellow "Dry run mode: not actually merging PR:\n"
94-
[:white [:bold "Would run: "] [:underline (str/join " " cmd)]]])
95-
(apply p/sh cmd))))
36+
:babashka/exit 1})))))
37+
38+
(defn- find-pr-view [source-branch target-branch]
39+
(let [pr-num (-> (p/sh "gh" "pr" "view" (u/head-ref-name source-branch target-branch)
40+
"--json" "number"
41+
"--jq" ".number")
42+
:out
43+
str/trim)]
44+
(when pr-num (parse-long pr-num))))
45+
46+
(defn- resolve-conflicts
47+
"Resolve conflicts by auto-merging changes in artifact directories based on merge-strategy.
48+
If merge-strategy is :ours, prefer changes from the PR branch.
49+
If merge-strategy is :theirs, prefer changes from the target branch."
50+
[artifact-dirs merge-strategy]
51+
(let [conflicted-files (->> (p/shell {:out :string :continue true}
52+
"git" "diff" "--name-only" "--diff-filter=U")
53+
:out
54+
str/trim
55+
str/split-lines
56+
(remove str/blank?))
57+
strat (case merge-strategy
58+
:ours "--ours"
59+
:theirs "--theirs")]
60+
(if (empty? conflicted-files)
61+
(ice/p [:green "No conflicts to resolve"])
62+
(do
63+
(ice/p [:blue "Conflicted files: " (str/join ", " conflicted-files)])
64+
(ice/p [:blue "Artifact directories: " (str/join ", " artifact-dirs)])
65+
(doseq [dir artifact-dirs]
66+
(let [files-in-dir (filter #(str/starts-with? % dir) conflicted-files)]
67+
(when (seq files-in-dir)
68+
(ice/p [:yellow "Resolving conflicts in directory: " dir])
69+
(doseq [file files-in-dir]
70+
(ice/p [:yellow "Resolving file: " file])
71+
(ice/p [:yellow " - Checking out " strat ": | " (:out (p/sh "git" "checkout" strat file))])
72+
(ice/p [:yellow " - Adding file: | " (:out (p/sh "git" "add" file))])))))))))
73+
74+
(defn- update-and-merge-pr [source-branch target-branch pr-number merge-strategy]
75+
(let [head-ref-name (u/head-ref-name source-branch target-branch)]
76+
77+
78+
(ice/p [:blue "Updating PR branch..."])
79+
(ice/p [:blue "Attempting merge with origin/master..."])
80+
(let [merge-result (p/sh {:continue true} "git" "merge" "origin/master")]
81+
(when-not (zero? (:exit merge-result))
82+
(ice/p [:red "✗ Merge failed: " (:err merge-result)])
83+
(let [winner (if (= merge-strategy :ours) "PR" "master")]
84+
(ice/p [:yellow "Attempting to resolve conflicts with git, preferring changes from " winner "..."])
85+
(resolve-conflicts (u/->artifact-dirs target-branch) merge-strategy)
86+
;; Do the commit, now that we've resolved conflicts
87+
(pr-str (p/sh "git" "commit" "--no-edit" "-m"
88+
(str "Merge " target-branch " for PR #(" pr-number ")"
89+
", preferring changes from " winner)))))
90+
91+
(ice/p [:blue "Pushing changes to PR branch..."])
92+
(ice/p "Result: " (pr-str (p/sh "git" "push" "origin" head-ref-name))))
93+
94+
;; Wait a bit for GitHub to process to avoid a race condition
95+
(Thread/sleep 5000)
96+
97+
;; Merge the PR
98+
(ice/p [:blue "Merging PR #" pr-number "..."])
99+
(let [merge-result (p/sh {:continue true}
100+
"gh" "pr" "merge" (str pr-number)
101+
"--squash" "--delete-branch"
102+
"--repo" "metabase/docs.metabase.github.io")]
103+
(if (zero? (:exit merge-result))
104+
(ice/p [:green "✓ PR merged successfully!"])
105+
(ice/p [:red "✗ Merge failed: " [:bold (:err merge-result)]])))))
106+
107+
(defn- should-pr-win?
108+
"Determine if the current PR should win conflicts based on PR number comparison"
109+
[current-pr-number target-branch]
110+
(let [_ (p/sh "git" "fetch" "origin")
111+
latest-master-commit (-> (p/sh "git" "log" "--oneline" "-1" (str "origin/" target-branch))
112+
:out
113+
str/trim)
114+
;; Extract PR number from commit message like "[auto] adding content to docs-rc-notes->master (#380)"
115+
master-pr-match (re-find #"#(\d+)" latest-master-commit)
116+
master-pr-number (when master-pr-match (parse-long (second master-pr-match)))]
117+
(ice/p latest-master-commit)
118+
(ice/p [:blue "Current PR: #" current-pr-number])
119+
(ice/p [:blue "Latest master commit: " latest-master-commit])
120+
(when master-pr-number
121+
(ice/p [:blue "Latest master PR: #" master-pr-number]))
122+
123+
(cond
124+
(nil? master-pr-number)
125+
(do (ice/p [:yellow "No PR number found in master, defaulting to PR wins"])
126+
true)
127+
128+
(>= current-pr-number master-pr-number)
129+
(do (ice/p [:green "Current PR #" current-pr-number " is newer than master PR #" master-pr-number " - PR wins"])
130+
true)
131+
132+
:else
133+
(do (ice/p [:yellow "Current PR #" current-pr-number " is older than master PR #" master-pr-number " - master wins"])
134+
false))))
135+
136+
(defn- checkout-branch! [head-ref-name]
137+
;; First, discard any local changes that would prevent checkout
138+
(ice/p [:yellow "Discarding local changes..."])
139+
(p/sh "git" "reset" "--hard" "HEAD")
140+
(p/sh "git" "clean" "-fd")
141+
142+
;; Try to checkout the branch
143+
(let [checkout-result (p/shell {:continue true} "git" "checkout" head-ref-name)]
144+
(when-not (zero? (:exit checkout-result))
145+
;; Branch doesn't exist locally, create it from origin and force reset
146+
(ice/p [:yellow "Branch doesn't exist locally, creating from origin..."])
147+
(let [create-result (p/sh {:continue true} "git" "checkout" "-B" head-ref-name (str "origin/" head-ref-name))]
148+
(when-not (zero? (:exit create-result))
149+
(throw (ex-info (str "Failed to checkout or create branch " head-ref-name)
150+
{:branch head-ref-name
151+
:error (:err create-result)
152+
:babashka/exit 1})))))
153+
154+
;; Force reset to match the remote branch exactly
155+
(ice/p [:yellow "Force resetting to match remote branch..."])
156+
(p/sh "git" "reset" "--hard" (str "origin/" head-ref-name))))
96157

97158
(defn -main [& args]
98-
(let [{:keys [source-branch target-branch]
99-
dry-run? :dry-run
100-
:as opts} (cli/parse-opts args cli-spec)
159+
(println "Merge opertaion running at: " (str (java.time.Instant/now)))
160+
(let [{:keys [source-branch target-branch]} (cli/parse-opts args cli-spec)
101161
[source-branch target-branch] (mapv str/trim [source-branch target-branch])
102-
pr-number (source+target-branch->pr-number source-branch target-branch)]
103-
(when-not pr-number
104-
(throw (ex-info (ice/p-str [:red "No PR found for source branch "] [:bold source-branch] " and target branch " [:bold target-branch] ".")
105-
{:babashka/exit 1 :opts opts})))
106-
(update-pr-branch {:dry-run? dry-run?
107-
:pr-number pr-number
108-
:head-ref-name (str source-branch "->" target-branch)})
109-
(ice/p [:green "Merging PR for branch "] [:bold source-branch] " into " [:bold target-branch] " with PR number " [:bold (pr-str pr-number)])
110-
(gh-pr-merge dry-run? pr-number)))
162+
head-ref-name (u/head-ref-name source-branch target-branch)]
163+
164+
;; Ensure we're working with the latest remote state
165+
(ice/p [:blue "Fetching latest from origin..."]) (p/sh "git" "fetch" "origin")
166+
(ice/p [:blue "Checking out branch: " head-ref-name]) (checkout-branch! head-ref-name)
167+
168+
(let [current-branch (:out (p/sh "git" "branch" "--show-current"))
169+
_ (ice/p [:green "Currently on branch: " (str/trim current-branch)])
170+
pr-number-view (try (find-pr-view source-branch target-branch)
171+
(catch Exception e
172+
(ice/p [:red "Error finding pr-number via view: " (ex-message e)])))
173+
pr-number-list (try (find-pr-list source-branch target-branch)
174+
(catch Exception e
175+
(ice/p [:red "Error finding pr-number via list: " (ex-message e)])))
176+
pr-number (or pr-number-view pr-number-list)
177+
merge-strategy (if (should-pr-win? pr-number target-branch) :ours :theirs)]
178+
(ice/p [:green "Merging PR #" pr-number ": " (u/head-ref-name source-branch target-branch) " | with strategy: " [:blue merge-strategy]])
179+
(update-and-merge-pr source-branch target-branch pr-number merge-strategy))))
111180

112181
(when (= *file* (System/getProperty "babashka.file"))
113182
(apply -main *command-line-args*))

script/update_or_create_pr.clj

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,26 +37,11 @@
3737
;; _ (println "→ Curl data: " (pr-str curl-data))
3838
pr-data (-> raw-data :out (json/parse-string true))
3939
_ (println "→ PR data: " (pr-str pr-data))
40-
pr-info (first (filter #(= (:headRefName %) (str source "->" target))
40+
pr-info (first (filter #(= (:headRefName %) (u/head-ref-name source target))
4141
pr-data))]
4242
(println "→ PR info:" pr-info)
4343
(:number pr-info)))
4444

45-
(defn ->artifact-dirs [category release-num]
46-
(cond
47-
(= category :master) ["_docs/master"
48-
"_site/docs/master"]
49-
50-
(= (u/config-docs-version) release-num)
51-
["_docs/latest"
52-
"_site/docs/latest"
53-
(str "_docs/v0." release-num)
54-
(str "_site/docs/v0." release-num)]
55-
56-
(= category :release) [(str "_docs/v0." release-num)
57-
(str "_site/docs/v0." release-num)]
58-
:else []))
59-
6045
(defn- report-pr-body [source-branch target-branch artifact-dirs pr-number]
6146
(str/join "\n"
6247
[(str "`" source-branch "` -> `" target-branch "`")
@@ -88,15 +73,15 @@
8873
{:babashka/exit 1}))))
8974
(println "→ Source Branch info: " source-branch))
9075

91-
target-branch-name (str source-branch "->" target-branch)
76+
target-branch-name (u/head-ref-name source-branch target-branch)
9277
_ (p/shell "git" "checkout" "-B" target-branch-name)
9378

9479
update-dirs (remove str/blank? (str/split update-dirs #","))
9580
_ (u/pp ["update-dirs" update-dirs])
9681

9782
artifact-dirs (concat
9883
update-dirs
99-
(->artifact-dirs category release-num))
84+
(u/->artifact-dirs category release-num))
10085
_ (doseq [ad artifact-dirs]
10186
(println "Adding" ad "...")
10287
(p/sh {:continue true} "git" "add" ad))

script/util.clj

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,25 @@
6262
(str/replace #"'" ""))))
6363
(str/split-lines (slurp (str (fs/expand-home "~/.zshrc")))))))}}
6464
cmd))
65+
66+
(defn ->artifact-dirs
67+
([target-branch-name]
68+
(let [[category release-num] (categorize-branchname target-branch-name)]
69+
(->artifact-dirs category release-num)))
70+
([category release-num]
71+
(cond
72+
(= category :master) ["_docs/master"
73+
"_site/docs/master"]
74+
75+
(= (config-docs-version) release-num)
76+
["_docs/latest"
77+
"_site/docs/latest"
78+
(str "_docs/v0." release-num)
79+
(str "_site/docs/v0." release-num)]
80+
81+
(= category :release) [(str "_docs/v0." release-num)
82+
(str "_site/docs/v0." release-num)]
83+
:else [])))
84+
85+
(defn head-ref-name [source-branch target-branch]
86+
(str source-branch "->" target-branch))

0 commit comments

Comments
 (0)