diff --git a/build.gradle b/build.gradle index 3aa33ba3..476dff28 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,7 @@ buildConfig { packageName = 'app' buildConfigField 'int', 'VERSION_CODE', '1' buildConfigField 'String', 'VERSION', '0.0.1' + buildConfigField 'String', 'PROFILE_URL', 'https://sourcerer.io/' } mainClassName = "app.MainKt" @@ -37,7 +38,7 @@ repositories { } dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" compile "com.beust:jcommander:1.72" compile 'com.google.protobuf:protobuf-java:3.0.0' diff --git a/src/main/kotlin/app/Config.kt b/src/main/kotlin/app/Config.kt index e7de389d..7743bb1e 100644 --- a/src/main/kotlin/app/Config.kt +++ b/src/main/kotlin/app/Config.kt @@ -15,6 +15,7 @@ class Config ( var localRepos: MutableSet = mutableSetOf() ) { fun addRepo(repo: LocalRepo) { + localRepos.remove(repo) // Fields may be updated. localRepos.add(repo) } diff --git a/src/main/kotlin/app/Main.kt b/src/main/kotlin/app/Main.kt index 38ed7bef..9b4820f8 100644 --- a/src/main/kotlin/app/Main.kt +++ b/src/main/kotlin/app/Main.kt @@ -4,7 +4,6 @@ package app import app.model.LocalRepo -import app.model.Repo import app.ui.ConsoleUi import app.utils.CommandConfig import app.utils.CommandAdd @@ -13,6 +12,7 @@ import app.utils.CommandRemove import app.utils.Options import app.utils.PasswordHelper import app.utils.RepoHelper +import app.utils.UiHelper import com.beust.jcommander.JCommander fun main(argv: Array) { @@ -61,7 +61,9 @@ fun startUi() { fun doAdd(commandAdd: CommandAdd) { val path = commandAdd.path if (path != null && RepoHelper.isValidRepo(path)) { - Configurator.addLocalRepoPersistent(LocalRepo(path)) + val localRepo = LocalRepo(path) + localRepo.hashAllContributors = commandAdd.hashAll + Configurator.addLocalRepoPersistent(localRepo) Configurator.saveToFile() println("Added git repository at $path.") } else { @@ -103,8 +105,8 @@ fun doRemove(commandRemove: CommandRemove) { fun doSetup() { if (!Configurator.isFirstLaunch()) { - println("Are you sure that you want to setup Sourcerer again? [y/n]") - if ((readLine() ?: "").toLowerCase() == "y") { + if (UiHelper.confirm("Are you sure that you want to setup Sourcerer " + + "again?", defaultIsYes = false)) { Configurator.resetAndSave() } } diff --git a/src/main/kotlin/app/RepoHasher.kt b/src/main/kotlin/app/RepoHasher.kt index b1a39a48..4e81f6f9 100644 --- a/src/main/kotlin/app/RepoHasher.kt +++ b/src/main/kotlin/app/RepoHasher.kt @@ -3,41 +3,38 @@ package app +import app.extractors.Extractor import app.model.Author import app.model.Commit +import app.model.DiffContent import app.model.LocalRepo import app.model.Repo import app.utils.RepoHelper import io.reactivex.Observable +import io.reactivex.schedulers.Schedulers import org.apache.commons.codec.digest.DigestUtils import org.eclipse.jgit.api.Git +import org.eclipse.jgit.diff.DiffEntry import org.eclipse.jgit.lib.Repository -import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevWalk import java.io.File import java.io.IOException - -// TODO(anatoly): Implement commit statistics. +import java.nio.charset.Charset +import org.eclipse.jgit.diff.DiffFormatter +import org.eclipse.jgit.lib.ObjectId +import org.eclipse.jgit.errors.MissingObjectException +import org.eclipse.jgit.util.io.DisabledOutputStream +import java.nio.file.Paths +import java.util.concurrent.TimeUnit /** * RepoHasher hashes repository and uploads stats to server. */ class RepoHasher(val localRepo: LocalRepo) { private var repo: Repo = Repo() - private val git: Git? = loadGit() - private val gitRepo: Repository? = git?.repository - - // Added and removed commits in local repo in comparison to server history. - private var addedCommits: MutableList = mutableListOf() - private var removedCommits: MutableList = mutableListOf() - - private val commitMapper: (RevCommit) -> Commit = { Commit(it) } - - private val emailFilter: (Commit) -> Boolean = { - val email = it.author.email - localRepo.hashAllAuthors || (email == localRepo.author.email || - repo.emails.contains(email)) - } + private val git: Git = loadGit() ?: + throw IllegalStateException("Git failed to load") + private val gitRepo: Repository = git.repository private fun loadGit(): Git? { return try { @@ -50,11 +47,10 @@ class RepoHasher(val localRepo: LocalRepo) { } private fun closeGit() { - gitRepo?.close() - git?.close() + gitRepo.close() + git.close() } - /* To identify and distinguish different repos we calculate its rehash. Repos may have forks. Such repos should be tracked independently. Therefore, rehash of repo calculated by values of: @@ -64,8 +60,8 @@ class RepoHasher(val localRepo: LocalRepo) { To associate forked repos with primary repo rehash of initial commit stored separately too. */ private fun getRepoRehashes() { - val initialRevCommit = getObservableRevCommits().blockingLast() - repo.initialCommitRehash = Commit(initialRevCommit).rehash + val initialCommit = getObservableCommits().blockingLast() + repo.initialCommitRehash = initialCommit.rehash var repoRehash = repo.initialCommitRehash if (localRepo.remoteOrigin.isNotBlank()) { @@ -78,8 +74,7 @@ class RepoHasher(val localRepo: LocalRepo) { } private fun getRepoConfig() { - gitRepo ?: return - val config = gitRepo.getConfig() + val config = gitRepo.config localRepo.author = Author( name = config.getString("user", null, "name") ?: "", email = config.getString("user", null, "email") ?: "") @@ -90,91 +85,158 @@ class RepoHasher(val localRepo: LocalRepo) { } private fun isKnownRepo(): Boolean { - if (Configurator.getRepos().find { it.rehash == repo.rehash } != null) { - return true - } - return false - } - - private fun rehashNewCommits() { - addedCommits = mutableListOf() - removedCommits = repo.commits.toMutableList() - val lastServerCommit: Commit = repo.commits.last() - var isLastServerCommitChecked: Boolean = false - - val commitsObservable = getObservableRevCommits() - commitsObservable.map(commitMapper).filter(emailFilter) - .filter { !isLastServerCommitChecked } - .blockingSubscribe({ // OnNext - Logger.info("Commit: ${it.rehash}") - if (it == lastServerCommit) { - isLastServerCommitChecked = true - } - if (removedCommits.contains(it)) { - removedCommits.remove(it) - } else { - addedCommits.add(it) + return Configurator.getRepos() + .find { it.rehash == repo.rehash } != null + } + + private fun findFirstOverlappingCommit(): Commit? { + val serverHistoryCommits = repo.commits.toHashSet() + return getObservableCommits() + .skipWhile { commit -> !serverHistoryCommits.contains(commit) } + .blockingFirst(null) + } + + private fun hashAndSendCommits() { + val lastKnownCommit = repo.commits.lastOrNull() + val knownCommits = repo.commits.toHashSet() + getObservableCommits() + .pairWithNext() // Pair commits to get diff. + .takeWhile { (new, _) -> // Hash until last known commit. + new.rehash != lastKnownCommit?.rehash } + .filter { (new, _) -> knownCommits.isEmpty() // Don't hash known. + || !knownCommits.contains(new) } + .filter { (new, _) -> emailFilter(new) } // Email filtering. + .map { (new, old) -> // Mapping and stats extraction. + val diffContents = getDiffContents(new, old) + Logger.debug("Commit: ${new.raw?.name ?: ""}: " + + new.raw?.shortMessage) + Logger.debug("Diff: ${diffContents.size} entries") + new.stats = Extractor.extract(diffContents) + Logger.debug("Stats: ${new.stats.size} entries") + new } - }, { t -> // OnError - Logger.error("Error while hashing: $t") - }) + .observeOn(Schedulers.io()) // Different thread for data sending. + .buffer(20, TimeUnit.SECONDS) // Group ready commits by time. + .doOnNext { commitsBundle -> // Send ready commits. + postCommitsToServer(commitsBundle) } + .blockingSubscribe({ + // OnNext + }, { t -> // OnError + Logger.error("Error while hashing: $t") + }) } - private fun rehashAllCommits() { - addedCommits = mutableListOf() + fun getDiffContents(commitNew: Commit, + commitOld: Commit): List { + // TODO(anatoly): Binary files. + val revCommitNew = commitNew.raw + val revCommitOld = commitOld.raw + if (revCommitNew == null || revCommitOld == null) { + return listOf() + } + + return DiffFormatter(DisabledOutputStream.INSTANCE).use { formatter -> + formatter.setRepository(gitRepo) + formatter.scan(revCommitOld.tree, revCommitNew.tree) + // RENAME change type doesn't change file content. + .filter { it.changeType != DiffEntry.ChangeType.RENAME } + .map { diff -> + val added = mutableListOf() + val deleted = mutableListOf() + + val new = getContentByObjectId(diff.newId.toObjectId()) + val old = getContentByObjectId(diff.oldId.toObjectId()) + + formatter.toFileHeader(diff).toEditList().forEach { edit -> + val addBegin = edit.beginB + val addEnd = edit.endB - 1 + val delBegin = edit.beginA + val delEnd = edit.endA - 1 + added.addAll(new.filterIndexed( + inRange(addBegin, addEnd))) + deleted.addAll(old.filterIndexed( + inRange(delBegin, delEnd))) + } + + val path = when (diff.changeType) { + DiffEntry.ChangeType.DELETE -> diff.oldPath + else -> diff.newPath + } - val commitsObservable = getObservableRevCommits() - commitsObservable.map(commitMapper).filter(emailFilter) - .blockingSubscribe({ // OnNext - Logger.info("Commit: ${it.rehash}") - addedCommits.add(it) - }, { t -> // OnError - Logger.error("Error while hashing: $t") - }) + DiffContent(Paths.get(path), added, deleted) + } + } + } + + private fun getContentByObjectId(objectId: ObjectId): List { + return try { + gitRepo.open(objectId).bytes.toString(Charset.defaultCharset()) + .split('\n') + } catch (e: MissingObjectException) { + listOf() + } } private fun getRepoFromServer() { repo = SourcererApi.getRepo(repo.rehash) } - private fun sendRepoToServer() { + private fun postRepoToServer() { SourcererApi.postRepo(repo) } - private fun sendAddedCommits() { - if (addedCommits.isNotEmpty()) { - SourcererApi.postCommits(addedCommits) + private fun postCommitsToServer(commits: List) { + if (commits.isNotEmpty()) { + Logger.debug("${commits.size} hashed commits sending") + SourcererApi.postCommits(commits) } } - private fun sendRemovedCommits() { - if (removedCommits.isNotEmpty()) { - SourcererApi.deleteCommits(removedCommits) + private fun deleteCommitsOnServer(commits: List) { + if (commits.isNotEmpty()) { + Logger.debug("${commits.size} deleted commits sending") + SourcererApi.deleteCommits(commits) } } - private fun getObservableRevCommits(): Observable = - Observable.create { subscriber -> - if (gitRepo != null) { - try { - val revWalk = RevWalk(gitRepo) - val commitId = gitRepo.resolve(RepoHelper.MASTER_BRANCH) - revWalk.markStart(revWalk.parseCommit(commitId)) - for (commit in revWalk) { - Logger.debug("Commit produced: ${commit.name}") - subscriber.onNext(commit) - } - } catch (e: Exception) { - Logger.error("Commit producing error", e) - subscriber.onError(e) + private fun getObservableCommits(): Observable = + Observable.create { subscriber -> + try { + val revWalk = RevWalk(gitRepo) + val commitId = gitRepo.resolve(RepoHelper.MASTER_BRANCH) + revWalk.markStart(revWalk.parseCommit(commitId)) + for (revCommit in revWalk) { + Logger.debug("Commit produced: ${revCommit.name}") + subscriber.onNext(Commit(revCommit)) } - } else { - Logger.error("Repository not loaded") + } catch (e: Exception) { + Logger.error("Commit producing error", e) + subscriber.onError(e) } + Logger.debug("Commit producing completed") subscriber.onComplete() } + private val emailFilter: (Commit) -> Boolean = { + val email = it.author.email + localRepo.hashAllContributors || (email == localRepo.author.email || + repo.emails.contains(email)) + } + + private fun inRange(indexFrom: Int, indexTo: Int) = { index: Int, _: Any -> + index >= indexFrom && index <= indexTo + } + + fun Observable.pairWithNext(): Observable> { + return this.map { emit -> Pair(emit, emit) } + // Accumulate emits by prev-next pair. + .scan { pairAccumulated, pairNext -> + Pair(pairAccumulated.second, pairNext.second) + } + .skip(1) // Skip initial not paired emit. + } + fun update() { if (!RepoHelper.isValidRepo(localRepo.path)) { Logger.error("Invalid repo $localRepo") @@ -187,19 +249,18 @@ class RepoHasher(val localRepo: LocalRepo) { if (isKnownRepo()) { getRepoFromServer() - rehashNewCommits() - sendRemovedCommits() - // Rehash all if all commits from server history removed. - if (removedCommits.size == repo.commits.size) { - rehashAllCommits() - } - } else { - rehashAllCommits() + // Delete missing commits. If found at least one common commit + // then next commits are not deleted because hash of a commit + // calculated including hashes of its parents. + val firstOverlapCommit = findFirstOverlappingCommit() + val deletedCommits = repo.commits + .takeWhile { it.rehash != firstOverlapCommit?.rehash } + deleteCommitsOnServer(deletedCommits) } - sendAddedCommits() - sendRepoToServer() + hashAndSendCommits() + postRepoToServer() println("Hashing $localRepo successfully finished.") closeGit() diff --git a/src/main/kotlin/app/extractors/EmptyExtractor.kt b/src/main/kotlin/app/extractors/EmptyExtractor.kt new file mode 100644 index 00000000..15f48dc2 --- /dev/null +++ b/src/main/kotlin/app/extractors/EmptyExtractor.kt @@ -0,0 +1,13 @@ +// Copyright 2017 Sourcerer Inc. All Rights Reserved. +// Author: Anatoly Kislov (anatoly@sourcerer.io) + +package app.extractors + +import app.model.DiffContent +import app.model.Stats + +class EmptyExtractor : ExtractorInterface { + override fun extract(diffs: List): List { + return listOf() + } +} diff --git a/src/main/kotlin/app/extractors/Extractor.kt b/src/main/kotlin/app/extractors/Extractor.kt new file mode 100644 index 00000000..c960b151 --- /dev/null +++ b/src/main/kotlin/app/extractors/Extractor.kt @@ -0,0 +1,35 @@ +// Copyright 2017 Sourcerer Inc. All Rights Reserved. +// Author: Anatoly Kislov (anatoly@sourcerer.io) + +package app.extractors + +import app.model.DiffContent +import app.model.Stats +import app.utils.FileHelper + +object Extractor : ExtractorInterface { + val TYPE_LANGUAGE = 1 + val TYPE_KEYWORD = 2 + + val SEPARATOR = ">" + + val JAVA_FILE_EXTENSIONS = listOf("java") + val PYTHON_FILE_EXTENSIONS = listOf("py", "py3") + + fun create(extension: String): ExtractorInterface { + return when (extension) { + in JAVA_FILE_EXTENSIONS -> JavaExtractor() + in PYTHON_FILE_EXTENSIONS -> PythonExtractor() + else -> EmptyExtractor() + } + } + + override fun extract(diffs: List): List { + return diffs.groupBy { diff -> FileHelper.getFileExtension(diff.path) } + .map { (extension, diffs) -> create(extension).extract(diffs) } + .fold(mutableListOf()) { accStats, stats -> + accStats.addAll(stats) + accStats + } + } +} diff --git a/src/main/kotlin/app/extractors/ExtractorInterface.kt b/src/main/kotlin/app/extractors/ExtractorInterface.kt new file mode 100644 index 00000000..34a03a79 --- /dev/null +++ b/src/main/kotlin/app/extractors/ExtractorInterface.kt @@ -0,0 +1,11 @@ +// Copyright 2017 Sourcerer Inc. All Rights Reserved. +// Author: Anatoly Kislov (anatoly@sourcerer.io) + +package app.extractors + +import app.model.DiffContent +import app.model.Stats + +interface ExtractorInterface { + fun extract(diffs: List): List +} diff --git a/src/main/kotlin/app/extractors/JavaExtractor.kt b/src/main/kotlin/app/extractors/JavaExtractor.kt new file mode 100644 index 00000000..4999406b --- /dev/null +++ b/src/main/kotlin/app/extractors/JavaExtractor.kt @@ -0,0 +1,57 @@ +// Copyright 2017 Sourcerer Inc. All Rights Reserved. +// Author: Anatoly Kislov (anatoly@sourcerer.io) + +package app.extractors + +import app.model.DiffContent +import app.model.Stats + +class JavaExtractor : ExtractorInterface { + val NAME = "Java" + + val KEYWORDS = listOf("abstract", "continue", "for", "new", "switch", + "assert", "default", "goto", "package", "synchronized", "boolean", + "do", "if", "private", "this", "break", "double", "implements", + "protected", "throw", "byte", "else", "import", "public", "throws", + "case", "enum", "instanceof", "return", "transient", "catch", + "extends", "int", "short", "try", "char", "final", "interface", + "static", "void", "class", "finally", "long", "strictfp", + "volatile", "const", "float", "native", "super", "while") + + override fun extract(diffs: List): List { + val stats = mutableListOf() + + val added = diffs.fold(mutableListOf()) { total, diff -> + total.addAll(diff.added) + total + } + + val deleted = diffs.fold(mutableListOf()) { total, diff -> + total.addAll(diff.deleted) + total + } + + // Language stats. + stats.add(Stats( + numLinesAdded = added.size, + numLinesDeleted = deleted.size, + type = Extractor.TYPE_LANGUAGE, + tech = NAME)) + + // Keywords stats. + // TODO(anatoly): ANTLR parsing. + KEYWORDS.forEach { keyword -> + val totalAdded = added.count { line -> line.contains(keyword)} + val totalDeleted = deleted.count { line -> line.contains(keyword)} + if (totalAdded > 0 || totalDeleted > 0) { + stats.add(Stats( + numLinesAdded = totalAdded, + numLinesDeleted = totalDeleted, + type = Extractor.TYPE_KEYWORD, + tech = NAME + Extractor.SEPARATOR + keyword)) + } + } + + return stats + } +} diff --git a/src/main/kotlin/app/extractors/PythonExtractor.kt b/src/main/kotlin/app/extractors/PythonExtractor.kt new file mode 100644 index 00000000..7dd4ebdc --- /dev/null +++ b/src/main/kotlin/app/extractors/PythonExtractor.kt @@ -0,0 +1,26 @@ +// Copyright 2017 Sourcerer Inc. All Rights Reserved. +// Author: Anatoly Kislov (anatoly@sourcerer.io) + +package app.extractors + +import app.model.DiffContent +import app.model.Stats + +class PythonExtractor : ExtractorInterface { + val NAME = "Python" + + override fun extract(diffs: List): List { + val stats = mutableListOf() + + // Language stats. + stats.add(Stats( + numLinesAdded = diffs.fold(0) { total, diffContent -> + total + diffContent.added.size }, + numLinesDeleted = diffs.fold(0) { total, diffContent -> + total + diffContent.deleted.size }, + type = Extractor.TYPE_LANGUAGE, + tech = NAME)) + + return stats + } +} diff --git a/src/main/kotlin/app/model/Commit.kt b/src/main/kotlin/app/model/Commit.kt index cb104d0e..84800a68 100644 --- a/src/main/kotlin/app/model/Commit.kt +++ b/src/main/kotlin/app/model/Commit.kt @@ -21,16 +21,20 @@ data class Commit( var dateTimestamp: Int = 0, var isQommit: Boolean = false, var numLinesAdded: Int = 0, - var numLinesDeleted: Int = 0 - // TODO(anatoly): add Stats. + var numLinesDeleted: Int = 0, + var stats: List = mutableListOf() ) { + // Wrapping JGit's RevCommit. + var raw: RevCommit? = null // Not sent to sever. + constructor(revCommit: RevCommit) : this() { + raw = revCommit + rehash = DigestUtils.sha256Hex(revCommit.id.name) author = Author(revCommit.authorIdent.name, revCommit.authorIdent.emailAddress) dateTimestamp = revCommit.commitTime treeRehash = DigestUtils.sha256Hex(revCommit.tree.name) - // TODO(anatoly): add Stats, isQommit, numLines. } @Throws(InvalidParameterException::class) @@ -43,7 +47,7 @@ data class Commit( isQommit = proto.isQommit numLinesAdded = proto.numLinesAdded numLinesDeleted = proto.numLinesDeleted - // TODO(anatoly): add Stats. + stats = proto.statsList.map { Stats(it) } } @Throws(InvalidProtocolBufferException::class) @@ -62,8 +66,8 @@ data class Commit( .setIsQommit(isQommit) .setNumLinesAdded(numLinesAdded) .setNumLinesDeleted(numLinesDeleted) + .addAllStats(stats.map { it.getProto() }) .build() - // TODO(anatoly): add Stats. } fun serialize(): ByteArray { diff --git a/src/main/kotlin/app/model/DiffContent.kt b/src/main/kotlin/app/model/DiffContent.kt new file mode 100644 index 00000000..b7cde3f5 --- /dev/null +++ b/src/main/kotlin/app/model/DiffContent.kt @@ -0,0 +1,16 @@ +// Copyright 2017 Sourcerer Inc. All Rights Reserved. +// Author: Anatoly Kislov (anatoly@sourcerer.io) + +package app.model + +import java.nio.file.Path +import java.nio.file.Paths + +/** + * Per file diff from commit. + */ +data class DiffContent( + var path: Path = Paths.get(""), + var added: List = listOf(), + var deleted: List = listOf() +) diff --git a/src/main/kotlin/app/model/LocalRepo.kt b/src/main/kotlin/app/model/LocalRepo.kt index 2b0e1d14..3490dfd7 100644 --- a/src/main/kotlin/app/model/LocalRepo.kt +++ b/src/main/kotlin/app/model/LocalRepo.kt @@ -3,8 +3,8 @@ package app.model -data class LocalRepo(var path: String = "", - var hashAllAuthors: Boolean = false) { +data class LocalRepo(var path: String = "") { + var hashAllContributors: Boolean = false var author: Author = Author() var remoteOrigin: String = "" var userName: String = "" diff --git a/src/main/kotlin/app/model/Repo.kt b/src/main/kotlin/app/model/Repo.kt index 3b8506b9..b609d23b 100644 --- a/src/main/kotlin/app/model/Repo.kt +++ b/src/main/kotlin/app/model/Repo.kt @@ -51,7 +51,7 @@ data class Repo( override fun equals(other: Any?): Boolean { if (this === other) return true if (other?.javaClass != javaClass) return false - return rehash == (other as Commit).rehash + return rehash == (other as Repo).rehash } override fun hashCode(): Int { diff --git a/src/main/kotlin/app/ui/AddRepoState.kt b/src/main/kotlin/app/ui/AddRepoState.kt index 5c3d6994..5f816076 100644 --- a/src/main/kotlin/app/ui/AddRepoState.kt +++ b/src/main/kotlin/app/ui/AddRepoState.kt @@ -6,13 +6,14 @@ package app.ui import app.Configurator import app.model.LocalRepo import app.utils.RepoHelper +import app.utils.UiHelper /** * Add repository dialog console UI state. */ class AddRepoState constructor(val context: Context) : ConsoleState { override fun doAction() { - if (Configurator.getLocalRepos().isEmpty()) return + if (Configurator.getLocalRepos().isNotEmpty()) return while (true) { println("Type a path to repository, or hit Enter to start " @@ -28,8 +29,11 @@ class AddRepoState constructor(val context: Context) : ConsoleState { } else { if (RepoHelper.isValidRepo(pathString)) { println("Added git repository at $pathString.") - Configurator.addLocalRepoPersistent( - LocalRepo(pathString)) + val localRepo = LocalRepo(pathString) + localRepo.hashAllContributors = UiHelper.confirm("Do you " + + "want to hash commits of all contributors?", + defaultIsYes = true) + Configurator.addLocalRepoPersistent(localRepo) Configurator.saveToFile() } else { println("No valid git repository found at $pathString.") diff --git a/src/main/kotlin/app/ui/AuthState.kt b/src/main/kotlin/app/ui/AuthState.kt index c5717f0e..b0f98111 100644 --- a/src/main/kotlin/app/ui/AuthState.kt +++ b/src/main/kotlin/app/ui/AuthState.kt @@ -3,6 +3,7 @@ package app.ui +import app.BuildConfig import app.Configurator import app.SourcererApi import app.utils.PasswordHelper @@ -68,7 +69,7 @@ class AuthState constructor(val context: Context) : ConsoleState { Configurator.setRepos(user.repos) println("You are successfully authenticated. Your profile page is " - + Configurator.getUsername()) + + BuildConfig.PROFILE_URL + Configurator.getUsername()) saveCredentialsIfChanged() return true } catch (e: RequestException) { diff --git a/src/main/kotlin/app/utils/CommandAdd.kt b/src/main/kotlin/app/utils/CommandAdd.kt index a59a1d30..fc3cbcdf 100644 --- a/src/main/kotlin/app/utils/CommandAdd.kt +++ b/src/main/kotlin/app/utils/CommandAdd.kt @@ -15,4 +15,9 @@ class CommandAdd { // Path to analyzed repository. @Parameter(description = "REPOPATH") var path: String? = null + + // Hash commits of all contributors. + @Parameter(names = arrayOf("-a", "--all"), + description = "Hash commits of all contributors.") + var hashAll: Boolean = false } diff --git a/src/main/kotlin/app/utils/FileHelper.kt b/src/main/kotlin/app/utils/FileHelper.kt new file mode 100644 index 00000000..6f9b937d --- /dev/null +++ b/src/main/kotlin/app/utils/FileHelper.kt @@ -0,0 +1,14 @@ +// Copyright 2017 Sourcerer Inc. All Rights Reserved. +// Author: Anatoly Kislov (anatoly@sourcerer.io) + +package app.utils + +import java.nio.file.Path + +object FileHelper { + fun getFileExtension(path: Path): String { + val fileName = path.fileName.toString() + return fileName.substringAfterLast( + delimiter = '.', missingDelimiterValue = "") + } +} diff --git a/src/main/kotlin/app/utils/UiHelper.kt b/src/main/kotlin/app/utils/UiHelper.kt new file mode 100644 index 00000000..c7d9671d --- /dev/null +++ b/src/main/kotlin/app/utils/UiHelper.kt @@ -0,0 +1,17 @@ +// Copyright 2017 Sourcerer Inc. All Rights Reserved. +// Author: Anatoly Kislov (anatoly@sourcerer.io) + +package app.utils + +object UiHelper { + fun confirm(message: String, defaultIsYes: Boolean): Boolean { + val yes = if (defaultIsYes) "Y" else "y" + val no = if (!defaultIsYes) "N" else "n" + println("$message [$yes/$no]") + val oppositeDefaultValue = if (defaultIsYes) no else yes + if ((readLine() ?: "").toLowerCase() == oppositeDefaultValue) { + return !defaultIsYes + } + return defaultIsYes + } +}