Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 54
ADFA-2878: Create git module#962
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
c42458316742717fa6acd64170ec6293cbf36965bd3541cdbcd2c893b305e2ceb2eb63094b8d0aca9343d38174aFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| /build |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| plugins { | ||
| alias(libs.plugins.android.library) | ||
| alias(libs.plugins.kotlin.android) | ||
| } | ||
| android { | ||
| namespace = "com.itsaky.androidide.git.core" | ||
| compileSdk = 35 | ||
| defaultConfig { | ||
| minSdk = 27 | ||
| testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | ||
| consumerProguardFiles("consumer-rules.pro") | ||
| } | ||
| buildTypes { | ||
| release { | ||
| isMinifyEnabled = false | ||
| proguardFiles( | ||
| getDefaultProguardFile("proguard-android-optimize.txt"), | ||
| "proguard-rules.pro" | ||
| ) | ||
| } | ||
| } | ||
| compileOptions { | ||
| sourceCompatibility = JavaVersion.VERSION_17 | ||
| targetCompatibility = JavaVersion.VERSION_17 | ||
| isCoreLibraryDesugaringEnabled = true | ||
| } | ||
| kotlinOptions { | ||
| jvmTarget = "17" | ||
| } | ||
| } | ||
| dependencies { | ||
| implementation(libs.androidx.core.ktx.v1120) | ||
| implementation(libs.androidx.appcompat.v171) | ||
| implementation(libs.google.material) | ||
| implementation(libs.git.jgit) | ||
| coreLibraryDesugaring(libs.desugar.jdk.libs.v215) | ||
| implementation(libs.common.kotlin.coroutines.core) | ||
| implementation(libs.common.kotlin.coroutines.android) | ||
| testImplementation(libs.tests.junit) | ||
| androidTestImplementation(libs.tests.androidx.junit) | ||
| androidTestImplementation(libs.tests.androidx.espresso.core) | ||
| } | ||
dara-abijo-adfa marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # Add project specific ProGuard rules here. | ||
| # You can control the set of applied configuration files using the | ||
| # proguardFiles setting in build.gradle. | ||
| # | ||
| # For more details, see | ||
| # http://developer.android.com/guide/developing/tools/proguard.html | ||
| # If your project uses WebView with JS, uncomment the following | ||
| # and specify the fully qualified class name to the JavaScript interface | ||
| # class: | ||
| #-keepclassmembers class fqcn.of.javascript.interface.for.webview { | ||
| # public *; | ||
| #} | ||
| # Uncomment this to preserve the line number information for | ||
| # debugging stack traces. | ||
| #-keepattributes SourceFile,LineNumberTable | ||
| # If you keep the line number information, uncomment this to | ||
| # hide the original source file name. | ||
| #-renamesourcefileattribute SourceFile |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package com.itsaky.androidide.git.core | ||
| import com.itsaky.androidide.git.core.models.GitBranch | ||
| import com.itsaky.androidide.git.core.models.GitCommit | ||
| import com.itsaky.androidide.git.core.models.GitStatus | ||
| import java.io.File | ||
| import java.io.Closeable | ||
| /** | ||
| * Interface defining core Git repository operations. | ||
| */ | ||
| interface GitRepository : Closeable { | ||
| val rootDir: File | ||
| suspend fun getStatus(): GitStatus | ||
| suspend fun getCurrentBranch(): GitBranch? | ||
| suspend fun getBranches(): List<GitBranch> | ||
| suspend fun getHistory(limit: Int = 50): List<GitCommit> | ||
| suspend fun getDiff(file: File): String | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| package com.itsaky.androidide.git.core | ||
| import android.util.Log | ||
| import org.eclipse.jgit.storage.file.FileRepositoryBuilder | ||
| import java.io.File | ||
| import kotlinx.coroutines.Dispatchers | ||
| import kotlinx.coroutines.withContext | ||
| /** | ||
| * Utility class for detecting and opening Git repositories. | ||
| */ | ||
| object GitRepositoryManager { | ||
dara-abijo-adfa marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| /** | ||
| * Checks if a directory is a Git repository. | ||
| */ | ||
| suspend fun isRepository(dir: File): Boolean = withContext(Dispatchers.IO) { | ||
| try { | ||
| FileRepositoryBuilder() | ||
| .readEnvironment() | ||
| .findGitDir(dir) | ||
| .build().use { repository -> | ||
| repository.objectDatabase.exists() | ||
| } | ||
| } catch (e: Exception) { | ||
| Log.e("GitRepositoryManager", "Error checking Git repository: ${e.message}") | ||
| false | ||
| } | ||
| } | ||
| /** | ||
| * Opens a Git repository at the given directory. | ||
| * Returns null if no repository is found. | ||
| */ | ||
| suspend fun openRepository(dir: File): GitRepository? = withContext(Dispatchers.IO) { | ||
| if (isRepository(dir)) { | ||
| JGitRepository(dir) | ||
| } else { | ||
| null | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| package com.itsaky.androidide.git.core | ||
| import com.itsaky.androidide.git.core.models.* | ||
| import org.eclipse.jgit.api.Git | ||
| import org.eclipse.jgit.api.ListBranchCommand.ListMode | ||
| import org.eclipse.jgit.lib.Constants | ||
| import org.eclipse.jgit.lib.Repository | ||
| import org.eclipse.jgit.revwalk.RevCommit | ||
| import org.eclipse.jgit.storage.file.FileRepositoryBuilder | ||
| import java.io.File | ||
| import kotlinx.coroutines.Dispatchers | ||
| import kotlinx.coroutines.withContext | ||
| /** | ||
| * JGit-based implementation of the [GitRepository] interface. | ||
| */ | ||
| class JGitRepository(override val rootDir: File) : GitRepository { | ||
| private val repository: Repository = FileRepositoryBuilder() | ||
dara-abijo-adfa marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| .setWorkTree(rootDir) | ||
| .findGitDir(rootDir) | ||
| .build() | ||
| private val git: Git = Git(repository) | ||
dara-abijo-adfa marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| override suspend fun getStatus(): GitStatus = withContext(Dispatchers.IO) { | ||
| val jgitStatus = git.status().call() | ||
| val staged = mutableListOf<FileChange>() | ||
| val unstaged = mutableListOf<FileChange>() | ||
| val untracked = mutableListOf<FileChange>() | ||
| val conflicted = mutableListOf<FileChange>() | ||
| jgitStatus.added.forEach { staged.add(FileChange(it, ChangeType.ADDED)) } | ||
| jgitStatus.changed.forEach { staged.add(FileChange(it, ChangeType.MODIFIED)) } | ||
| jgitStatus.removed.forEach { staged.add(FileChange(it, ChangeType.DELETED)) } | ||
| jgitStatus.modified.forEach { unstaged.add(FileChange(it, ChangeType.MODIFIED)) } | ||
| jgitStatus.missing.forEach { unstaged.add(FileChange(it, ChangeType.DELETED)) } | ||
| jgitStatus.untracked.forEach { untracked.add(FileChange(it, ChangeType.UNTRACKED)) } | ||
| jgitStatus.conflicting.forEach { conflicted.add(FileChange(it, ChangeType.CONFLICTED)) } | ||
| GitStatus( | ||
| isClean = jgitStatus.isClean, | ||
| hasConflicts = jgitStatus.conflicting.isNotEmpty(), | ||
| staged = staged, | ||
| unstaged = unstaged, | ||
| untracked = untracked, | ||
| conflicted = conflicted | ||
| ) | ||
| } | ||
| override suspend fun getCurrentBranch(): GitBranch? = withContext(Dispatchers.IO) { | ||
| val head = repository.fullBranch ?: return@withContext null | ||
| val shortName = repository.branch ?: head | ||
| GitBranch( | ||
| name = shortName, | ||
| fullName = head, | ||
| isCurrent = true, | ||
| isRemote = head.startsWith(Constants.R_REMOTES) | ||
| ) | ||
| } | ||
| override suspend fun getBranches(): List<GitBranch> = withContext(Dispatchers.IO) { | ||
| val currentBranch = repository.fullBranch | ||
| git.branchList().setListMode(ListMode.ALL).call().map { ref -> | ||
| GitBranch( | ||
| name = Repository.shortenRefName(ref.name), | ||
| fullName = ref.name, | ||
| isCurrent = ref.name == currentBranch, | ||
| isRemote = ref.name.startsWith(Constants.R_REMOTES) | ||
| ) | ||
| } | ||
| } | ||
| override suspend fun getHistory(limit: Int): List<GitCommit> = withContext(Dispatchers.IO) { | ||
| try { | ||
| git.log().setMaxCount(limit).call().map { revCommit -> | ||
| revCommit.toGitCommit() | ||
| } | ||
| } catch (_: org.eclipse.jgit.api.errors.NoHeadException) { | ||
| emptyList() | ||
| } | ||
| } | ||
| override suspend fun getDiff(file: File): String = withContext(Dispatchers.IO) { | ||
| "" | ||
| } | ||
| private fun RevCommit.toGitCommit(): GitCommit { | ||
| val author = authorIdent | ||
| return GitCommit( | ||
| hash = name, | ||
| shortHash = name.take(7), | ||
| authorName = author.name, | ||
| authorEmail = author.emailAddress, | ||
| message = fullMessage, | ||
| timestamp = author.`when`.time, | ||
| parentHashes = parents.map { it.name } | ||
| ) | ||
| } | ||
| override fun close() { | ||
| repository.close() | ||
| git.close() | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.itsaky.androidide.git.core.models | ||
| /** | ||
| * Represents the type of change in a file tracked by Git. | ||
| */ | ||
| enum class ChangeType { | ||
| ADDED, | ||
| MODIFIED, | ||
| DELETED, | ||
| UNTRACKED, | ||
| RENAMED, | ||
| CONFLICTED | ||
| } | ||
| /** | ||
| * Represents a single file change in the repository. | ||
| */ | ||
| data class FileChange( | ||
| val path: String, | ||
| val type: ChangeType, | ||
| val oldPath: String? = null | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.itsaky.androidide.git.core.models | ||
| /** | ||
| * Represents a Git branch. | ||
| */ | ||
| data class GitBranch( | ||
| val name: String, | ||
| val fullName: String, | ||
| val isCurrent: Boolean, | ||
| val isRemote: Boolean, | ||
| val remoteName: String? = null | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.itsaky.androidide.git.core.models | ||
| /** | ||
| * Represents a Git commit. | ||
| */ | ||
| data class GitCommit( | ||
| val hash: String, | ||
| val shortHash: String, | ||
| val authorName: String, | ||
| val authorEmail: String, | ||
| val message: String, | ||
| val timestamp: Long, | ||
| val parentHashes: List<String> | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package com.itsaky.androidide.git.core.models | ||
| /** | ||
| * Represents the aggregate status of a Git repository. | ||
| */ | ||
| data class GitStatus( | ||
| val isClean: Boolean, | ||
| val hasConflicts: Boolean, | ||
| val staged: List<FileChange>, | ||
| val unstaged: List<FileChange>, | ||
| val untracked: List<FileChange>, | ||
| val conflicted: List<FileChange> | ||
| ) { | ||
| companion object { | ||
| val EMPTY = GitStatus( | ||
| isClean = true, | ||
| hasConflicts = false, | ||
| staged = emptyList(), | ||
| unstaged = emptyList(), | ||
| untracked = emptyList(), | ||
| conflicted = emptyList() | ||
| ) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package com.itsaky.androidide.git.core | ||
| import com.itsaky.androidide.git.core.models.* | ||
| import org.junit.Assert.* | ||
| import org.junit.Test | ||
| /** | ||
| * Unit tests for Git models. | ||
| */ | ||
| class GitModelsTest { | ||
| @Test | ||
| fun testGitStatusEmpty() { | ||
| val status = GitStatus.EMPTY | ||
| assertTrue(status.isClean) | ||
| assertFalse(status.hasConflicts) | ||
| assertTrue(status.staged.isEmpty()) | ||
| assertTrue(status.unstaged.isEmpty()) | ||
| assertTrue(status.untracked.isEmpty()) | ||
| assertTrue(status.conflicted.isEmpty()) | ||
| } | ||
| @Test | ||
| fun testFileChange() { | ||
| val change = FileChange("path/to/file.txt", ChangeType.MODIFIED) | ||
| assertEquals("path/to/file.txt", change.path) | ||
| assertEquals(ChangeType.MODIFIED, change.type) | ||
| assertNull(change.oldPath) | ||
| } | ||
| @Test | ||
| fun testGitBranch() { | ||
| val branch = GitBranch( | ||
| name = "main", | ||
| fullName = "refs/heads/main", | ||
| isCurrent = true, | ||
| isRemote = false | ||
| ) | ||
| assertEquals("main", branch.name) | ||
| assertTrue(branch.isCurrent) | ||
| assertFalse(branch.isRemote) | ||
| } | ||
| @Test | ||
| fun testGitCommit() { | ||
| val commit = GitCommit( | ||
| hash = "abcdef1234567890", | ||
| shortHash = "abcdef1", | ||
| authorName = "Author", | ||
| authorEmail = "author@example.com", | ||
| message = "Commit message", | ||
| timestamp = 1234567890L, | ||
| parentHashes = emptyList() | ||
| ) | ||
| assertEquals("abcdef1234567890", commit.hash) | ||
| assertEquals("abcdef1", commit.shortHash) | ||
| assertEquals("Commit message", commit.message) | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.