Skip to content

Commit 70d6f86

Browse files
committed
refactor: dedupe confirm_delete/2 + id/name/type table
mix ex_dna flagged two code clones in v0.2.1 (carried forward from v0.2.0): 1. confirm_delete/2 (projects.ex, repos.ex) An identical y/N prompt body. Extracted to AdoCli.CLI.Helpers.confirm_delete/2. The local defp wrappers were removed (reach flagged them as trivial forwarders); call sites now invoke the Helpers version directly. 2. id/name/type table printer (connections.ex, wikis.ex) Identical Enum.each over a list of maps formatting id / name / type columns. Extracted to AdoCli.CLI.Helpers.print_id_name_type_table/1. Both helpers were added to AdoCli.CLI.Helpers, which gained 'import CliMate.CLI' so it can call write/1 and halt_error/1. Other changes: * mix.exs: add 'ex_dna' to the mix ci alias (8th quality step). mix ci now fails if ex_dna reports clones. * mix.lock: transitive dep bumps (json_codec 0.1.4 -> 0.1.5, quackdb 0.5.12 -> 0.5.13). Verified: * mix ex_dna -> 'No code duplication detected' (43 files) * mix ci -> 8/8 steps green (compile, format, credo, deps.unlock, deps.audit, xref, dialyzer, test --cover) * mix ex_dna added to mix ci * 313 tests pass (no regressions) * reach: trivial-forwarder warning gone
1 parent 5bbb328 commit 70d6f86

7 files changed

Lines changed: 68 additions & 35 deletions

File tree

lib/ado_cli/cli/connections.ex

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,7 @@ defmodule AdoCli.CLI.Connections do
8181
writeln("#{String.pad_trailing("ID", 40)} #{String.pad_trailing("Name", 30)} Type")
8282
writeln(String.duplicate("─", 85))
8383

84-
Enum.each(conns, fn c ->
85-
writeln(
86-
"#{String.pad_trailing(c["id"] || "", 40)} #{String.pad_trailing(c["name"] || "", 30)} #{c["type"] || ""}"
87-
)
88-
end)
84+
AdoCli.CLI.Helpers.print_id_name_type_table(conns)
8985

9086
writeln("")
9187
writeln("#{length(conns)} connection(s)")

lib/ado_cli/cli/helpers.ex

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ defmodule AdoCli.CLI.Helpers do
1010
shape); otherwise it's human-readable formatted text.
1111
"""
1212

13+
import CliMate.CLI
1314
alias AdoCli.CLI.Output
1415

1516
@doc """
@@ -166,4 +167,63 @@ defmodule AdoCli.CLI.Helpers do
166167

167168
defp network_error_classification(reason),
168169
do: {"network_error", "Request failed: #{inspect(reason, limit: 200)}"}
170+
171+
@doc """
172+
Prompts the user to confirm a destructive action.
173+
174+
Prints `Delete {kind} '{id}'? This cannot be undone. [y/N] ` and
175+
waits for input on stdin. Returns `:ok` if the user types `y` (or
176+
`Y`); otherwise calls `halt_error/1` with "Aborted." and does not
177+
return.
178+
179+
Used by the delete subcommands of `ado projects` and `ado repos`
180+
(and any other command that needs a y/n confirmation before
181+
destructive actions). Extracted from two near-identical copies
182+
that ex_dna flagged as code clones.
183+
184+
Examples:
185+
186+
case confirm_delete("project", "MyProject") do
187+
:ok -> Client.delete(...)
188+
end
189+
"""
190+
@spec confirm_delete(String.t(), String.t()) :: :ok | no_return()
191+
def confirm_delete(kind, id) do
192+
write("Delete #{kind} '#{id}'? This cannot be undone. [y/N] ")
193+
194+
if String.downcase(String.trim(IO.gets(""))) == "y" do
195+
:ok
196+
else
197+
halt_error("Aborted.")
198+
end
199+
end
200+
201+
@doc """
202+
Prints a 3-column table of `id / name / type` for a list of maps.
203+
204+
Each item in `rows` is expected to be a map (or struct) with
205+
string fields `"id"`, `"name"`, and `"type"`. The column widths
206+
(40 and 30) are fixed — chosen to match the existing
207+
`ado connections list` and `ado wikis list` output styles. Missing
208+
fields render as empty strings (not "nil").
209+
210+
Used by the `list` subcommands of `ado connections` and `ado wikis`
211+
(extracted from two near-identical copies that ex_dna flagged as
212+
code clones).
213+
214+
Example:
215+
216+
rows = [%{"id" => "abc", "name" => "My Connection", "type" => "github"}]
217+
print_id_name_type_table(rows)
218+
"""
219+
@spec print_id_name_type_table([map()]) :: :ok
220+
def print_id_name_type_table(rows) do
221+
Enum.each(rows, fn row ->
222+
writeln(
223+
"#{String.pad_trailing(row["id"] || "", 40)} " <>
224+
"#{String.pad_trailing(row["name"] || "", 30)} " <>
225+
"#{row["type"] || ""}"
226+
)
227+
end)
228+
end
169229
end

