Skip to content

Commit 7757b6a

Browse files
committed
feat: add date fun facts (APP-61, APP-49) (#32)
* feat: add day of week and time of day fun facts (APP-49, APP-61) * wip: add account of time zones, switch to long type for timestamp * feat: test for date facts, add date to commit in TestRepo * fix: add FactKey for all key names, fix PR requests * chore: add year to test
1 parent 50a5f01 commit 7757b6a

File tree

8 files changed

+168
-29
lines changed

8 files changed

+168
-29
lines changed

src/main/kotlin/app/FactKey.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package app
2+
3+
object FactKey {
4+
val COMMITS_DAY_WEEK = "commits-day-week-"
5+
val COMMITS_DAY_TIME = "commits-day-time-"
6+
val LINE_LONGEVITY = "line-longevity-avg"
7+
val LINE_LONGEVITY_REPO = "line-longevity-repo-avg"
8+
}

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
package app.hashers
55

6+
import app.FactKey
67
import app.Logger
78
import app.api.Api
89
import app.config.Configurator
@@ -80,9 +81,6 @@ class CodeLongevity(private val localRepo: LocalRepo,
8081
private val serverRepo: Repo,
8182
private val api: Api,
8283
private val git: Git, tailRev: String = "") {
83-
val CODE_LONGEVITY_KEY = "line-longevity-avg"
84-
val CODE_LONGEVITY_REPO_KEY = "line-longevity-repo-avg"
85-
8684
val repo: Repository = git.repository
8785
val head: RevCommit =
8886
RevWalk(repo).parseCommit(repo.resolve(RepoHelper.MASTER_BRANCH))
@@ -121,7 +119,7 @@ class CodeLongevity(private val localRepo: LocalRepo,
121119
val repoAvg = if (repoTotal > 0) { repoSum / repoTotal } else 0
122120
val stats = mutableListOf<Fact>()
123121
stats.add(Fact(repo = serverRepo,
124-
key = CODE_LONGEVITY_REPO_KEY,
122+
key = FactKey.LINE_LONGEVITY_REPO,
125123
value = repoAvg.toDouble()))
126124
val repoAvgDays = repoAvg / secondsInDay
127125
Logger.info("Repo average code line age is $repoAvgDays days, "
@@ -131,7 +129,7 @@ class CodeLongevity(private val localRepo: LocalRepo,
131129
val total = totals[email] ?: 0
132130
val avg = if (total > 0) { sums[email]!! / total } else 0
133131
stats.add(Fact(repo = serverRepo,
134-
key = CODE_LONGEVITY_KEY,
132+
key = FactKey.LINE_LONGEVITY,
135133
value = avg.toDouble(),
136134
author = Author(email = email)))
137135
if (email == localRepo.author.email) {

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

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33

44
package app.hashers
55

6+
import app.FactKey
67
import app.Logger
78
import app.api.Api
89
import app.extractors.Extractor
10+
import app.model.Author
911
import app.model.Commit
1012
import app.model.DiffContent
1113
import app.model.DiffFile
1214
import app.model.DiffRange
15+
import app.model.Fact
1316
import app.model.LocalRepo
1417
import app.model.Repo
1518
import app.utils.RepoHelper
@@ -26,6 +29,8 @@ import org.eclipse.jgit.lib.ObjectId
2629
import org.eclipse.jgit.errors.MissingObjectException
2730
import org.eclipse.jgit.revwalk.RevCommit
2831
import org.eclipse.jgit.util.io.DisabledOutputStream
32+
import java.time.LocalDateTime
33+
import java.time.ZoneOffset
2934
import java.util.concurrent.TimeUnit
3035

3136
/**
@@ -35,32 +40,52 @@ class CommitHasher(private val localRepo: LocalRepo,
3540
private val repo: Repo = Repo(),
3641
private val api: Api,
3742
private val git: Git) {
43+
3844
private val gitRepo: Repository = git.repository
3945

4046
private fun findFirstOverlappingCommit(): Commit? {
4147
val serverHistoryCommits = repo.commits.toHashSet()
42-
return getObservableCommits()
48+
return getCommitsAsObservable()
4349
.skipWhile { commit -> !serverHistoryCommits.contains(commit) }
4450
.blockingFirst(null)
4551
}
4652

4753
private fun hashAndSendCommits() {
4854
val lastKnownCommit = repo.commits.lastOrNull()
4955
val knownCommits = repo.commits.toHashSet()
56+
57+
val factsDayWeek = hashMapOf<Author, Array<Int>>()
58+
val factsDayTime = hashMapOf<Author, Array<Int>>()
59+
5060
// Commits are combined in pairs, an empty commit concatenated to
5161
// calculate the diff of the initial commit.
52-
Observable.concat(getObservableCommits(), Observable.just(Commit()))
62+
Observable.concat(getCommitsAsObservable()
63+
.doOnNext { commit ->
64+
Logger.debug("Commit: ${commit.raw?.name ?: ""}: "
65+
+ commit.raw?.shortMessage)
66+
commit.repo = repo
67+
68+
// Calculate facts.
69+
val author = commit.author
70+
val factDayWeek = factsDayWeek[author] ?: Array(7) { 0 }
71+
val factDayTime = factsDayTime[author] ?: Array(24) { 0 }
72+
val timestamp = commit.dateTimestamp
73+
val dateTime = LocalDateTime.ofEpochSecond(timestamp, 0,
74+
ZoneOffset.ofTotalSeconds(commit.dateTimeZoneOffset * 60))
75+
// The value is numbered from 1 (Monday) to 7 (Sunday).
76+
factDayWeek[dateTime.dayOfWeek.value - 1] += 1
77+
// Hour from 0 to 23.
78+
factDayTime[dateTime.hour] += 1
79+
factsDayWeek[author] = factDayWeek
80+
factsDayTime[author] = factDayTime
81+
}, Observable.just(Commit()))
5382
.pairWithNext() // Pair commits to get diff.
5483
.takeWhile { (new, _) -> // Hash until last known commit.
5584
new.rehash != lastKnownCommit?.rehash }
5685
.filter { (new, _) -> knownCommits.isEmpty() // Don't hash known.
5786
|| !knownCommits.contains(new) }
5887
.filter { (new, _) -> emailFilter(new) } // Email filtering.
5988
.map { (new, old) -> // Mapping and stats extraction.
60-
Logger.debug("Commit: ${new.raw?.name ?: ""}: "
61-
+ new.raw?.shortMessage)
62-
new.repo = repo
63-
6489
val diffFiles = getDiffFiles(new, old)
6590
Logger.debug("Diff: ${diffFiles.size} entries")
6691
new.stats = Extractor().extract(diffFiles)
@@ -73,18 +98,33 @@ class CommitHasher(private val localRepo: LocalRepo,
7398
total + file.getAllAdded().size }
7499
new.numLinesDeleted = diffFiles.fold(0) { total, file ->
75100
total + file.getAllDeleted().size }
76-
77101
new
78102
}
79103
.observeOn(Schedulers.io()) // Different thread for data sending.
80104
.buffer(20, TimeUnit.SECONDS) // Group ready commits by time.
81-
.doOnNext { commitsBundle -> // Send ready commits.
82-
postCommitsToServer(commitsBundle) }
83-
.blockingSubscribe({
84-
// OnNext
85-
}, { t -> // OnError
86-
Logger.error("Error while hashing: $t")
87-
t.printStackTrace()
105+
.blockingSubscribe({ commitsBundle -> // OnNext.
106+
postCommitsToServer(commitsBundle) // Send ready commits.
107+
}, { e -> // OnError.
108+
Logger.error("Error while hashing: $e")
109+
}, { // OnComplete.
110+
val facts = mutableListOf<Fact>()
111+
factsDayTime.map { (author, list) ->
112+
list.forEachIndexed { hour, count ->
113+
if (count > 0) {
114+
facts.add(Fact(repo, FactKey.COMMITS_DAY_TIME +
115+
hour, count.toDouble(), author))
116+
}
117+
}
118+
}
119+
factsDayWeek.map { (author, list) ->
120+
list.forEachIndexed { day, count ->
121+
if (count > 0) {
122+
facts.add(Fact(repo, FactKey.COMMITS_DAY_WEEK +
123+
day, count.toDouble(), author))
124+
}
125+
}
126+
}
127+
postFactsToServer(facts)
88128
})
89129
}
90130

@@ -142,14 +182,21 @@ class CommitHasher(private val localRepo: LocalRepo,
142182
}
143183
}
144184

185+
private fun postFactsToServer(facts: List<Fact>) {
186+
if (facts.isNotEmpty()) {
187+
api.postFacts(facts)
188+
Logger.debug("Sent ${facts.size} facts to server")
189+
}
190+
}
191+
145192
private fun deleteCommitsOnServer(commits: List<Commit>) {
146193
if (commits.isNotEmpty()) {
147194
api.deleteCommits(commits)
148195
Logger.debug("Sent ${commits.size} deleted commits to server")
149196
}
150197
}
151198

152-
private fun getObservableCommits(): Observable<Commit> =
199+
private fun getCommitsAsObservable(): Observable<Commit> =
153200
Observable.create { subscriber ->
154201
try {
155202
val revWalk = RevWalk(gitRepo)

src/main/kotlin/app/model/Commit.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ data class Commit(
1818
// Tree rehash used for adjustments of stats due to rebase and fraud.
1919
var treeRehash: String = "",
2020
var author: Author = Author(),
21-
var dateTimestamp: Int = 0,
21+
var dateTimestamp: Long = 0,
22+
var dateTimeZoneOffset: Int = 0,
2223
var isQommit: Boolean = false,
2324
var numLinesAdded: Int = 0,
2425
var numLinesDeleted: Int = 0,
@@ -33,7 +34,8 @@ data class Commit(
3334
rehash = DigestUtils.sha256Hex(revCommit.id.name)
3435
author = Author(revCommit.authorIdent.name,
3536
revCommit.authorIdent.emailAddress)
36-
dateTimestamp = revCommit.commitTime
37+
dateTimestamp = revCommit.authorIdent.getWhen().time / 1000
38+
dateTimeZoneOffset = revCommit.authorIdent.timeZoneOffset
3739
treeRehash = DigestUtils.sha256Hex(revCommit.tree.name)
3840
}
3941

src/main/proto/sourcerer.proto

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ message Commit {
2424
string author_email = 5;
2525

2626
// Timestamp of a commit creation in seconds UTC+0.
27-
uint32 date = 6;
27+
uint64 date = 6;
2828

2929
// Is quality commit.
3030
bool is_qommit = 7;

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import app.model.*
1212
import test.utils.TestRepo
1313

1414
import kotlin.test.assertEquals
15-
import kotlin.test.assertNotEquals
1615

1716
import org.eclipse.jgit.revwalk.RevCommit
1817

@@ -60,7 +59,7 @@ class CodeLongevityTest : Spek({
6059
val fileName = "test1.txt"
6160

6261
// t1: initial insertion
63-
testRepo.newFile(fileName, listOf("line1", "line2"))
62+
testRepo.createFile(fileName, listOf("line1", "line2"))
6463
val rev1 = testRepo.commit("inital commit")
6564
val lines1 = CodeLongevity(
6665
LocalRepo(testRepoPath), Repo(), MockApi(), testRepo.git).compute()
@@ -177,7 +176,7 @@ class CodeLongevityTest : Spek({
177176
"line17",
178177
"line18"
179178
)
180-
testRepo.newFile(fileName, fileContent)
179+
testRepo.createFile(fileName, fileContent)
181180
val rev1 = testRepo.commit("inital commit")
182181
val lines1 = CodeLongevity(
183182
LocalRepo(testRepoPath), Repo(), MockApi(), testRepo.git).compute()

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

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package test.tests.hashers
66

7+
import app.FactKey
78
import app.api.MockApi
89
import app.hashers.CommitHasher
910
import app.model.*
@@ -12,11 +13,14 @@ import org.eclipse.jgit.api.Git
1213
import org.jetbrains.spek.api.Spek
1314
import org.jetbrains.spek.api.dsl.given
1415
import org.jetbrains.spek.api.dsl.it
16+
import test.utils.TestRepo
1517
import java.io.File
18+
import java.util.*
1619
import java.util.stream.StreamSupport.stream
1720
import kotlin.streams.toList
1821
import kotlin.test.assertEquals
1922
import kotlin.test.assertNotEquals
23+
import kotlin.test.assertTrue
2024

2125
class CommitHasherTest : Spek({
2226
val userName = "Contributor"
@@ -56,6 +60,14 @@ class CommitHasherTest : Spek({
5660
return lastCommit
5761
}
5862

63+
fun createDate(year: Int = 2017, month: Int = 1, day: Int = 1,
64+
hour: Int = 0, minute: Int = 0, seconds: Int = 0): Date {
65+
val cal = Calendar.getInstance()
66+
// Month in calendar is 0-based.
67+
cal.set(year, month - 1, day, hour, minute, seconds)
68+
return cal.time
69+
}
70+
5971
given("repo with initial commit and no history") {
6072
repo.commits = listOf()
6173

@@ -199,5 +211,69 @@ class CommitHasherTest : Spek({
199211
}
200212
}
201213

214+
given("test of commits date facts") {
215+
val testRepoPath = "../testrepo1"
216+
val testRepo = TestRepo(testRepoPath)
217+
val author1 = Author("Test1", "[email protected]")
218+
val author2 = Author("Test2", "[email protected]")
219+
220+
val repo = Repo(rehash = "rehash", commits = listOf())
221+
val mockApi = MockApi(mockRepo = repo)
222+
val facts = mockApi.receivedFacts
223+
224+
afterEachTest {
225+
facts.clear()
226+
}
227+
228+
it("send two facts") {
229+
testRepo.createFile("test1.txt", listOf("line1", "line2"))
230+
testRepo.commit(message = "initial commit",
231+
author = author1,
232+
// Sunday.
233+
date = createDate(year = 2017, month = 1, day = 1,
234+
hour = 13, minute = 0, seconds = 0))
235+
236+
CommitHasher(localRepo, repo, mockApi, testRepo.git).update()
237+
238+
assertEquals(2, facts.size)
239+
assertTrue(facts.contains(Fact(repo, FactKey.COMMITS_DAY_TIME + 13,
240+
1.0, author1)))
241+
assertTrue(facts.contains(Fact(repo, FactKey.COMMITS_DAY_WEEK + 6,
242+
1.0, author1)))
243+
}
244+
245+
it("send more facts") {
246+
testRepo.createFile("test2.txt", listOf("line1", "line2"))
247+
testRepo.commit(message = "second commit",
248+
author = author2,
249+
// Monday.
250+
date = createDate(year=2017, month = 1, day = 2,
251+
hour = 18, minute = 0, seconds = 0))
252+
253+
testRepo.createFile("test3.txt", listOf("line1", "line2"))
254+
testRepo.commit(message = "third commit",
255+
author = author1,
256+
// Monday.
257+
date = createDate(month = 1, day = 2,
258+
hour = 13, minute = 0, seconds = 0))
259+
260+
CommitHasher(localRepo, repo, mockApi, testRepo.git).update()
261+
262+
assertEquals(5, facts.size)
263+
assertTrue(facts.contains(Fact(repo, FactKey.COMMITS_DAY_TIME + 18,
264+
1.0, author2)))
265+
assertTrue(facts.contains(Fact(repo, FactKey.COMMITS_DAY_WEEK + 0,
266+
1.0, author2)))
267+
assertTrue(facts.contains(Fact(repo, FactKey.COMMITS_DAY_TIME + 13,
268+
2.0, author1)))
269+
assertTrue(facts.contains(Fact(repo, FactKey.COMMITS_DAY_WEEK + 0,
270+
1.0, author1)))
271+
}
272+
273+
afterGroup {
274+
testRepo.destroy()
275+
}
276+
}
277+
202278
Runtime.getRuntime().exec("src/test/delete_repo.sh").waitFor()
203279
})

0 commit comments

Comments
 (0)