Skip to content

Commit be282b3

Browse files
committed
Auto-commit: Sync changes [2026-03-05]
1 parent 350e056 commit be282b3

2 files changed

Lines changed: 183 additions & 280 deletions

File tree

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/env julia
2+
#=
3+
Repo Reconciliation Tool
4+
Compares GitLab and GitHub repositories to identify sync gaps.
5+
Fully ported to Julia.
6+
=#
7+
8+
using HTTP
9+
using JSON
10+
using Dates
11+
12+
# Configuration
13+
const GITLAB_USER = "hyperpolymath"
14+
const GITLAB_GROUPS = ["maa-framework"]
15+
const GITHUB_USER = "hyperpolymath"
16+
17+
struct Repo
18+
name::String
19+
platform::String
20+
url::String
21+
last_activity::String
22+
description::String
23+
end
24+
25+
function get_gitlab_repos(token::String)
26+
repos = Repo[]
27+
headers = ["PRIVATE-TOKEN" => token]
28+
29+
# User repos
30+
page = 1
31+
while true
32+
url = "https://gitlab.com/api/v4/users/$GITLAB_USER/projects?per_page=100&page=$page"
33+
resp = HTTP.get(url, headers)
34+
if resp.status != 200
35+
println("GitLab API error: $(resp.status)")
36+
break
37+
end
38+
data = JSON.parse(String(resp.body))
39+
if isempty(data)
40+
break
41+
end
42+
for r in data
43+
push!(repos, Repo(
44+
r["path"],
45+
"gitlab",
46+
r["web_url"],
47+
get(r, "last_activity_at", ""),
48+
get(r, "description", "") === nothing ? "" : r["description"]
49+
))
50+
end
51+
page += 1
52+
end
53+
54+
# Group repos
55+
for group in GITLAB_GROUPS
56+
page = 1
57+
while true
58+
url = "https://gitlab.com/api/v4/groups/$group/projects?per_page=100&page=$page&include_subgroups=true"
59+
try
60+
resp = HTTP.get(url, headers)
61+
if resp.status != 200; break; end
62+
data = JSON.parse(String(resp.body))
63+
if isempty(data); break; end
64+
for r in data
65+
push!(repos, Repo(
66+
r["path"],
67+
"gitlab",
68+
r["web_url"],
69+
get(r, "last_activity_at", ""),
70+
get(r, "description", "") === nothing ? "" : r["description"]
71+
))
72+
end
73+
page += 1
74+
catch
75+
break
76+
end
77+
end
78+
end
79+
return repos
80+
end
81+
82+
function get_github_repos(token::String)
83+
repos = Repo[]
84+
headers = ["Authorization" => "token $token"]
85+
86+
page = 1
87+
while true
88+
url = "https://api.github.com/users/$GITHUB_USER/repos?per_page=100&page=$page"
89+
resp = HTTP.get(url, headers)
90+
if resp.status != 200
91+
println("GitHub API error: $(resp.status)")
92+
break
93+
end
94+
data = JSON.parse(String(resp.body))
95+
if isempty(data)
96+
break
97+
end
98+
for r in data
99+
push!(repos, Repo(
100+
r["name"],
101+
"github",
102+
r["html_url"],
103+
get(r, "pushed_at", ""),
104+
get(r, "description", "") === nothing ? "" : r["description"]
105+
))
106+
end
107+
page += 1
108+
end
109+
return repos
110+
end
111+
112+
function reconcile(gitlab_repos, github_repos)
113+
gl_map = Dict(r.name => r for r in gitlab_repos)
114+
gh_map = Dict(r.name => r for r in github_repos)
115+
116+
all_names = sort(collect(union(keys(gl_map), keys(gh_map))))
117+
118+
result = Dict(
119+
"gitlab_only" => [],
120+
"github_only" => [],
121+
"both_synced" => [],
122+
"both_diverged" => [],
123+
"unknown" => []
124+
)
125+
126+
for name in all_names
127+
gl = get(gl_map, name, nothing)
128+
gh = get(gh_map, name, nothing)
129+
130+
if gl !== nothing && gh === nothing
131+
push!(result["gitlab_only"], Dict("name" => name, "url" => gl.url, "action" => "Mirror to GitHub"))
132+
elseif gh !== nothing && gl === nothing
133+
push!(result["github_only"], Dict("name" => name, "url" => gh.url, "action" => "New on GitHub"))
134+
else
135+
# Both exist - check dates
136+
try
137+
gl_dt = DateTime(replace(gl.last_activity, "Z" => ""), dateformat"yyyy-mm-ddTHH:MM:SS")
138+
gh_dt = DateTime(replace(gh.last_activity, "Z" => ""), dateformat"yyyy-mm-ddTHH:MM:SS")
139+
140+
diff_days = abs(Dates.value(Dates.Day(gl_dt - gh_dt)))
141+
142+
if diff_days > 1
143+
push!(result["both_diverged"], Dict("name" => name, "diff" => diff_days, "ahead" => gl_dt > gh_dt ? "GitLab" : "GitHub"))
144+
else
145+
push!(result["both_synced"], Dict("name" => name))
146+
end
147+
catch
148+
push!(result["unknown"], Dict("name" => name))
149+
end
150+
end
151+
end
152+
return result
153+
end
154+
155+
function main()
156+
gl_token = get(ENV, "GITLAB_TOKEN", "")
157+
gh_token = get(ENV, "GITHUB_TOKEN", "")
158+
159+
if isempty(gl_token) || isempty(gh_token)
160+
println("Please set GITLAB_TOKEN and GITHUB_TOKEN environment variables")
161+
return
162+
end
163+
164+
println("Fetching repos...")
165+
gl_repos = get_gitlab_repos(gl_token)
166+
gh_repos = get_github_repos(gh_token)
167+
168+
println("Reconciling...")
169+
res = reconcile(gl_repos, gh_repos)
170+
171+
println("\n" * "="^40)
172+
println("SUMMARY")
173+
println("-"^40)
174+
println("GitLab only: $(length(res["gitlab_only"]))")
175+
println("GitHub only: $(length(res["github_only"]))")
176+
println("Diverged: $(length(res["both_diverged"]))")
177+
println("Synced: $(length(res["both_synced"]))")
178+
println("="^40)
179+
end
180+
181+
if abspath(PROGRAM_FILE) == @__FILE__
182+
main()
183+
end

0 commit comments

Comments
 (0)