lib/ado_cli/cli/projects.ex

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ defmodule AdoCli.CLI.Projects do
196196
project_id = parsed.arguments.project_id
197197

198198
unless Map.get(parsed.options, :force) do
199-
confirm_delete("project", project_id)
199+
AdoCli.CLI.Helpers.confirm_delete("project", project_id)
200200
end
201201

202202
case Client.delete("/_apis/projects/#{URI.encode(project_id)}") do
@@ -232,16 +232,6 @@ defmodule AdoCli.CLI.Projects do
232232
defp process_template_id("cmmi"), do: "27450541-8e31-4150-9947-dc59f998fc01"
233233
defp process_template_id(unknown), do: unknown
234234

235-
defp confirm_delete(kind, id) do
236-
write("Delete #{kind} '#{id}'? This cannot be undone. [y/N] ")
237-
238-
if String.downcase(String.trim(IO.gets(""))) == "y" do
239-
:ok
240-
else
241-
halt_error("Aborted.")
242-
end
243-
end
244-
245235
# ── Formatting ────────────────────────────────────────────────────────
246236

247237
defp print_projects_table(projects) do

lib/ado_cli/cli/repos.ex

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ defmodule AdoCli.CLI.Repos do
199199
repo_id = parsed.arguments.repo_id
200200

201201
unless Map.get(parsed.options, :force) do
202-
confirm_delete("repository", "#{project}/#{repo_id}")
202+
AdoCli.CLI.Helpers.confirm_delete("repository", "#{project}/#{repo_id}")
203203
end
204204

205205
case Client.delete("/#{URI.encode(project)}/_apis/git/repositories/#{URI.encode(repo_id)}") do
@@ -217,16 +217,6 @@ defmodule AdoCli.CLI.Repos do
217217

218218
# ── Helpers ───────────────────────────────────────────────────────────
219219

220-
defp confirm_delete(kind, id) do
221-
write("Delete #{kind} '#{id}'? This cannot be undone. [y/N] ")
222-
223-
if String.downcase(String.trim(IO.gets(""))) == "y" do
224-
:ok
225-
else
226-
halt_error("Aborted.")
227-
end
228-
end
229-
230220
# ── Formatting ────────────────────────────────────────────────────────
231221

232222
defp print_repos_table(repos) do

lib/ado_cli/cli/wikis.ex

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -222,11 +222,7 @@ defmodule AdoCli.CLI.Wikis do
222222
writeln("#{String.pad_trailing("ID", 40)} #{String.pad_trailing("Name", 30)} Type")
223223
writeln(String.duplicate("─", 85))
224224

225-
Enum.each(wikis, fn w ->
226-
writeln(
227-
"#{String.pad_trailing(w["id"] || "", 40)} #{String.pad_trailing(w["name"] || "", 30)} #{w["type"] || ""}"
228-
)
229-
end)
225+
AdoCli.CLI.Helpers.print_id_name_type_table(wikis)
230226

231227
writeln("")
232228
writeln("#{length(wikis)} wiki(s)")

