From 0f0d1994e589b4f2b2603c882f52c79e2307a4f7 Mon Sep 17 00:00:00 2001 From: Nosweh <62889203+Nosweh@users.noreply.github.com> Date: Fri, 28 Aug 2020 17:31:40 +0200 Subject: Add Activity to view the Git commit log (#1056) --- .../main/java/com/zeapo/pwdstore/PasswordStore.kt | 1 + .../com/zeapo/pwdstore/git/GitConfigActivity.kt | 68 ++++++++++++++++------ .../java/com/zeapo/pwdstore/git/log/GitCommit.kt | 18 ++++++ .../com/zeapo/pwdstore/git/log/GitLogActivity.kt | 38 ++++++++++++ .../com/zeapo/pwdstore/git/log/GitLogAdapter.kt | 56 ++++++++++++++++++ .../java/com/zeapo/pwdstore/git/log/GitLogModel.kt | 53 +++++++++++++++++ .../java/com/zeapo/pwdstore/utils/Extensions.kt | 23 ++++++++ 7 files changed, 240 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/com/zeapo/pwdstore/git/log/GitCommit.kt create mode 100644 app/src/main/java/com/zeapo/pwdstore/git/log/GitLogActivity.kt create mode 100644 app/src/main/java/com/zeapo/pwdstore/git/log/GitLogAdapter.kt create mode 100644 app/src/main/java/com/zeapo/pwdstore/git/log/GitLogModel.kt (limited to 'app/src/main/java') diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt index d9afc7c9..6a0d707c 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt @@ -48,6 +48,7 @@ import com.zeapo.pwdstore.crypto.BasePgpActivity.Companion.getLongName import com.zeapo.pwdstore.crypto.DecryptActivity import com.zeapo.pwdstore.crypto.PasswordCreationActivity import com.zeapo.pwdstore.git.BaseGitActivity +import com.zeapo.pwdstore.git.log.GitLogActivity import com.zeapo.pwdstore.git.GitOperationActivity import com.zeapo.pwdstore.git.GitServerConfigActivity import com.zeapo.pwdstore.git.config.AuthMode diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt index 9205e7fc..89376bfd 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt @@ -4,20 +4,24 @@ */ package com.zeapo.pwdstore.git +import android.content.Intent import android.os.Bundle import android.os.Handler import android.util.Patterns import androidx.core.os.postDelayed import androidx.lifecycle.lifecycleScope +import com.github.ajalt.timberkt.e import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.zeapo.pwdstore.R import com.zeapo.pwdstore.databinding.ActivityGitConfigBinding import com.zeapo.pwdstore.git.config.GitSettings +import com.zeapo.pwdstore.git.log.GitLogActivity import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.viewBinding import kotlinx.coroutines.launch import org.eclipse.jgit.lib.Constants +import org.eclipse.jgit.lib.Repository class GitConfigActivity : BaseGitActivity() { @@ -33,23 +37,7 @@ class GitConfigActivity : BaseGitActivity() { else binding.gitUserName.setText(GitSettings.authorName) binding.gitUserEmail.setText(GitSettings.authorEmail) - val repo = PasswordRepository.getRepository(PasswordRepository.getRepositoryDirectory()) - if (repo != null) { - try { - val objectId = repo.resolve(Constants.HEAD) - val ref = repo.getRef("refs/heads/${GitSettings.branch}") - val head = if (ref.objectId.equals(objectId)) ref.name else "DETACHED" - binding.gitCommitHash.text = String.format("%s (%s)", objectId.abbreviate(8).name(), head) - - // enable the abort button only if we're rebasing - val isRebasing = repo.repositoryState.isRebasing - binding.gitAbortRebase.isEnabled = isRebasing - binding.gitAbortRebase.alpha = if (isRebasing) 1.0f else 0.5f - } catch (ignored: Exception) { - } - } - binding.gitAbortRebase.setOnClickListener { lifecycleScope.launch { launchGitOperation(BREAK_OUT_OF_DETACHED) } } - binding.gitResetToRemote.setOnClickListener { lifecycleScope.launch { launchGitOperation(REQUEST_RESET) } } + setupTools() binding.saveButton.setOnClickListener { val email = binding.gitUserEmail.text.toString().trim() val name = binding.gitUserName.text.toString().trim() @@ -66,4 +54,50 @@ class GitConfigActivity : BaseGitActivity() { } } } + + /** + * Sets up the UI components of the tools section. + */ + private fun setupTools() { + val repo = PasswordRepository.getRepository(null) + if (repo != null) { + binding.gitHeadStatus.text = headStatusMsg(repo) + // enable the abort button only if we're rebasing + val isRebasing = repo.repositoryState.isRebasing + binding.gitAbortRebase.isEnabled = isRebasing + binding.gitAbortRebase.alpha = if (isRebasing) 1.0f else 0.5f + } + binding.gitLog.setOnClickListener { + try { + intent = Intent(this, GitLogActivity::class.java) + startActivity(intent) + } catch (ex: Exception) { + e(ex) { "Failed to start GitLogActivity" } + } + } + binding.gitAbortRebase.setOnClickListener { lifecycleScope.launch { launchGitOperation(BREAK_OUT_OF_DETACHED) } } + binding.gitResetToRemote.setOnClickListener { lifecycleScope.launch { launchGitOperation(REQUEST_RESET) } } + } + + /** + * Returns a user-friendly message about the current state of HEAD. + * + * The state is recognized to be either pointing to a branch or detached. + */ + private fun headStatusMsg(repo: Repository): String { + return try { + val headRef = repo.getRef(Constants.HEAD) + if (headRef.isSymbolic) { + val branchName = headRef.target.name + val shortBranchName = Repository.shortenRefName(branchName) + getString(R.string.git_head_on_branch, shortBranchName) + } else { + val commitHash = headRef.objectId.abbreviate(8).name() + getString(R.string.git_head_detached, commitHash) + } + } catch (ex: Exception) { + e(ex) { "Error getting HEAD reference" } + getString(R.string.git_head_missing) + } + } } diff --git a/app/src/main/java/com/zeapo/pwdstore/git/log/GitCommit.kt b/app/src/main/java/com/zeapo/pwdstore/git/log/GitCommit.kt new file mode 100644 index 00000000..d2425592 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/log/GitCommit.kt @@ -0,0 +1,18 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package com.zeapo.pwdstore.git.log + +import java.util.Date + +/** + * Basic information about a git commit. + * + * @property hash full-length hash of the commit object. + * @property shortMessage the commit's short message (i.e. title line). + * @property authorName name of the commit's author without email address. + * @property time time when the commit was created. + */ +data class GitCommit(val hash: String, val shortMessage: String, val authorName: String, val time: Date) diff --git a/app/src/main/java/com/zeapo/pwdstore/git/log/GitLogActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/log/GitLogActivity.kt new file mode 100644 index 00000000..8c1c0f09 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/log/GitLogActivity.kt @@ -0,0 +1,38 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package com.zeapo.pwdstore.git.log + +import android.os.Bundle +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.zeapo.pwdstore.databinding.ActivityGitLogBinding +import com.zeapo.pwdstore.git.BaseGitActivity +import com.zeapo.pwdstore.utils.viewBinding + +/** + * Displays the repository's git commits in git-log fashion. + * + * It provides basic information about each commit by way of a non-interactive RecyclerView. + */ +class GitLogActivity : BaseGitActivity() { + + private val binding by viewBinding(ActivityGitLogBinding::inflate) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + createRecyclerView() + } + + private fun createRecyclerView() { + binding.gitLogRecyclerView.apply { + setHasFixedSize(true) + addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL)) + adapter = GitLogAdapter() + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/log/GitLogAdapter.kt b/app/src/main/java/com/zeapo/pwdstore/git/log/GitLogAdapter.kt new file mode 100644 index 00000000..a15e7f7e --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/log/GitLogAdapter.kt @@ -0,0 +1,56 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package com.zeapo.pwdstore.git.log + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.github.ajalt.timberkt.e +import com.zeapo.pwdstore.databinding.GitLogRowLayoutBinding +import java.text.DateFormat +import java.util.Date + +private fun shortHash(hash: String): String { + return hash.substring(0 until 8) +} + +private fun stringFrom(date: Date): String { + return DateFormat.getDateTimeInstance().format(date) +} + +/** + * @see GitLogActivity + */ +class GitLogAdapter : RecyclerView.Adapter() { + + private val model = GitLogModel() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(parent.context) + val binding = GitLogRowLayoutBinding.inflate(inflater, parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { + val commit = model.get(position) + if (commit == null) { + e { "There is no git commit for view holder at position $position." } + return + } + viewHolder.bind(commit) + } + + override fun getItemCount() = model.size + + class ViewHolder(private val binding: GitLogRowLayoutBinding) : RecyclerView.ViewHolder(binding.root) { + + fun bind(commit: GitCommit) = with(binding) { + gitLogRowMessage.text = commit.shortMessage + gitLogRowHash.text = shortHash(commit.hash) + gitLogRowTime.text = stringFrom(commit.time) + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/git/log/GitLogModel.kt b/app/src/main/java/com/zeapo/pwdstore/git/log/GitLogModel.kt new file mode 100644 index 00000000..22c1ec78 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/git/log/GitLogModel.kt @@ -0,0 +1,53 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package com.zeapo.pwdstore.git.log + +import com.github.ajalt.timberkt.e +import com.zeapo.pwdstore.utils.PasswordRepository +import com.zeapo.pwdstore.utils.hash +import com.zeapo.pwdstore.utils.time +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.revwalk.RevCommit + +private fun commits(): Iterable { + val repo = PasswordRepository.getRepository(null) + if (repo == null) { + e { "Could not access git repository" } + return listOf() + } + return try { + Git(repo).log().call() + } catch (exc: Exception) { + e(exc) { "Failed to obtain git commits" } + listOf() + } +} + +/** + * Provides [GitCommit]s from a git-log of the password git repository. + * + * All commits are acquired on the first request to this object. + */ +class GitLogModel { + + // All commits are acquired here at once. Acquiring the commits in batches would not have been + // entirely sensible because the amount of computation required to obtain commit number n from + // the log includes the amount of computation required to obtain commit number n-1 from the log. + // This is because the commit graph is walked from HEAD to the last commit to obtain. + // Additionally, tests with 1000 commits in the log have not produced a significant delay in the + // user experience. + private val cache: MutableList by lazy { + commits().map { + GitCommit(it.hash, it.shortMessage, it.authorIdent.name, it.time) + }.toMutableList() + } + val size = cache.size + + fun get(index: Int): GitCommit? { + if (index >= size) e { "Cannot get git commit with index $index. There are only $size." } + return cache.getOrNull(index) + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt index 561b8d99..edc01776 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt @@ -28,6 +28,9 @@ import com.zeapo.pwdstore.git.GitCommandExecutor import com.zeapo.pwdstore.git.operation.GitOperation import com.zeapo.pwdstore.utils.PasswordRepository.Companion.getRepositoryDirectory import java.io.File +import java.util.Date +import org.eclipse.jgit.lib.ObjectId +import org.eclipse.jgit.revwalk.RevCommit const val OPENPGP_PROVIDER = "org.sufficientlysecure.keychain" @@ -162,3 +165,23 @@ val Context.autofillManager: AutofillManager? fun File.isInsideRepository(): Boolean { return canonicalPath.contains(getRepositoryDirectory().canonicalPath) } + +/** + * Unique SHA-1 hash of this commit as hexadecimal string. + * + * @see RevCommit.id + */ +val RevCommit.hash: String + get() = ObjectId.toString(id) + +/** + * Time this commit was made with second precision. + * + * @see RevCommit.commitTime + */ +val RevCommit.time: Date + get() { + val epochSeconds = commitTime.toLong() + val epochMilliseconds = epochSeconds * 1000 + return Date(epochMilliseconds) + } -- cgit v1.2.3