diff --git a/app/src/main/java/com/itsaky/androidide/adapters/EditorBottomSheetTabAdapter.kt b/app/src/main/java/com/itsaky/androidide/adapters/EditorBottomSheetTabAdapter.kt index 8a79c6ed2e..ee589b45f4 100644 --- a/app/src/main/java/com/itsaky/androidide/adapters/EditorBottomSheetTabAdapter.kt +++ b/app/src/main/java/com/itsaky/androidide/adapters/EditorBottomSheetTabAdapter.kt @@ -25,6 +25,7 @@ import com.itsaky.androidide.app.IDEApplication.Companion.getPluginManager import com.itsaky.androidide.fragments.DiagnosticsListFragment import com.itsaky.androidide.fragments.SearchResultFragment import com.itsaky.androidide.fragments.debug.DebuggerFragment +import com.itsaky.androidide.fragments.git.GitBottomSheetFragment import com.itsaky.androidide.fragments.output.AppLogFragment import com.itsaky.androidide.fragments.output.BuildOutputFragment import com.itsaky.androidide.fragments.output.IDELogFragment @@ -53,6 +54,7 @@ class EditorBottomSheetTabAdapter( const val TAB_SEARCH_RESULTS = 4 const val TAB_DEBUGGER = 5 const val TAB_AGENT = 6 + const val TAB_GIT = 7 } private val allTabs = @@ -120,6 +122,14 @@ class EditorBottomSheetTabAdapter( tooltipTag = TooltipTag.PROJECT_AGENT, ), ) + + add( + Tab( + title = fragmentActivity.getString(R.string.git_title), + fragmentClass = GitBottomSheetFragment::class.java, + itemId = TAB_GIT, + ), + ) } } diff --git a/app/src/main/java/com/itsaky/androidide/di/AppModule.kt b/app/src/main/java/com/itsaky/androidide/di/AppModule.kt index f3c46cc425..8e2f15ff39 100644 --- a/app/src/main/java/com/itsaky/androidide/di/AppModule.kt +++ b/app/src/main/java/com/itsaky/androidide/di/AppModule.kt @@ -5,8 +5,9 @@ import com.itsaky.androidide.agent.GeminiMacroProcessor import com.itsaky.androidide.agent.viewmodel.ChatViewModel import com.itsaky.androidide.analytics.AnalyticsManager import com.itsaky.androidide.analytics.IAnalyticsManager -import org.koin.androidx.viewmodel.dsl.viewModel +import com.itsaky.androidide.viewmodel.GitBottomSheetViewModel import org.koin.dsl.module +import org.koin.core.module.dsl.viewModel val coreModule = module { @@ -19,4 +20,7 @@ val coreModule = viewModel { ChatViewModel() } + viewModel { + GitBottomSheetViewModel() + } } diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt new file mode 100644 index 0000000000..04647d53a8 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt @@ -0,0 +1,57 @@ +package com.itsaky.androidide.fragments.git + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import com.itsaky.androidide.R +import com.itsaky.androidide.databinding.FragmentGitBottomSheetBinding +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 var _binding: FragmentGitBottomSheetBinding? = null + private val binding get() = _binding!! + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + 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") + }) + + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.gitStatus.collectLatest { status -> + val allChanges = status.staged + status.unstaged + status.untracked + status.conflicted + + if (allChanges.isEmpty()) { + binding.emptyView.visibility = View.VISIBLE + binding.recyclerView.visibility = View.GONE + } else { + binding.emptyView.visibility = View.GONE + binding.recyclerView.visibility = View.VISIBLE + adapter.submitList(allChanges) + } + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/GitDiffViewerDialog.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/GitDiffViewerDialog.kt new file mode 100644 index 0000000000..197a594527 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/GitDiffViewerDialog.kt @@ -0,0 +1,165 @@ +package com.itsaky.androidide.fragments.git + +import android.graphics.Canvas +import android.graphics.Paint +import android.os.Bundle +import android.text.SpannableStringBuilder +import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE +import android.text.style.ForegroundColorSpan +import android.text.style.LineBackgroundSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat.getColor +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import com.itsaky.androidide.R +import com.itsaky.androidide.databinding.DialogGitDiffBinding +import com.itsaky.androidide.viewmodel.GitBottomSheetViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File + +class GitDiffViewerDialog : DialogFragment() { + + private val viewModel: GitBottomSheetViewModel by activityViewModels() + + private var filePath: String = "" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, R.style.Theme_AndroidIDE) + filePath = arguments?.getString(ARG_FILE_PATH) ?: getString(R.string.diff_viewer) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return inflater.inflate(R.layout.dialog_git_diff, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val binding = DialogGitDiffBinding.bind(view) + + binding.diffTitle.text = filePath + binding.diffText.text = getString(R.string.diff_loading) + + viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { + val repo = viewModel.currentRepository + val diff = if (repo != null && repo.rootDir.exists()) { + val file = File(repo.rootDir, filePath) + repo.getDiff(file) + } else { + null + } ?: getString(R.string.unable_to_load_diff) + + withContext(Dispatchers.Main) { + binding.diffText.text = applyDiffFormatting(diff) + } + } + + binding.btnClose.setOnClickListener { + dismiss() + } + } + + private fun applyDiffFormatting(diff: String): SpannableStringBuilder { + val builder = SpannableStringBuilder() + val lines = diff.split("\n") + + // Find the index of the first diff chunk (starts with @@) + val firstChunkIndex = lines.indexOfFirst { it.startsWith("@@") } + val startIndex = if (firstChunkIndex != -1) firstChunkIndex else 0 + + val context = requireContext() + val colorAdd = getColor(context, R.color.git_diff_add_text) + val bgAdd = getColor(context, R.color.git_diff_add_bg) + val colorDel = getColor(context, R.color.git_diff_del_text) + val bgDel = getColor(context, R.color.git_diff_del_bg) + val colorHeader = getColor(context, R.color.git_diff_header_text) + + for (i in startIndex until lines.size) { + val line = lines[i] + val startIdx = builder.length + when { + line.startsWith("+") && !line.startsWith("+++") -> { + val formattedLine = line.replaceFirst("+", "+ ") + builder.append(formattedLine).append("\n") + val endIdx = builder.length + + builder.setSpan(ForegroundColorSpan(colorAdd), startIdx, endIdx, + SPAN_EXCLUSIVE_EXCLUSIVE + ) + builder.setSpan(FullWidthBackgroundColorSpan(bgAdd), startIdx, endIdx, + SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + line.startsWith("-") && !line.startsWith("---") -> { + val formattedLine = line.replaceFirst("-", "- ") + builder.append(formattedLine).append("\n") + val endIdx = builder.length + + builder.setSpan(ForegroundColorSpan(colorDel), startIdx, endIdx, + SPAN_EXCLUSIVE_EXCLUSIVE + ) + builder.setSpan(FullWidthBackgroundColorSpan(bgDel), startIdx, endIdx, + SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + line.startsWith("@@") -> { + builder.append(line).append("\n") + val endIdx = builder.length + + builder.setSpan(ForegroundColorSpan(colorHeader), startIdx, endIdx, + SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + else -> { + val formattedLine = if (line.startsWith(" ")) " $line" else line + builder.append(formattedLine).append("\n") + } + } + } + + return builder + } + + private class FullWidthBackgroundColorSpan(private val color: Int) : LineBackgroundSpan { + override fun drawBackground( + canvas: Canvas, paint: Paint, + left: Int, right: Int, top: Int, baseline: Int, bottom: Int, + text: CharSequence, start: Int, end: Int, lineNumber: Int + ) { + val oldColor = paint.color + paint.color = color + canvas.drawRect(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat(), paint) + paint.color = oldColor + } + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + } + + companion object { + private const val ARG_FILE_PATH = "arg_file_path" + + fun newInstance(filePath: String): GitDiffViewerDialog { + return GitDiffViewerDialog().apply { + arguments = Bundle().apply { + putString(ARG_FILE_PATH, filePath) + } + } + } + } +} diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/adapter/GitFileChangeAdapter.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/adapter/GitFileChangeAdapter.kt new file mode 100644 index 0000000000..6b679d10f7 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/adapter/GitFileChangeAdapter.kt @@ -0,0 +1,84 @@ +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.R +import com.itsaky.androidide.databinding.ItemGitFileChangeBinding +import com.itsaky.androidide.git.core.models.ChangeType +import com.itsaky.androidide.git.core.models.FileChange + +class GitFileChangeAdapter( + private val onFileClicked: (FileChange) -> Unit +) : ListAdapter(DiffCallback()) { + + // Keep track of which files are selected to be committed + val selectedFiles = mutableSetOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = ItemGitFileChangeBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val change = getItem(position) + holder.bind(change) + } + + inner class ViewHolder(private val binding: ItemGitFileChangeBinding) : RecyclerView.ViewHolder(binding.root) { + + init { + itemView.setOnClickListener { + val pos = bindingAdapterPosition + if (pos != RecyclerView.NO_POSITION) { + onFileClicked(getItem(pos)) + } + } + + binding.checkbox.setOnCheckedChangeListener { _, isChecked -> + val pos = bindingAdapterPosition + if (pos != RecyclerView.NO_POSITION) { + val change = getItem(pos) + if (isChecked) { + selectedFiles.add(change.path) + } else { + selectedFiles.remove(change.path) + } + } + } + } + + fun bind(change: FileChange) { + binding.filePath.text = change.path + + binding.checkbox.isChecked = selectedFiles.contains(change.path) + + val (imageRes, descRes) = when (change.type) { + ChangeType.ADDED -> R.drawable.ic_file_added to R.string.desc_file_added + ChangeType.MODIFIED -> R.drawable.ic_file_modified to R.string.desc_file_modified + ChangeType.DELETED -> R.drawable.ic_file_deleted to R.string.desc_file_deleted + ChangeType.UNTRACKED -> R.drawable.ic_file_added to R.string.desc_file_untracked + ChangeType.RENAMED -> R.drawable.ic_file_renamed to R.string.desc_file_renamed + ChangeType.CONFLICTED -> R.drawable.ic_file_conflicted to R.string.desc_file_conflicted + } + binding.statusIcon.apply { + setImageResource(imageRes) + contentDescription = binding.root.context.getString(descRes) + } + } + } + + class DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: FileChange, newItem: FileChange): Boolean { + return oldItem.path == newItem.path + } + + override fun areContentsTheSame(oldItem: FileChange, newItem: FileChange): Boolean { + return oldItem == newItem + } + } +} diff --git a/app/src/main/java/com/itsaky/androidide/viewmodel/BottomSheetViewModel.kt b/app/src/main/java/com/itsaky/androidide/viewmodel/BottomSheetViewModel.kt index bff0336eb9..bda6632cfd 100644 --- a/app/src/main/java/com/itsaky/androidide/viewmodel/BottomSheetViewModel.kt +++ b/app/src/main/java/com/itsaky/androidide/viewmodel/BottomSheetViewModel.kt @@ -31,6 +31,7 @@ class BottomSheetViewModel : ViewModel() { const val TAB_SEARCH_RESULT = EditorBottomSheetTabAdapter.TAB_SEARCH_RESULTS const val TAB_DEBUGGER = EditorBottomSheetTabAdapter.TAB_DEBUGGER const val TAB_AGENT = EditorBottomSheetTabAdapter.TAB_AGENT + const val TAB_GIT = EditorBottomSheetTabAdapter.TAB_GIT } @Keep @@ -42,6 +43,7 @@ class BottomSheetViewModel : ViewModel() { TAB_SEARCH_RESULT, TAB_DEBUGGER, TAB_AGENT, + TAB_GIT, ) @Retention(AnnotationRetention.SOURCE) annotation class TabDef diff --git a/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt b/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt new file mode 100644 index 0000000000..479aff4879 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt @@ -0,0 +1,101 @@ +package com.itsaky.androidide.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.itsaky.androidide.eventbus.events.editor.DocumentSaveEvent +import com.itsaky.androidide.eventbus.events.file.FileCreationEvent +import com.itsaky.androidide.eventbus.events.file.FileDeletionEvent +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.GitStatus +import com.itsaky.androidide.projects.IProjectManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.slf4j.LoggerFactory +import java.io.File + +class GitBottomSheetViewModel : ViewModel() { + + private val log = LoggerFactory.getLogger(GitBottomSheetViewModel::class.java) + + private val _gitStatus = MutableStateFlow(GitStatus.EMPTY) + val gitStatus: StateFlow = _gitStatus.asStateFlow() + + var currentRepository: GitRepository? = null + private set + + init { + EventBus.getDefault().register(this) + initializeRepository() + } + + override fun onCleared() { + super.onCleared() + EventBus.getDefault().unregister(this) + currentRepository?.close() + } + + private fun initializeRepository() { + viewModelScope.launch { + try { + val projectDir = File(IProjectManager.getInstance().projectDirPath) + currentRepository = GitRepositoryManager.openRepository(projectDir) + refreshStatus() + } catch (e: Exception) { + log.error("Failed to initialize repository", e) + _gitStatus.value = GitStatus.EMPTY + } + } + } + + /** + * Refreshes the Git status of the project. + */ + fun refreshStatus() { + viewModelScope.launch { + try { + currentRepository?.let { repo -> + val status = repo.getStatus() + _gitStatus.value = status + } ?: run { + _gitStatus.value = GitStatus.EMPTY + } + } catch (e: Exception) { + log.error("Failed to refresh git status", e) + _gitStatus.value = GitStatus.EMPTY + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onDocumentSaved(event: DocumentSaveEvent) { + refreshStatus() + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onProjectFilesChanged(event: ListProjectFilesRequestEvent) { + refreshStatus() + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onFileCreated(event: FileCreationEvent) { + refreshStatus() + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onFileDeleted(event: FileDeletionEvent) { + refreshStatus() + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onFileRenamed(event: FileRenameEvent) { + refreshStatus() + } +} diff --git a/app/src/main/res/drawable/ic_file_added.xml b/app/src/main/res/drawable/ic_file_added.xml new file mode 100644 index 0000000000..2c18d57bc0 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_added.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_file_conflicted.xml b/app/src/main/res/drawable/ic_file_conflicted.xml new file mode 100644 index 0000000000..b8a26a680f --- /dev/null +++ b/app/src/main/res/drawable/ic_file_conflicted.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_deleted.xml b/app/src/main/res/drawable/ic_file_deleted.xml new file mode 100644 index 0000000000..c3308c3b14 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_deleted.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_modified.xml b/app/src/main/res/drawable/ic_file_modified.xml new file mode 100644 index 0000000000..f21f6d331c --- /dev/null +++ b/app/src/main/res/drawable/ic_file_modified.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_renamed.xml b/app/src/main/res/drawable/ic_file_renamed.xml new file mode 100644 index 0000000000..b4bdc5c068 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_renamed.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/layout/dialog_git_diff.xml b/app/src/main/res/layout/dialog_git_diff.xml new file mode 100644 index 0000000000..6b59bea643 --- /dev/null +++ b/app/src/main/res/layout/dialog_git_diff.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_git_bottom_sheet.xml b/app/src/main/res/layout/fragment_git_bottom_sheet.xml new file mode 100644 index 0000000000..55b8249df5 --- /dev/null +++ b/app/src/main/res/layout/fragment_git_bottom_sheet.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/layout/item_git_file_change.xml b/app/src/main/res/layout/item_git_file_change.xml new file mode 100644 index 0000000000..a715843686 --- /dev/null +++ b/app/src/main/res/layout/item_git_file_change.xml @@ -0,0 +1,36 @@ + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 563068d8d0..e728ab5e68 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,4 +19,10 @@ Code on the Go Use simplified prompt + File added + File modified + File deleted + File untracked + File renamed + File conflicted diff --git a/git-core/src/main/java/com/itsaky/androidide/git/core/JGitRepository.kt b/git-core/src/main/java/com/itsaky/androidide/git/core/JGitRepository.kt index 50bcb752a3..791c502044 100644 --- a/git-core/src/main/java/com/itsaky/androidide/git/core/JGitRepository.kt +++ b/git-core/src/main/java/com/itsaky/androidide/git/core/JGitRepository.kt @@ -11,6 +11,16 @@ import java.io.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.eclipse.jgit.api.errors.NoHeadException +import org.eclipse.jgit.diff.DiffFormatter +import org.eclipse.jgit.dircache.DirCacheIterator +import org.eclipse.jgit.revwalk.RevWalk +import org.eclipse.jgit.treewalk.AbstractTreeIterator +import org.eclipse.jgit.treewalk.CanonicalTreeParser +import org.eclipse.jgit.treewalk.EmptyTreeIterator +import org.eclipse.jgit.treewalk.FileTreeIterator +import org.eclipse.jgit.treewalk.filter.PathFilter +import java.io.ByteArrayOutputStream /** * JGit-based implementation of the [GitRepository] interface. @@ -24,6 +34,18 @@ class JGitRepository(override val rootDir: File) : GitRepository { private val git: Git = Git(repository) + private fun getHeadTree(repository: Repository): AbstractTreeIterator { + val head = repository.resolve(Constants.HEAD) ?: return EmptyTreeIterator() + val treeParser = CanonicalTreeParser() + RevWalk(repository).use { revWalk -> + val commit = revWalk.parseCommit(head) + repository.newObjectReader().use { reader -> + treeParser.reset(reader, commit.tree.id) + } + } + return treeParser + } + override suspend fun getStatus(): GitStatus = withContext(Dispatchers.IO) { val jgitStatus = git.status().call() @@ -43,6 +65,8 @@ class JGitRepository(override val rootDir: File) : GitRepository { jgitStatus.conflicting.forEach { conflicted.add(FileChange(it, ChangeType.CONFLICTED)) } + + GitStatus( isClean = jgitStatus.isClean, hasConflicts = jgitStatus.conflicting.isNotEmpty(), @@ -81,13 +105,29 @@ class JGitRepository(override val rootDir: File) : GitRepository { git.log().setMaxCount(limit).call().map { revCommit -> revCommit.toGitCommit() } - } catch (_: org.eclipse.jgit.api.errors.NoHeadException) { + } catch (_: NoHeadException) { emptyList() } } override suspend fun getDiff(file: File): String = withContext(Dispatchers.IO) { - "" + val relativePath = file.toRelativeString(rootDir).replace('\\', '/') + val outputStream = ByteArrayOutputStream() + DiffFormatter(outputStream).use { formatter -> + formatter.setRepository(repository) + val indexTree = DirCacheIterator(repository.readDirCache()) + val workingTree = FileTreeIterator(repository) + formatter.pathFilter = PathFilter.create(relativePath) + formatter.format(indexTree, workingTree) + + // If empty, check staged diff + if (outputStream.size() == 0) { + val headTree = getHeadTree(repository) + val freshIndexTree = DirCacheIterator(repository.readDirCache()) + formatter.format(headTree, freshIndexTree) + } + } + outputStream.toString() } private fun RevCommit.toGitCommit(): GitCommit { diff --git a/resources/src/main/res/values/colors.xml b/resources/src/main/res/values/colors.xml index 3f393c9dde..3019563276 100755 --- a/resources/src/main/res/values/colors.xml +++ b/resources/src/main/res/values/colors.xml @@ -29,4 +29,12 @@ #233490 #E4E2E6 #00BE00 + + + #1B5E20 + #E8F5E9 + #B71C1C + #FFEBEE + #0277BD + diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml index e2c8f71966..2c7e566923 100644 --- a/resources/src/main/res/values/strings.xml +++ b/resources/src/main/res/values/strings.xml @@ -1162,4 +1162,9 @@ Clone failed: %1$s Cloning repository… Pick folder + Git + No uncommitted changes + Unable to load diff + Diff Viewer + Loading Git Diff…