mix.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ defmodule AdoCli.MixProject do
155155
"deps.audit",
156156
"xref graph --label compile-connected --fail-above 0",
157157
"ci.dialyzer",
158-
"test --cover"
158+
"test --cover",
159+
"ex_dna"
159160
],
160161
quality: [
161162
"compile --all-warnings --warnings-as-errors",

mix.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"finch": {:hex, :finch, "0.22.0", "5c48fa6f9706a78eb9036cacb67b8b996b4e66d111c543f4c29bb0f879a6806b", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b94e83c47780fc6813f746a1f1a34ee65cda42da4c5ea26a68f0acc4498e23dc"},
2323
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
2424
"jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
25-
"json_codec": {:hex, :json_codec, "0.1.4", "bb1d0b74dcd4c9daa8a3d5a3e12aeda5ea2db8659a261b1b291302f8a5d7d159", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "3bc1f8d44857a579de01f2bb7a625de43ffebad1534309ab419dab610fe3c876"},
25+
"json_codec": {:hex, :json_codec, "0.1.5", "44532ea8604f3587d0e8b0b33002745ba068c9cb1663d73c1eee49a5e7cd7e78", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6631dd3ce56a567e57ccb7a1129f57bb9206663fbb644c6ee47acdba36933f73"},
2626
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
2727
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
2828
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
@@ -37,7 +37,7 @@
3737
"pi_bridge": {:hex, :pi_bridge, "0.6.21", "f1a63da5f49fef4f6b35e98edd5400cbd64a49fb8bda84be41b7e91f3deb0b89", [:mix], [{:bandit, "~> 1.8", [hex: :bandit, repo: "hexpm", optional: false]}, {:dune, "~> 0.3", [hex: :dune, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.13", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ex_ast, "~> 0.12", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:json_codec, "~> 0.1.3", [hex: :json_codec, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:quackdb, "~> 0.5.4", [hex: :quackdb, repo: "hexpm", optional: false]}, {:reach, "~> 2.6", [hex: :reach, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:req_llm, "~> 1.6", [hex: :req_llm, repo: "hexpm", optional: true]}], "hexpm", "b226fb60965a8e928186af3c3b96fe8f0fefd8c6cc7be679d6e169ba84105de9"},
3838
"plug": {:hex, :plug, "1.19.2", "e4950525b22c6789dfb38a3f95d47171ba159da3fc5a33be9643b43d5e8adb98", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b6fce20a56af5e60fa5dfecf3f907bb98ec981be43c79a3809a499bc3d133de0"},
3939
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
40-
"quackdb": {:hex, :quackdb, "0.5.12", "461f3997c4b8f5640a3a128e8e49875e0b7c6f74d1a26aa6f59c9fc142d5ed34", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.7", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:explorer, "~> 0.11", [hex: :explorer, repo: "hexpm", optional: true]}, {:fsst, "~> 0.1.2", [hex: :fsst, repo: "hexpm", optional: true]}, {:geo, "~> 4.1", [hex: :geo, repo: "hexpm", optional: true]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:muontrap, "~> 1.5", [hex: :muontrap, repo: "hexpm", optional: false]}, {:table, "~> 0.1", [hex: :table, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:varint, "~> 1.6", [hex: :varint, repo: "hexpm", optional: false]}], "hexpm", "a9fb12190c465bb6ec76132829e3bd4eae4d60318d097c84070c28a41115e1bf"},
40+
"quackdb": {:hex, :quackdb, "0.5.13", "854294a5a85e0f2972d2229da8dfb25afd2cda500e80e477f6987bff823c2b21", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.7", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:explorer, "~> 0.11", [hex: :explorer, repo: "hexpm", optional: true]}, {:fsst, "~> 0.1.2", [hex: :fsst, repo: "hexpm", optional: true]}, {:geo, "~> 4.1", [hex: :geo, repo: "hexpm", optional: true]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:muontrap, "~> 1.5", [hex: :muontrap, repo: "hexpm", optional: false]}, {:table, "~> 0.1", [hex: :table, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:varint, "~> 1.6", [hex: :varint, repo: "hexpm", optional: false]}], "hexpm", "a5d18e54de682bde9f4ab1866126aa90c26223866bc693bf56cdf9dafa3859d3"},
4141
"reach": {:hex, :reach, "2.7.5", "2148096233ebf84f1b9c79d23134c3262f546303af07ee21f7e9d7ed281ff616", [:mix], [{:boxart, "~> 0.3.3", [hex: :boxart, repo: "hexpm", optional: true]}, {:ex_ast, "~> 0.12.0", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:ex_dna, "~> 1.5", [hex: :ex_dna, repo: "hexpm", optional: true]}, {:libgraph, "~> 0.16.0", [hex: :libgraph, repo: "hexpm", optional: false]}, {:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: true]}, {:makeup_js, "~> 0.1", [hex: :makeup_js, repo: "hexpm", optional: true]}, {:quickbeam, "~> 0.10", [hex: :quickbeam, repo: "hexpm", optional: true]}], "hexpm", "b31fd7cf23a649a6f76f11168b2ef296845441d6498474634ec673dee2d60567"},
4242
"req": {:hex, :req, "0.6.1", "7b904c8b42d0e08136a5c6aba024fd12fc79a1ed8856e7a3522b0917f7e75113", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21.0 or ~> 0.22.0", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "aaf11c9c80f2df2364630b3594e1857fe610d8ea7cb994e1ce3dcb55f204ff1c"},
4343
"sourceror": {:hex, :sourceror, "1.12.0", "da354c5f35aad3cc1132f5d5b0d8437d865e2661c263260480bab51b5eedb437", [:mix], [], "hexpm", "755703683bd014ebcd5de9acc24b68fb874a660a568d1d63f8f98cd8a6ef9cd0"},

0 commit comments

Comments
 (0)