Skip to content

Commit f18f99a

Browse files
committed
lazybox: suppport repo worker
repo_lfs and repo_unshallow
1 parent 43317d4 commit f18f99a

File tree

3 files changed

+270
-9
lines changed

3 files changed

+270
-9
lines changed

lazybox/src/main/kotlin/cfig/lazybox/App.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cfig.lazybox
22

33
import cfig.lazybox.staging.DiffCI
4+
import cfig.lazybox.staging.RepoWorker
45
import cfig.lazybox.sysinfo.BootChart
56
import cfig.lazybox.sysinfo.CpuInfo
67
import cfig.lazybox.sysinfo.Pidstat
@@ -26,9 +27,11 @@ fun main(args: Array<String>) {
2627
println("cpuinfo : get cpu info from /sys/devices/system/cpu/")
2728
println("sysinfo : get overall system info from Android")
2829
println("\nIncubating usage:")
29-
println("apps : get apk file list from Android")
30-
println("dmainfo : parse /d/dma_buf/bufinfo")
31-
println("diffci : find changelist files from CI server based on date and time ranges")
30+
println("apps : get apk file list from Android")
31+
println("dmainfo : parse /d/dma_buf/bufinfo")
32+
println("diffci : find changelist files from CI server based on date and time ranges")
33+
println("repo_lfs : pull LFS files from Git repositories managed by 'repo'")
34+
println("repo_unshallow: unshallow Git repositories managed by 'repo'")
3235
exitProcess(0)
3336
}
3437
if (args[0] == "cpuinfo") {
@@ -102,4 +105,10 @@ fun main(args: Array<String>) {
102105
if (args[0] == "diffci") {
103106
DiffCI().run(args.drop(1).toTypedArray())
104107
}
108+
if (args[0] == "repo_lfs") {
109+
RepoWorker().lfsPullRepo(args.drop(1).toTypedArray())
110+
}
111+
if (args[0] == "repo_unshallow") {
112+
RepoWorker().unshallowRepo(args.drop(1).toTypedArray())
113+
}
105114
}

lazybox/src/main/kotlin/cfig/lazybox/staging/DiffCI.kt

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package cfig.lazybox.staging
22

33
import org.slf4j.LoggerFactory
4+
import java.io.File
45
import java.io.IOException
6+
import java.net.HttpURLConnection
7+
import java.net.MalformedURLException
58
import java.net.URI
9+
import java.net.URL
610
import java.net.http.HttpClient
711
import java.net.http.HttpRequest
812
import java.net.http.HttpResponse
@@ -20,7 +24,7 @@ class DiffCI {
2024
private val dateFormat = DateTimeFormatter.ofPattern("yyyyMMdd")
2125
private val dateTimeFormat = DateTimeFormatter.ofPattern("yyyyMMddHHmm")
2226
private val buildIdRegex = ".*/(\\d{12})/changelist".toRegex()
23-
private val log = LoggerFactory.getLogger(DiffCI::class.java)
27+
private val log = LoggerFactory.getLogger(DiffCI::class.simpleName)
2428

2529
fun run(args: Array<String>) {
2630
if (baseUrl == FAKE_URL) {
@@ -85,7 +89,7 @@ class DiffCI {
8589
val allUrls = mutableListOf<String>()
8690
var currentDate = startDate
8791
while (currentDate <= endDate) {
88-
log.info("\n----- Processing Date: $currentDate -----")
92+
log.info("----- Processing Date: $currentDate -----")
8993
val dailyUrls = finder.findChangelistUrlsForDate(currentDate)
9094
allUrls.addAll(dailyUrls)
9195
currentDate = currentDate.plusDays(1)
@@ -109,7 +113,7 @@ class DiffCI {
109113
val allUrls = mutableListOf<String>()
110114
var currentDate = startDateTime.toLocalDate()
111115
while (currentDate <= endDateTime.toLocalDate()) {
112-
log.info("\n----- Processing Date: $currentDate -----")
116+
log.info("----- Processing Date: $currentDate -----")
113117
val dailyUrls = finder.findChangelistUrlsForDate(currentDate)
114118

115119
val filteredUrls = dailyUrls.filter { url ->
@@ -130,19 +134,85 @@ class DiffCI {
130134

131135
private fun printUsageAndExit() {
132136
log.info("Error: Invalid number of arguments.")
133-
log.info("\nUsage:")
137+
log.info("Usage:")
134138
log.info(" Single Date: --args='diffci <yyyymmdd>'")
135139
log.info(" Date Range: --args='diffci <start_date> <end_date>'")
136140
log.info(" Time Range: --args='diffci <start_datetime> <end_datetime>'")
137-
log.info("\nExamples:")
141+
log.info("Examples:")
138142
log.info(" --args='diffci 20250628'")
139143
log.info(" --args='diffci 20250627 20250628'")
140144
log.info(" --args='diffci 202506281000 202506281430'")
141145
exitProcess(1)
142146
}
143147

148+
fun fetchChangeList(urlsToFetch: List<String>) {
149+
val OUTPUT_FILENAME = "merged_content.txt"
150+
if (urlsToFetch.isEmpty()) {
151+
return
152+
}
153+
val outputFile = File(OUTPUT_FILENAME)
154+
log.info("Starting to fetch content from ${urlsToFetch.size} URLs ...")
155+
156+
outputFile.bufferedWriter().use { writer ->
157+
// Iterate over each URL string in the list.
158+
urlsToFetch.forEachIndexed { index, urlString ->
159+
log.info("(${index + 1}/${urlsToFetch.size}) Fetching: $urlString")
160+
try {
161+
// Create a URL object from the string.
162+
val url = URL(urlString)
163+
val connection = (url.openConnection() as HttpURLConnection).also {
164+
it.requestMethod = "GET"
165+
it.connectTimeout = 5000 // 5 seconds
166+
it.readTimeout = 5000 // 5 seconds
167+
}
168+
val responseCode = connection.responseCode
169+
// Write a header for this URL's content in the output file.
170+
writer.write("--- START: Content from $urlString ---")
171+
writer.newLine()
172+
// Check for a successful HTTP response (2xx status codes).
173+
if (responseCode in 200..299) {
174+
connection.inputStream.bufferedReader().use { reader ->
175+
// Read the content line by line and write to the file.
176+
reader.forEachLine { line ->
177+
writer.write(line)
178+
writer.newLine()
179+
}
180+
}
181+
} else {
182+
val errorMessage = "Received non-OK response code: $responseCode"
183+
log.info("Warning: $errorMessage for $urlString")
184+
writer.write("! FAILED: $errorMessage")
185+
writer.newLine()
186+
}
187+
188+
} catch (e: MalformedURLException) {
189+
val errorMessage = "Invalid URL format"
190+
log.error("Error for '$urlString': $errorMessage. Skipping.")
191+
writer.write("! FAILED: $errorMessage")
192+
writer.newLine()
193+
} catch (e: IOException) {
194+
val errorMessage = "Error reading data (I/O problem): ${e.message}"
195+
log.info("Error for '$urlString': $errorMessage. Skipping.")
196+
writer.write("! FAILED: $errorMessage")
197+
writer.newLine()
198+
} catch (e: Exception) {
199+
val errorMessage = "An unexpected error occurred: ${e.message}"
200+
log.error("Error for '$urlString': $errorMessage. Skipping.")
201+
writer.write("! FAILED: $errorMessage")
202+
writer.newLine()
203+
} finally {
204+
// Write a footer and add extra newlines for separation.
205+
writer.write("--- END: Content from $urlString ---")
206+
writer.newLine()
207+
writer.newLine()
208+
}
209+
}
210+
}
211+
log.info("Process complete. '${outputFile.absolutePath}'.")
212+
}
213+
144214
private fun printResults(urls: List<String>, isTimeRange: Boolean = false) {
145-
log.info("\n--- Results ---")
215+
log.info("--- Results ---")
146216
if (urls.isNotEmpty()) {
147217
log.info("Successfully found ${urls.size} changelist files:")
148218
urls.forEach { log.info(it) }
@@ -151,6 +221,7 @@ class DiffCI {
151221
log.info("No changelist files were found for the specified $rangeType.")
152222
}
153223
log.info("---------------")
224+
fetchChangeList(urls)
154225
}
155226

156227
private fun parseDate(dateStr: String): LocalDate? {
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package cfig.lazybox.staging
2+
3+
import java.io.File
4+
import java.io.ByteArrayOutputStream
5+
import org.apache.commons.exec.CommandLine
6+
import org.apache.commons.exec.DefaultExecutor
7+
import org.apache.commons.exec.ExecuteWatchdog
8+
import org.apache.commons.exec.PumpStreamHandler
9+
import org.slf4j.LoggerFactory
10+
11+
class RepoWorker {
12+
private val log = LoggerFactory.getLogger(RepoWorker::class.simpleName)
13+
fun lfsPullRepo(args: Array<String>) {
14+
val startPath = args.firstOrNull() ?: "."
15+
val dirList = getMatchRepositories(startPath, ::isLfsEnabled, "LFS Enabled")
16+
log.warn("Found ${dirList.size} repositories with LFS enabled.")
17+
dirList.forEach { repoDir ->
18+
val relativePath = repoDir.toRelativeString(File(startPath).canonicalFile).ifEmpty { "." }
19+
log.info("Pulling [$relativePath]...")
20+
pullLfsContent(repoDir)
21+
}
22+
log.info("✨ Scan and pull complete.")
23+
}
24+
25+
fun unshallowRepo(args: Array<String>) {
26+
val startPath = args.firstOrNull() ?: "."
27+
val dirList = getMatchRepositories(startPath, ::isShallowClone, "Shallow Clone")
28+
log.warn("Found ${dirList.size} shallow repositories.")
29+
dirList.forEach { repoDir ->
30+
val relativePath = repoDir.toRelativeString(File(startPath).canonicalFile).ifEmpty { "." }
31+
log.info("Unshallowing [$relativePath]...")
32+
unshallowGit(repoDir)
33+
}
34+
log.info("✨ Scan and unshallow complete.")
35+
}
36+
37+
private fun getMatchRepositories(
38+
startPath: String,
39+
checker: (File) -> Boolean,
40+
checkerName: String = "LFS Enabled"
41+
): List<File> {
42+
val ret = mutableListOf<File>()
43+
val startDir = File(startPath).canonicalFile
44+
log.info("🔍 Finding Git repositories using 'repo forall' in: ${startDir.absolutePath}")
45+
val gitRepositories = findGitRepositoriesWithRepo(startDir)
46+
if (gitRepositories.isEmpty()) {
47+
log.info("No Git repositories found. Make sure you are running this in a directory managed by 'repo'.")
48+
return listOf<File>()
49+
}
50+
log.info("✅ Found ${gitRepositories.size} Git repositories. Now checking for $checkerName status ...")
51+
gitRepositories.forEach { repoDir ->
52+
val relativePath = repoDir.toRelativeString(startDir).ifEmpty { "." }
53+
if (checker(repoDir)) {
54+
log.info("Checking [$relativePath]...")
55+
log.info(" -> ✅ $checkerName")
56+
ret.add(repoDir)
57+
} else {
58+
//log.info("Checking [$relativePath]...")
59+
//log.info(" -> 🔴 LFS Not Enabled.")
60+
}
61+
}
62+
return ret
63+
}
64+
65+
private fun pullLfsContent(repoDir: File) {
66+
try {
67+
val commandLine = CommandLine("git").also {
68+
it.addArgument("lfs")
69+
it.addArgument("pull")
70+
}
71+
DefaultExecutor().also {
72+
//it.watchdog = ExecuteWatchdog(600000)
73+
it.streamHandler = PumpStreamHandler(System.out, System.err)
74+
it.workingDirectory = repoDir
75+
it.setExitValue(0)
76+
it.execute(commandLine)
77+
}
78+
log.info(" -> ✅ 'git lfs pull' completed successfully.")
79+
} catch (e: Exception) {
80+
log.error(" -> ❌ 'git lfs pull' failed for ${repoDir.name}: ${e.message}")
81+
}
82+
}
83+
84+
private fun unshallowGit(repoDir: File) {
85+
try {
86+
val commandLine = CommandLine("git").also {
87+
it.addArgument("fetch")
88+
it.addArgument("--progress")
89+
it.addArgument("--unshallow")
90+
}
91+
DefaultExecutor().also {
92+
//it.watchdog = ExecuteWatchdog(180000)
93+
it.streamHandler = PumpStreamHandler(System.out, System.err)
94+
it.workingDirectory = repoDir
95+
it.setExitValue(0)
96+
it.execute(commandLine)
97+
}
98+
log.info(" -> ✅ 'git fetch --unshallow' completed successfully.")
99+
} catch (e: Exception) {
100+
log.error(" -> ❌ 'git fetch --unshallow' failed for ${repoDir.name}: ${e.message}")
101+
}
102+
}
103+
104+
private fun findGitRepositoriesWithRepo(workingDir: File): List<File> {
105+
return try {
106+
val commandLine = CommandLine("repo").also {
107+
it.addArgument("forall")
108+
it.addArgument("-c")
109+
it.addArgument("pwd")
110+
}
111+
val outputStream = ByteArrayOutputStream()
112+
DefaultExecutor().also {
113+
it.watchdog = ExecuteWatchdog(60000) // 60 seconds
114+
it.streamHandler = PumpStreamHandler(outputStream)
115+
it.workingDirectory = workingDir
116+
it.setExitValue(0)
117+
it.execute(commandLine)
118+
}
119+
val output = outputStream.toString().trim()
120+
if (output.isEmpty()) {
121+
emptyList()
122+
} else {
123+
output.split(System.lineSeparator())
124+
.map { it.trim() }
125+
.filter { it.isNotEmpty() }
126+
.map { File(it) }
127+
}
128+
} catch (e: Exception) {
129+
log.error("Error executing 'repo forall': ${e.message}")
130+
log.error("Please ensure the 'repo' tool is installed and you are in a valid repo workspace.")
131+
emptyList()
132+
}
133+
}
134+
135+
private fun isLfsEnabled(repoDir: File): Boolean {
136+
return try {
137+
val commandLine = CommandLine("git").also {
138+
it.addArgument("lfs")
139+
it.addArgument("track")
140+
}
141+
val outputStream = ByteArrayOutputStream()
142+
DefaultExecutor().also {
143+
it.watchdog = ExecuteWatchdog(5000)
144+
it.streamHandler = PumpStreamHandler(outputStream)
145+
it.workingDirectory = repoDir
146+
it.setExitValue(0)
147+
it.execute(commandLine)
148+
}
149+
val output = outputStream.toString()
150+
val lines = output.lines().filter { it.isNotBlank() }
151+
return lines.size > 1
152+
} catch (e: Exception) {
153+
false
154+
}
155+
}
156+
157+
fun isShallowClone(repoDir: File): Boolean {
158+
if (!repoDir.isDirectory) {
159+
return false
160+
}
161+
return try {
162+
val commandLine = CommandLine("git").also {
163+
it.addArgument("rev-parse")
164+
it.addArgument("--is-shallow-repository")
165+
}
166+
val outputStream = ByteArrayOutputStream()
167+
val executor = DefaultExecutor().also {
168+
it.watchdog = ExecuteWatchdog(5000)
169+
it.streamHandler = PumpStreamHandler(outputStream)
170+
it.workingDirectory = repoDir
171+
it.setExitValue(0)
172+
}
173+
executor.execute(commandLine)
174+
val output = outputStream.toString().trim()
175+
return output.equals("true", ignoreCase = true)
176+
177+
} catch (e: Exception) {
178+
false
179+
}
180+
}
181+
}

0 commit comments

Comments
 (0)