Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package com.itsaky.androidide.fragments.git

import android.os.Bundle
import android.view.View
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.itsaky.androidide.R
Expand All @@ -11,12 +13,11 @@ import com.itsaky.androidide.fragments.git.adapter.GitFileChangeAdapter
import com.itsaky.androidide.viewmodel.GitBottomSheetViewModel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import androidx.fragment.app.activityViewModels

class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) {

private val viewModel: GitBottomSheetViewModel by activityViewModels()
private lateinit var adapter: GitFileChangeAdapter
private lateinit var fileChangeAdapter: GitFileChangeAdapter

private var _binding: FragmentGitBottomSheetBinding? = null
private val binding get() = _binding!!
Expand All @@ -25,29 +26,75 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentGitBottomSheetBinding.bind(view)

adapter = GitFileChangeAdapter(onFileClicked = { change ->
// Show diff in a dialog when changed file is clicked
val dialog = GitDiffViewerDialog.newInstance(change.path)
dialog.show(childFragmentManager, "GitDiffViewerDialog")
})
fileChangeAdapter = GitFileChangeAdapter(
onFileClicked = { change ->
// Show diff in a dialog when changed file is clicked
val dialog = GitDiffViewerDialog.newInstance(change.path)
dialog.show(childFragmentManager, "GitDiffViewerDialog")
},
onSelectionChanged = {
validateCommitButton()
}
)

binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter
binding.recyclerView.adapter = fileChangeAdapter

viewLifecycleOwner.lifecycleScope.launch {
viewModel.gitStatus.collectLatest { status ->
val allChanges = status.staged + status.unstaged + status.untracked + status.conflicted
val allChanges =
status.staged + status.unstaged + status.untracked + status.conflicted

if (allChanges.isEmpty()) {
binding.emptyView.visibility = View.VISIBLE
binding.recyclerView.visibility = View.GONE
binding.commitSection.visibility = View.GONE
} else {
binding.emptyView.visibility = View.GONE
binding.recyclerView.visibility = View.VISIBLE
adapter.submitList(allChanges)
binding.commitSection.visibility = View.VISIBLE
fileChangeAdapter.submitList(allChanges)
}
}

}

setupCommitUI()

binding.commitHistoryButton.setOnClickListener {
val dialog = GitCommitHistoryDialog()
dialog.show(childFragmentManager, "CommitHistoryDialog")
}

}

private fun setupCommitUI() {
binding.commitSummary.doAfterTextChanged { validateCommitButton() }
binding.commitDescription.doAfterTextChanged { validateCommitButton() }

binding.commitButton.setOnClickListener {
val summary = binding.commitSummary.text?.toString()?.trim() ?: ""
val description = binding.commitDescription.text?.toString()?.trim()

if (summary.isNotEmpty() && fileChangeAdapter.selectedFiles.isNotEmpty()) {
viewModel.commitChanges(
summary = summary,
description = description,
selectedPaths = fileChangeAdapter.selectedFiles.toList()
)

// Clear the inputs on commit attempt
binding.commitSummary.text?.clear()
binding.commitDescription.text?.clear()
fileChangeAdapter.selectedFiles.clear()
}
}
}

private fun validateCommitButton() {
val hasSummary = !binding.commitSummary.text.isNullOrBlank()
val hasSelection = fileChangeAdapter.selectedFiles.isNotEmpty()
binding.commitButton.isEnabled = hasSummary && hasSelection
}

override fun onDestroyView() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.itsaky.androidide.fragments.git

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.itsaky.androidide.R
import com.itsaky.androidide.databinding.DialogGitCommitHistoryBinding
import com.itsaky.androidide.fragments.git.adapter.GitCommitHistoryAdapter
import com.itsaky.androidide.viewmodel.GitBottomSheetViewModel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch

class GitCommitHistoryDialog : DialogFragment() {

private lateinit var binding: DialogGitCommitHistoryBinding
private val viewModel: GitBottomSheetViewModel by activityViewModels()
private lateinit var commitHistoryAdapter: GitCommitHistoryAdapter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.Theme_AndroidIDE)
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DialogGitCommitHistoryBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
commitHistoryAdapter = GitCommitHistoryAdapter()
val linearLayoutManager = LinearLayoutManager(requireContext())
val dividerItemDecoration = DividerItemDecoration(
binding.rvCommitHistory.context,
linearLayoutManager.orientation
)
binding.rvCommitHistory.apply {
layoutManager = linearLayoutManager
addItemDecoration(dividerItemDecoration)
adapter = commitHistoryAdapter
}

viewModel.getCommitHistoryList()

