Skip to content

Commit 00d7c67

Browse files
authored
feat: detect colleagues (APP-159) (#147)
1 parent d97dd71 commit 00d7c67

File tree

2 files changed

+146
-11
lines changed

2 files changed

+146
-11
lines changed

src/main/kotlin/app/hashers/CodeLongevity.kt

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,14 @@ class CodeLine(val repo: Repository,
7373
/**
7474
* The code line's age in seconds.
7575
*/
76-
val age : Long
77-
get() = (to.commit.commitTime - from.commit.commitTime).toLong()
76+
var age : Long = 0
77+
get() {
78+
if (field == 0L) {
79+
field = (to.commit.commitTime - from.commit.commitTime).toLong()
80+
}
81+
return field
82+
}
83+
set(v) { field = v }
7884

7985
/**
8086
* The code line text.
@@ -85,9 +91,21 @@ class CodeLine(val repo: Repository,
8591
/**
8692
* Email address of the line's author.
8793
*/
88-
val email : String
94+
val authorEmail : String
8995
get() = from.commit.authorIdent.emailAddress
9096

97+
/**
98+
* Email address of the line's changer.
99+
*/
100+
val editorEmail : String?
101+
get() = if (isDeleted) to.commit.authorIdent.emailAddress else null
102+
103+
/**
104+
* A date when the line was changed.
105+
*/
106+
val editDate : Date
107+
get() = Date(to.commit.getCommitTime().toLong() * 1000)
108+
91109
/**
92110
* True if the line is deleted.
93111
*/
@@ -107,7 +125,71 @@ class CodeLine(val repo: Repository,
107125
val state = if (isDeleted) "deleted" else "alive"
108126
return "Line '$text' - '${from.file}:${from.line}' added in $fc $fd\n" +
109127
" ${revState} '${to.file}:${to.line}' in $tc $td,\n" +
110-
" age: ${age} ms - $state"
128+
" age: ${age} s - $state"
129+
}
130+
}
131+
132+
/**
133+
* Detects colleagues and their 'work vicinity' from commits.
134+
*/
135+
class Colleagues {
136+
// A map of <colleague_email1, colleague_email2> pairs to pairs of
137+
// <month, time>, which indicates to a minimum time in ms between all line
138+
// changes for these two colleagues in a given month (yyyy-mm).
139+
private val map: HashMap<Pair<String, String>,
140+
HashMap<String, Long>> = hashMapOf()
141+
142+
fun collect(line: CodeLine) {
143+
// TODO(alex): ignore same user emails
144+
val authorEmail = line.authorEmail
145+
val editorEmail = line.editorEmail
146+
if (editorEmail == null || authorEmail == editorEmail) {
147+
return
148+
}
149+
val emails =
150+
if (editorEmail < authorEmail) Pair(editorEmail, authorEmail)
151+
else Pair(authorEmail, editorEmail)
152+
153+
val dates = map.getOrPut(emails, { hashMapOf() })
154+
val month = SimpleDateFormat("yyyy-MM").format(line.editDate)
155+
156+
Logger.trace { "collected colleague, age: ${line.age}" }
157+
var vicinity = dates.getOrPut(month, { line.age })
158+
if (vicinity > line.age) {
159+
dates.put(month, line.age)
160+
}
161+
}
162+
163+
fun updateStats() {
164+
// TODO(alex): report the stats to the server
165+
if (Logger.isDebug) {
166+
map.forEach( { (email1, email2), dates ->
167+
Logger.debug { "<$email1> <$email2>" }
168+
dates.forEach { month, vicinity ->
169+
Logger.debug { " $month: $vicinity ms" }
170+
}
171+
} )
172+
}
173+
}
174+
175+
/**
176+
* Return colleagues in a form of <email, month, time> for the given
177+
* email, where time indicates a minimal time in ms between all line edits
178+
* by these colleagues in a given month (yyyy-mm).
179+
*/
180+
fun get(email: String) : List<Triple<String, String, Long>> {
181+
return map
182+
.filter { (pair, _) -> pair.first == email || pair.second == email }
183+
.flatMap { (pair, dates) ->
184+
val colleagueEmail =
185+
if (email == pair.first) pair.second else pair.first
186+
187+
var list = mutableListOf<Triple<String, String, Long>>()
188+
dates.forEach { month, vicinity ->
189+
list.add(Triple(colleagueEmail, month, vicinity))
190+
}
191+
return list
192+
}
111193
}
112194
}
113195

@@ -152,6 +234,7 @@ class CodeLongevity(private val serverRepo: Repo,
152234

153235
val df = DiffFormatter(DisabledOutputStream.INSTANCE)
154236
val dataPath = FileHelper.getPath(serverRepo.rehash, "longevity")
237+
val colleagues = Colleagues()
155238

156239
init {
157240
df.setRepository(repo)
@@ -212,6 +295,8 @@ class CodeLongevity(private val serverRepo: Repo,
212295
api.postFacts(stats).onErrorThrow()
213296
Logger.info { "Sent ${stats.size} facts to server" }
214297
}
298+
299+
colleagues.updateStats();
215300
}
216301

217302
/**
@@ -244,24 +329,23 @@ class CodeLongevity(private val serverRepo: Repo,
244329

245330
// Update ages.
246331
getLinesObservable(storedHead).blockingSubscribe { line ->
247-
Logger.trace { "Scanning: ${line}" }
248-
if (line.to.isDeleted) {
249-
var age = line.age
332+
if (line.isDeleted) {
250333
if (ageData.lastingLines.contains(line.oldId)) {
251-
age += ageData.lastingLines.remove(line.oldId)!!.age
334+
line.age += ageData.lastingLines.remove(line.oldId)!!.age
252335
}
253-
val aggrAge = ageData.aggrAges.getOrPut(line.email,
336+
val aggrAge = ageData.aggrAges.getOrPut(line.authorEmail,
254337
{ CodeLineAges.AggrAge() } )
255-
aggrAge.sum += age
338+
aggrAge.sum += line.age
256339
aggrAge.count += 1
257340

341+
colleagues.collect(line);
258342
} else {
259343
var age = line.age
260344
if (ageData.lastingLines.contains(line.oldId)) {
261345
age += ageData.lastingLines.remove(line.oldId)!!.age
262346
}
263347
ageData.lastingLines.put(line.newId,
264-
CodeLineAges.LineInfo(age, line.email))
348+
CodeLineAges.LineInfo(age, line.authorEmail))
265349
}
266350
}
267351

src/test/kotlin/test/tests/hashers/CodeLongevityTest.kt

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,4 +542,55 @@ class CodeLongevityTest : Spek({
542542
testRepo.destroy()
543543
}
544544
}
545+
546+
given("'colleagues #1'") {
547+
val testRepoPath = "../CodeLongevity_colleagues1"
548+
val testRepo = TestRepo(testRepoPath)
549+
val testRehash = "rehash_colleagues1"
550+
val fileName = "test1.txt"
551+
val author1 = Author(testRepo.userName, testRepo.userEmail)
552+
val author2 = Author("Vasya Pupkin", "[email protected]")
553+
val emails = hashSetOf(author1.email, author2.email)
554+
555+
var serverRepo = Repo(rehash = testRehash)
556+
val mockApi = MockApi(mockRepo = serverRepo)
557+
558+
testRepo.createFile(fileName, listOf("line1", "line2"))
559+
testRepo.commit(message = "initial commit",
560+
author = author1,
561+
date = Calendar.Builder().setTimeOfDay(0, 0, 0).build().time)
562+
563+
testRepo.deleteLines(fileName, 1, 1)
564+
testRepo.commit(message = "delete line",
565+
author = author1,
566+
date = Calendar.Builder().setTimeOfDay(0, 1, 0).build().time)
567+
568+
testRepo.deleteLines(fileName, 0, 0)
569+
testRepo.commit(message = "delete line #2",
570+
author = author2,
571+
date = Calendar.Builder().setTimeOfDay(0, 1, 0).build().time)
572+
573+
val cl = CodeLongevity(serverRepo, emails, testRepo.git,
574+
{ _ -> fail("exception") })
575+
cl.updateStats(mockApi)
576+
577+
it("'t1'") {
578+
val triple1 = cl.colleagues.get(author1.email).get(0)
579+
assertEquals(triple1.first, author2.email, "Wrong colleague email #1")
580+
assertEquals(triple1.second, "1970-01", "Wrong colleague month #1")
581+
assertEquals(triple1.third, 60, "Wrong colleague vicinity #1")
582+
583+
val triple2 = cl.colleagues.get(author2.email).get(0)
584+
assertEquals(triple2.first, author1.email, "Wrong colleague email #1")
585+
assertEquals(triple2.second, "1970-01", "Wrong colleague month #1")
586+
assertEquals(triple2.third, 60, "Wrong colleague vicinity #1")
587+
}
588+
589+
afterGroup {
590+
CodeLongevity(
591+
Repo(rehash = testRehash), emails, testRepo.git,
592+
{ _ -> fail("exception") }).dropSavedData()
593+
testRepo.destroy()
594+
}
595+
}
545596
})

0 commit comments

Comments
 (0)