diff --git a/src/main/kotlin/app/api/Api.kt b/src/main/kotlin/app/api/Api.kt index b42f9a5b..ea33d98e 100644 --- a/src/main/kotlin/app/api/Api.kt +++ b/src/main/kotlin/app/api/Api.kt @@ -16,8 +16,9 @@ interface Api { fun authorize(): Result fun getUser(): Result - fun getRepo(repoRehash: String): Result - fun postRepo(repo: Repo): Result + fun postUser(user: User): Result + fun postRepo(repo: Repo): Result + fun postComplete(): Result fun postCommits(commitsList: List): Result fun deleteCommits(commitsList: List): Result fun postFacts(factsList: List): Result diff --git a/src/main/kotlin/app/api/MockApi.kt b/src/main/kotlin/app/api/MockApi.kt index 042e7aa5..667d38ad 100644 --- a/src/main/kotlin/app/api/MockApi.kt +++ b/src/main/kotlin/app/api/MockApi.kt @@ -19,6 +19,8 @@ class MockApi( // GET requests. var receivedAddedCommits: MutableList = mutableListOf() var receivedFacts: MutableList = mutableListOf() var receivedAuthors: MutableList = mutableListOf() + var receivedUsers: MutableList = mutableListOf() + var receivedComplete: Int = 0 // DELETE requests. var receivedDeletedCommits: MutableList = mutableListOf() @@ -33,14 +35,21 @@ class MockApi( // GET requests. return Result(mockUser) } - override fun getRepo(repoRehash: String): Result { - Logger.debug { "MockApi: getRepo request" } - return Result(mockRepo) + override fun postUser(user: User): Result { + Logger.debug { "MockApi: postUser request" } + receivedUsers.add(user) + return Result() } - override fun postRepo(repo: Repo): Result { - Logger.debug { "MockApi: postRepo request ($repo)" } + override fun postRepo(repo: Repo): Result { + Logger.debug { "MockApi: postRepo request" } receivedRepos.add(repo) + return Result(mockRepo) + } + + override fun postComplete(): Result { + Logger.debug { "MockApi: postComplete request " } + receivedComplete++ return Result() } diff --git a/src/main/kotlin/app/api/ServerApi.kt b/src/main/kotlin/app/api/ServerApi.kt index a7a03d85..25622c7e 100644 --- a/src/main/kotlin/app/api/ServerApi.kt +++ b/src/main/kotlin/app/api/ServerApi.kt @@ -76,15 +76,16 @@ class ServerApi (private val configurator: Configurator) : Api { private fun createRequestGetToken(): Request { return post("/auth").authenticate(username, password) - .header(getVersionCodeHeader()) + .header(getVersionCodeHeader()) } private fun createRequestGetUser(): Request { return get("/user") } - private fun createRequestGetRepo(repoRehash: String): Request { - return get("/repo/$repoRehash") + private fun createRequestPostUser(user: User): Request { + return post("/user").header(getContentTypeHeader()) + .body(user.serialize()) } private fun createRequestPostRepo(repo: Repo): Request { @@ -92,6 +93,10 @@ class ServerApi (private val configurator: Configurator) : Api { .body(repo.serialize()) } + private fun createRequestPostComplete(): Request { + return post("/complete").header(getContentTypeHeader()) + } + private fun createRequestPostCommits(commits: CommitGroup): Request { return post("/commits").header(getContentTypeHeader()) .body(commits.serialize()) @@ -154,18 +159,22 @@ class ServerApi (private val configurator: Configurator) : Api { { body -> User(body) }) } - override fun getRepo(repoRehash: String): Result { - if (repoRehash.isBlank()) { + override fun postUser(user: User): Result { + return makeRequest(createRequestPostUser(user), "postUser", {}) + } + + override fun postRepo(repo: Repo): Result { + if (repo.rehash.isBlank()) { throw IllegalArgumentException() } - return makeRequest(createRequestGetRepo(repoRehash), "getRepo", + return makeRequest(createRequestPostRepo(repo), "getRepo", { body -> Repo(body) }) } - override fun postRepo(repo: Repo): Result { - return makeRequest(createRequestPostRepo(repo), - "postRepo", {}) + override fun postComplete(): Result { + return makeRequest(createRequestPostComplete(), + "postComplete", {}) } override fun postCommits(commitsList: List): Result { diff --git a/src/main/kotlin/app/config/Configurator.kt b/src/main/kotlin/app/config/Configurator.kt index 0cab1036..c9960ed0 100644 --- a/src/main/kotlin/app/config/Configurator.kt +++ b/src/main/kotlin/app/config/Configurator.kt @@ -4,7 +4,7 @@ package app.config import app.model.LocalRepo -import app.model.Repo +import app.model.User import app.utils.Options interface Configurator { @@ -13,7 +13,7 @@ interface Configurator { fun getPassword(): String fun isValidCredentials(): Boolean fun getLocalRepos(): List - fun getRepos(): List + fun getUser(): User fun setUsernameCurrent(username: String) fun setPasswordCurrent(password: String) fun getUuidPersistent(): String @@ -21,7 +21,7 @@ interface Configurator { fun setPasswordPersistent(password: String) fun addLocalRepoPersistent(localRepo: LocalRepo) fun removeLocalRepoPersistent(localRepo: LocalRepo) - fun setRepos(repos: List) + fun setUser(user: User) fun isFirstLaunch(): Boolean fun loadFromFile() fun saveToFile() diff --git a/src/main/kotlin/app/config/FileConfigurator.kt b/src/main/kotlin/app/config/FileConfigurator.kt index c5577ca5..fa2b48e3 100644 --- a/src/main/kotlin/app/config/FileConfigurator.kt +++ b/src/main/kotlin/app/config/FileConfigurator.kt @@ -6,6 +6,7 @@ package app.config import app.Logger import app.model.LocalRepo import app.model.Repo +import app.model.User import app.utils.Options import app.utils.PasswordHelper import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility @@ -62,7 +63,7 @@ class FileConfigurator : Configurator { /** * Used to temporarily save list of repos that known by server. */ - private var repos: List = listOf() + private var user: User = User() /** * User directory path is where persistent config stored. @@ -143,10 +144,10 @@ class FileConfigurator : Configurator { } /** - * Gets list of temprorary saved repos. + * Gets temprorary saved user information. */ - override fun getRepos(): List { - return repos + override fun getUser(): User { + return user } /** @@ -199,10 +200,10 @@ class FileConfigurator : Configurator { } /** - * Temporarily sets list of repos. + * Temporarily sets info about user. */ - override fun setRepos(repos: List) { - this.repos = repos + override fun setUser(user: User) { + this.user = user } /** diff --git a/src/main/kotlin/app/config/MockConfigurator.kt b/src/main/kotlin/app/config/MockConfigurator.kt index 1b482c23..75c30bbf 100644 --- a/src/main/kotlin/app/config/MockConfigurator.kt +++ b/src/main/kotlin/app/config/MockConfigurator.kt @@ -4,14 +4,14 @@ package app.config import app.model.LocalRepo -import app.model.Repo +import app.model.User import app.utils.Options class MockConfigurator(var mockUsername: String = "", var mockPassword: String = "", var mockIsValidCredentials: Boolean = true, var mockIsFirstLaunch: Boolean = true, - var mockRepos: MutableList = mutableListOf(), + var mockUser: User = User(), var mockLocalRepos: MutableList = mutableListOf(), var uuid: String = "") : Configurator { @@ -39,8 +39,8 @@ class MockConfigurator(var mockUsername: String = "", return mockLocalRepos } - override fun getRepos(): List { - return mockRepos + override fun getUser(): User { + return mockUser } override fun setUsernameCurrent(username: String) { @@ -72,8 +72,8 @@ class MockConfigurator(var mockUsername: String = "", mockPersistent.localRepos.remove(localRepo) } - override fun setRepos(repos: List) { - mockRepos = repos.toMutableList() + override fun setUser(user: User) { + mockUser = user } override fun isFirstLaunch(): Boolean { diff --git a/src/main/kotlin/app/hashers/RepoHasher.kt b/src/main/kotlin/app/hashers/RepoHasher.kt index d38b3a85..2f6b0f0b 100644 --- a/src/main/kotlin/app/hashers/RepoHasher.kt +++ b/src/main/kotlin/app/hashers/RepoHasher.kt @@ -38,26 +38,18 @@ class RepoHasher(private val localRepo: LocalRepo, private val api: Api, val (rehashes, emails) = fetchRehashesAndEmails(git) localRepo.parseGitConfig(git.repository.config) - if (localRepo.author.email.isBlank()) { - throw IllegalStateException("Can't load email from Git config") - } - initServerRepo(rehashes.last) Logger.debug { "Local repo path: ${localRepo.path}" } + Logger.debug { "Repo remote: ${localRepo.remoteOrigin}" } Logger.debug { "Repo rehash: ${serverRepo.rehash}" } - val filteredEmails = filterEmails(emails) - - if (!isKnownRepo()) { - // Notify server about new contributor and his email. - postRepoToServer() - } + // Get repo setup (commits, emails to hash) from server. + postRepoFromServer() // Send all repo emails for invites. postAuthorsToServer(emails) - // Get repo setup (commits, emails to hash) from server. - getRepoFromServer() + val filteredEmails = filterEmails(emails) // Common error handling for subscribers. // Exceptions can't be thrown out of reactive chain. @@ -87,9 +79,6 @@ class RepoHasher(private val localRepo: LocalRepo, private val api: Api, onError(e) } - // Confirm hashing completion. - postRepoToServer() - if (errors.isNotEmpty()) { throw HashingException(errors) } @@ -116,26 +105,15 @@ class RepoHasher(private val localRepo: LocalRepo, private val api: Api, git.close() } - private fun isKnownRepo(): Boolean { - return configurator.getRepos() - .find { it.rehash == serverRepo.rehash } != null - } - - private fun getRepoFromServer() { - val repo = api.getRepo(serverRepo.rehash).getOrThrow() + private fun postRepoFromServer() { + val repo = api.postRepo(serverRepo).getOrThrow() serverRepo.commits = repo.commits - Logger.info{ + Logger.info { "Received repo from server with ${serverRepo.commits.size} commits" } Logger.debug { serverRepo.toString() } } - private fun postRepoToServer() { - serverRepo.commits = listOf() - api.postRepo(serverRepo).onErrorThrow() - Logger.debug { serverRepo.toString() } - } - private fun postAuthorsToServer(emails: HashSet) { api.postAuthors(emails.map { email -> Author(email=email, repo=serverRepo) @@ -143,10 +121,9 @@ class RepoHasher(private val localRepo: LocalRepo, private val api: Api, } private fun initServerRepo(initCommitRehash: String) { - serverRepo = Repo(userEmail = localRepo.author.email) - serverRepo.initialCommitRehash = initCommitRehash - serverRepo.rehash = RepoHelper.calculateRepoRehash( - serverRepo.initialCommitRehash, localRepo) + serverRepo = Repo(initialCommitRehash = initCommitRehash, + rehash = RepoHelper.calculateRepoRehash( + initCommitRehash, localRepo)) } private fun fetchRehashesAndEmails(git: Git): @@ -178,8 +155,7 @@ class RepoHasher(private val localRepo: LocalRepo, private val api: Api, } val knownEmails = hashSetOf() - knownEmails.add(localRepo.author.email) - knownEmails.add(serverRepo.userEmail) + knownEmails.addAll(configurator.getUser().emails.map { it.email }) knownEmails.addAll(serverRepo.emails) return knownEmails.filter { emails.contains(it) }.toHashSet() diff --git a/src/main/kotlin/app/model/Repo.kt b/src/main/kotlin/app/model/Repo.kt index 62e35562..b609d23b 100644 --- a/src/main/kotlin/app/model/Repo.kt +++ b/src/main/kotlin/app/model/Repo.kt @@ -15,8 +15,6 @@ data class Repo( var rehash: String = "", var initialCommitRehash: String = "", - var userEmail: String = "", - // Authors' email filter for hashed commits. If empty list then hash // only commits that created by current user. var emails: List = listOf(), @@ -28,7 +26,6 @@ data class Repo( constructor(proto: Protos.Repo) : this() { rehash = proto.rehash initialCommitRehash = proto.initialCommitRehash - userEmail = proto.userEmail emails = proto.emailsList commits = proto.commitsList.map { Commit(it) } } @@ -42,7 +39,6 @@ data class Repo( return Protos.Repo.newBuilder() .setRehash(rehash) .setInitialCommitRehash(rehash) - .setUserEmail(userEmail) .addAllEmails(emails) .addAllCommits(commits.map { it.getProto() }) .build() diff --git a/src/main/kotlin/app/model/User.kt b/src/main/kotlin/app/model/User.kt index c9181490..78f731bc 100644 --- a/src/main/kotlin/app/model/User.kt +++ b/src/main/kotlin/app/model/User.kt @@ -11,12 +11,14 @@ import java.security.InvalidParameterException * User information. */ data class User ( - var repos: MutableList = mutableListOf() + var repos: MutableList = mutableListOf(), + var emails: HashSet = hashSetOf() ) { @Throws(InvalidParameterException::class) constructor(proto: Protos.User) : this() { repos = proto.reposList.map { repo -> Repo(repo) } - .toMutableList() + .toMutableList() + emails = proto.emailsList.map { email -> UserEmail(email) }.toHashSet() } @Throws(InvalidProtocolBufferException::class) @@ -26,8 +28,9 @@ data class User ( fun getProto(): Protos.User { return Protos.User.newBuilder() - .addAllRepos(repos.map { repo -> repo.getProto() }) - .build() + .addAllRepos(repos.map { repo -> repo.getProto() }) + .addAllEmails(emails.map { email -> email.getProto() }) + .build() } fun serialize(): ByteArray { diff --git a/src/main/kotlin/app/model/UserEmail.kt b/src/main/kotlin/app/model/UserEmail.kt new file mode 100644 index 00000000..3dd4f964 --- /dev/null +++ b/src/main/kotlin/app/model/UserEmail.kt @@ -0,0 +1,58 @@ +// Copyright 2017 Sourcerer Inc. All Rights Reserved. +// Author: Anatoly Kislov (anatoly@sourcerer.io) + +package app.model + +import app.Protos +import com.google.protobuf.InvalidProtocolBufferException +import java.security.InvalidParameterException + +/** + * User information. + */ +class UserEmail( + var email: String = "", + var primary: Boolean = false, + var verified: Boolean = false +) { + @Throws(InvalidParameterException::class) + constructor(proto: Protos.UserEmail) : this() { + email = proto.email + primary = proto.primary + verified = proto.verified + } + + @Throws(InvalidProtocolBufferException::class) + constructor(bytes: ByteArray) : this(Protos.UserEmail.parseFrom(bytes)) + + constructor(serialized: String) : this(serialized.toByteArray()) + + fun getProto(): Protos.UserEmail { + return Protos.UserEmail.newBuilder() + .setEmail(email) + .setPrimary(primary) + .setVerified(verified) + .build() + } + + fun serialize(): ByteArray { + return getProto().toByteArray() + } + + override fun toString(): String { + val primary = if (this.primary) " (Primary)" else "" + val verified = if (this.verified) "Verified" else "Unverified" + return "${this.email}$primary — $verified" + } + + override fun equals(other: Any?): Boolean { + if (other is UserEmail) { + return email == other.email + } + return false + } + + override fun hashCode(): Int { + return email.hashCode() + } +} diff --git a/src/main/kotlin/app/ui/AddRepoState.kt b/src/main/kotlin/app/ui/AddRepoState.kt index 33e55194..b73c390a 100644 --- a/src/main/kotlin/app/ui/AddRepoState.kt +++ b/src/main/kotlin/app/ui/AddRepoState.kt @@ -21,8 +21,7 @@ class AddRepoState constructor(private val context: Context, if (configurator.getLocalRepos().isNotEmpty()) return while (true) { - println("Type a path to repository, or hit Enter to start " - + "hashing.") + println("Type a path to repository, or hit Enter to continue.") val pathString = readLine() ?: "" if (pathString.isEmpty()) { @@ -52,6 +51,6 @@ class AddRepoState constructor(private val context: Context, } override fun next() { - context.changeState(UpdateRepoState(context, api, configurator)) + context.changeState(EmailState(context, api, configurator)) } } diff --git a/src/main/kotlin/app/ui/AuthState.kt b/src/main/kotlin/app/ui/AuthState.kt index 66948e9b..5d48600c 100644 --- a/src/main/kotlin/app/ui/AuthState.kt +++ b/src/main/kotlin/app/ui/AuthState.kt @@ -83,12 +83,12 @@ class AuthState constructor(private val context: Context, error.ifNotNullThrow() val user = api.getUser().getOrThrow() - configurator.setRepos(user.repos) + configurator.setUser(user) println("You are successfully authenticated. Your profile page is " + BuildConfig.PROFILE_URL + configurator.getUsername()) - saveCredentialsIfChanged() + saveCredentialsIfChanged() Logger.username = configurator.getUsername() Logger.info(Logger.Events.AUTH) { "Auth success" } diff --git a/src/main/kotlin/app/ui/EmailState.kt b/src/main/kotlin/app/ui/EmailState.kt new file mode 100644 index 00000000..02dee5de --- /dev/null +++ b/src/main/kotlin/app/ui/EmailState.kt @@ -0,0 +1,87 @@ +package app.ui + +import app.Logger +import app.api.Api +import app.config.Configurator +import app.model.User +import app.model.UserEmail +import app.utils.UiHelper +import org.eclipse.jgit.api.Git +import java.io.File + +/** + * Update repositories console UI state. + */ +class EmailState constructor(private val context: Context, + private val api: Api, + private val configurator: Configurator) + : ConsoleState { + override fun doAction() { + val user = configurator.getUser() + + println("List of your emails:") + user.emails.forEach { email -> println(email) } + + val knownEmails = user.emails.map { it.email } + val newEmails = hashSetOf() + val configEmails = hashSetOf() + // TODO(anatoly): Tell about web editing emails, when it's ready. + // TODO(anatoly): Add global config parsing. + // TODO(anatoly): Add user config parsing. + + // Add emails from git configs. + for (repo in configurator.getLocalRepos()) { + try { + val git = Git.open(File(repo.path)) + val email = git.repository + .config.getString("user", null, "email") ?: "" + if (!knownEmails.contains(email)) { + configEmails.add(email) + } + } catch (e: Exception) { + Logger.error(e, "Error while parsing git config") + } + } + + if (configEmails.isNotEmpty()) { + println("Your git config contains untracked emails:") + configEmails.forEach { email -> println(email) } + if (UiHelper.confirm("Do you want to add this emails to your " + + "account?", defaultIsYes = true)) { + newEmails.addAll(configEmails) + } + } + + // Ask user to enter his emails. + if (UiHelper.confirm("Do you want to specify additional emails " + + "that you use in repositories?", defaultIsYes = false)) { + while (true) { + println("Type a email, or hit Enter to continue.") + val email = readLine() ?: "" + if (email.isBlank()) break + if (!knownEmails.contains(email)) newEmails.add(email) + } + } + + if (newEmails.isNotEmpty()) { + val newUserEmails = newEmails.map { UserEmail(it) } + // We will need new emails during hashing. + user.emails.addAll(newUserEmails) + + // Send new emails to server. + val userNewEmails = User(emails = newUserEmails.toHashSet()) + api.postUser(userNewEmails) + } + + // Warn user about need of confirmation. + if (user.emails.filter { email -> !email.verified }.isNotEmpty() || + newEmails.isNotEmpty()) { + println("Confirm your emails to show all statistics in " + + "profile.") + } + } + + override fun next() { + context.changeState(UpdateRepoState(context, api, configurator)) + } +} diff --git a/src/main/kotlin/app/ui/UpdateRepoState.kt b/src/main/kotlin/app/ui/UpdateRepoState.kt index d0d3d96a..28b52f1c 100644 --- a/src/main/kotlin/app/ui/UpdateRepoState.kt +++ b/src/main/kotlin/app/ui/UpdateRepoState.kt @@ -32,6 +32,7 @@ class UpdateRepoState constructor(private val context: Context, } } + api.postComplete().onErrorThrow() println("The repositories have been hashed. See result online on your " + "Sourcerer profile.") Logger.info(Logger.Events.HASHING_SUCCESS) { "Hashing success" } diff --git a/src/main/proto/sourcerer.proto b/src/main/proto/sourcerer.proto index f3d11913..153ea403 100644 --- a/src/main/proto/sourcerer.proto +++ b/src/main/proto/sourcerer.proto @@ -86,6 +86,15 @@ message AuthorGroup { message User { // List of known repos containing basic information for indentifying repo. repeated Repo repos = 1; + // List of known emails. + repeated UserEmail emails = 2; +} + +// Email of user with its status information. +message UserEmail { + string email = 1; + bool primary = 2; + bool verified = 3; } // Repository parameters for hashing. @@ -97,9 +106,6 @@ message Repo { // Rehash of first commit in repo. Used for indentifying forks. string initial_commit_rehash = 2; - // Email of app user from git config of repo. - string user_email = 3; - // Authors' email filter for hashed commits. If empty list then hash only // commits that created by current user. repeated string emails = 4; diff --git a/src/test/kotlin/test/tests/hashers/CodeLongevityTest.kt b/src/test/kotlin/test/tests/hashers/CodeLongevityTest.kt index 50cda460..a9d00f0a 100644 --- a/src/test/kotlin/test/tests/hashers/CodeLongevityTest.kt +++ b/src/test/kotlin/test/tests/hashers/CodeLongevityTest.kt @@ -60,6 +60,8 @@ class CodeLongevityTest : Spek({ assertEquals(isDeleted, actualLine.isDeleted, "'$lineText' line is deleted") } + Runtime.getRuntime().exec("rm -r ./.sourcerer/longevity").waitFor() + given("'line collecting #1'") { val testRepoPath = "../CodeLongevity_lc1" val testRepo = TestRepo(testRepoPath)