viewLifecycleOwner.lifecycleScope.launch {
viewModel.commitHistory.collectLatest { history ->
if (history.isEmpty()) {
binding.emptyView.visibility = View.VISIBLE
binding.rvCommitHistory.visibility = View.GONE
} else {
binding.emptyView.visibility = View.GONE
binding.rvCommitHistory.visibility = View.VISIBLE
commitHistoryAdapter.submitList(history)
}
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.itsaky.androidide.fragments.git.adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.itsaky.androidide.databinding.ItemGitCommitBinding
import com.itsaky.androidide.git.core.models.GitCommit
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

class GitCommitHistoryAdapter :
ListAdapter<GitCommit, GitCommitHistoryAdapter.ViewHolder>(DiffCallback()) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemGitCommitBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return ViewHolder(binding)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val commit = getItem(position)
holder.bind(commit)
}

class ViewHolder(private val binding: ItemGitCommitBinding) :
RecyclerView.ViewHolder(binding.root) {

private val dateFormat = SimpleDateFormat("dd MMM yyyy, HH:mm", Locale.getDefault())

fun bind(commit: GitCommit) {
binding.apply {
tvCommitMessage.text = commit.message
tvCommitAuthor.text = commit.authorName
tvCommitTime.text = dateFormat.format(Date(commit.timestamp))
}
}
}

class DiffCallback : DiffUtil.ItemCallback<GitCommit>() {
override fun areItemsTheSame(oldItem: GitCommit, newItem: GitCommit): Boolean {
return oldItem.hash == newItem.hash
}

override fun areContentsTheSame(oldItem: GitCommit, newItem: GitCommit): Boolean {
return oldItem == newItem
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import com.itsaky.androidide.git.core.models.ChangeType
import com.itsaky.androidide.git.core.models.FileChange

class GitFileChangeAdapter(
private val onFileClicked: (FileChange) -> Unit
private val onFileClicked: (FileChange) -> Unit,
private val onSelectionChanged: (Int) -> Unit = {}
) : ListAdapter<FileChange, GitFileChangeAdapter.ViewHolder>(DiffCallback()) {

// Keep track of which files are selected to be committed
Expand Down Expand Up @@ -48,6 +49,7 @@ class GitFileChangeAdapter(
} else {
selectedFiles.remove(change.path)
}
onSelectionChanged(selectedFiles.size)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.itsaky.androidide.eventbus.events.file.FileRenameEvent
import com.itsaky.androidide.events.ListProjectFilesRequestEvent
import com.itsaky.androidide.git.core.GitRepository
import com.itsaky.androidide.git.core.GitRepositoryManager
import com.itsaky.androidide.git.core.models.GitCommit
import com.itsaky.androidide.git.core.models.GitStatus
import com.itsaky.androidide.projects.IProjectManager
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -27,6 +28,8 @@ class GitBottomSheetViewModel : ViewModel() {

private val _gitStatus = MutableStateFlow(GitStatus.EMPTY)
val gitStatus: StateFlow<GitStatus> = _gitStatus.asStateFlow()
private val _commitHistory = MutableStateFlow(emptyList<GitCommit>())
val commitHistory: StateFlow<List<GitCommit>> = _commitHistory.asStateFlow()

var currentRepository: GitRepository? = null
private set
Expand Down Expand Up @@ -74,6 +77,39 @@ class GitBottomSheetViewModel : ViewModel() {
}
}

fun commitChanges(
summary: String,
description: String? = null,
selectedPaths: List<String>
) {
viewModelScope.launch {
try {
if (selectedPaths.isEmpty()) return@launch

val projectDir = File(IProjectManager.getInstance().projectDirPath)
val filesToStage = selectedPaths.map { File(projectDir, it) }

currentRepository?.stageFiles(filesToStage)

val message =
if (!description.isNullOrBlank()) "$summary\n\n$description" else summary
currentRepository?.commit(message)

refreshStatus()
} catch (e: Exception) {
log.error("Failed to commit changes", e)
}

}
}

fun getCommitHistoryList() {
viewModelScope.launch {
val history = currentRepository?.getHistory()
_commitHistory.value = history ?: emptyList()
}
}

@Subscribe(threadMode = ThreadMode.MAIN)
fun onDocumentSaved(event: DocumentSaveEvent) {
refreshStatus()
Expand Down
50 changes: 50 additions & 0 deletions app/src/main/res/layout/dialog_git_commit_history.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?colorSurface">

<TextView
android:id="@+id/tv_commit_history"
style="@style/TextAppearance.AppCompat.Headline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="@string/git_commit_history"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<com.google.android.material.divider.MaterialDivider
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="2dp"
app:layout_constraintTop_toBottomOf="@id/tv_commit_history" />

<TextView
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/no_commit_history"
android:textAppearance="?attr/textAppearanceBody2"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_commit_history"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider"
app:layout_constraintVertical_bias="0"
tools:listitem="@layout/item_git_commit" />

</androidx.constraintlayout.widget.ConstraintLayout>
Loading
Loading