Skip to content
Merged
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
91 changes: 90 additions & 1 deletion dash/src/components/applications/git-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { Label } from "@/components/ui/label"
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { toast } from "sonner"
import type { App } from "@/types/app"
import { Github } from "lucide-react"
import { Github, GitBranch } from "lucide-react"
import { Skeleton } from "@/components/ui/skeleton"

interface GitProviderTabProps {
Expand All @@ -31,6 +32,11 @@ export const GitProviderTab = ({ app }: GitProviderTabProps) => {
const [isRepoLoading, setIsRepoLoading] = useState(true)
const [isBranchLoading, setIsBranchLoading] = useState(false)

// Public Git state
const [publicGitUrl, setPublicGitUrl] = useState(app.gitCloneUrl || "")
const [publicGitBranch, setPublicGitBranch] = useState(app.gitBranch || "main")
const [isSavingPublicGit, setIsSavingPublicGit] = useState(false)

// this is for github app fetching
// FIX: name of this function should be changed
const fetchApp = async () => {
Expand Down Expand Up @@ -149,6 +155,38 @@ export const GitProviderTab = ({ app }: GitProviderTabProps) => {
}
}

const savePublicGitConfig = async () => {
if (!publicGitUrl.trim()) {
toast.error("Please enter a Git URL")
return
}

try {
setIsSavingPublicGit(true)
const res = await fetch("/api/apps/update", {
method: "PUT",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
appId: app.id,
gitProviderId: null,
gitRepository: null,
gitBranch: publicGitBranch || "main",
gitCloneUrl: publicGitUrl.trim(),
}),
})

const data = await res.json()
if (!data.success) throw new Error(data.error)

toast.success("Public Git configuration saved")
} catch {
toast.error("Failed to save configuration")
} finally {
setIsSavingPublicGit(false)
}
}

return (
<Tabs defaultValue="github" value={provider} onValueChange={setProvider} className="w-full space-y-8">

Expand All @@ -160,6 +198,11 @@ export const GitProviderTab = ({ app }: GitProviderTabProps) => {
GitHub
</TabsTrigger>

<TabsTrigger value="public-git" className="flex items-center gap-2">
<GitBranch className="h-4 w-4" />
Public Git
</TabsTrigger>

{/* <TabsTrigger value="gitlab" disabled className="flex items-center gap-2 opacity-70"> */}
{/* <Gitlab className="h-4 w-4" /> */}
{/* GitLab */}
Expand Down Expand Up @@ -298,6 +341,52 @@ export const GitProviderTab = ({ app }: GitProviderTabProps) => {
)}
</TabsContent>

{/* ✅ PUBLIC GIT TAB CONTENT */}
<TabsContent value="public-git">
<Card>
<CardHeader>
<CardTitle>Public Git Repository</CardTitle>
<CardDescription>
Deploy from any public Git repository by providing the URL and branch.
</CardDescription>
</CardHeader>

<CardContent className="space-y-6">
<div className="flex flex-col md:flex-row gap-6">
<div className="flex-1">
<Label className="text-muted-foreground">Git URL</Label>
<Input
className="mt-2"
placeholder="https://github.com/user/repo.git"
value={publicGitUrl}
onChange={(e) => setPublicGitUrl(e.target.value)}
/>
<p className="text-xs text-muted-foreground mt-1">
Enter the full clone URL of your public repository
</p>
</div>

<div className="flex-1">
<Label className="text-muted-foreground">Branch</Label>
<Input
className="mt-2"
placeholder="main"
value={publicGitBranch}
onChange={(e) => setPublicGitBranch(e.target.value)}
/>
<p className="text-xs text-muted-foreground mt-1">
The branch to deploy from (default: main)
</p>
</div>
</div>

<Button onClick={savePublicGitConfig} disabled={isSavingPublicGit} className="w-fit">
{isSavingPublicGit ? "Saving..." : "Save Configuration"}
</Button>
</CardContent>
</Card>
</TabsContent>

{/* ✅ OTHER PROVIDERS (Disabled) */}
<TabsContent value="gitlab">
<Card>
Expand Down
51 changes: 28 additions & 23 deletions dash/src/features/applications/AppPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,18 @@ export const AppPage = () => {
<div className="w-full overflow-x-auto mb-6 pb-1">
<TabsList className="inline-flex w-full min-w-fit">
<TabsTrigger value="info">Info</TabsTrigger>
{app.appType !== 'database' && <TabsTrigger value="git">Git</TabsTrigger>}
{app.appType !== 'database' && <TabsTrigger value="sources">Sources</TabsTrigger>}
<TabsTrigger value="environment">Environment</TabsTrigger>
{app.appType === 'web' && <TabsTrigger value="domains">Domains</TabsTrigger>}
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="stats">Stats</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
</div>
</TabsList >
</div >

{/* ✅ INFO TAB */}
<TabsContent value="info" className="space-y-6">
< TabsContent value="info" className="space-y-6" >
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
<div className="xl:col-span-2">
<AppInfo app={app} latestCommit={latestCommit} />
Expand All @@ -117,25 +117,29 @@ export const AppPage = () => {
<AppStats appId={app.id} appStatus={app.status} app={app} previewUrl={previewUrl} onStatusChange={refreshApp} />
</div>
</div>
</TabsContent>
</TabsContent >

{app.appType !== 'database' && (
<TabsContent value="git" className="space-y-6">
<GitProviderTab app={app} />
</TabsContent>
)}
{
app.appType !== 'database' && (
<TabsContent value="sources" className="space-y-6">
<GitProviderTab app={app} />
</TabsContent>
)
}

{/* ✅ ENVIRONMENT TAB */}
<TabsContent value="environment" className="space-y-6">
<EnvironmentVariables appId={app.id} />
</TabsContent>

{/* ✅ DOMAINS TAB */}
{app.appType === 'web' && (
<TabsContent value="domains" className="space-y-6">
<Domains appId={app.id} />
</TabsContent>
)}
{
app.appType === 'web' && (
<TabsContent value="domains" className="space-y-6">
<Domains appId={app.id} />
</TabsContent>
)
}

{/* ✅ DEPLOYMENTS TAB */}
<TabsContent value="deployments">
Expand All @@ -155,20 +159,21 @@ export const AppPage = () => {
<AppSettings app={app} onUpdate={refreshApp} />
<Volumes appId={app.id} appType={app.appType} />
</TabsContent>
</Tabs>
</main>
</Tabs >
</main >

{/* Edit Modal */}
<FormModal
< FormModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
title="Edit App"
fields={[
{ label: "App Name", name: "name", type: "text", defaultValue: app.name },
{ label: "Description", name: "description", type: "textarea", defaultValue: app.description || "" },
]}
fields={
[
{ label: "App Name", name: "name", type: "text", defaultValue: app.name },
{ label: "Description", name: "description", type: "textarea", defaultValue: app.description || "" },
]}
onSubmit={(data) => handleUpdateApp(data as { name: string; description: string })}
/>
</div>
</div >
);
};
4 changes: 2 additions & 2 deletions server/api/handlers/applications/getLatestCommit.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

"github.com/corecollectives/mist/api/handlers"
"github.com/corecollectives/mist/api/middleware"
"github.com/corecollectives/mist/github"
"github.com/corecollectives/mist/git"
"github.com/corecollectives/mist/models"
)

Expand Down Expand Up @@ -56,7 +56,7 @@ func GetLatestCommit(w http.ResponseWriter, r *http.Request) {
return
}

commit, err := github.GetLatestCommit(req.AppID, userInfo.ID)
commit, err := git.GetLatestCommit(req.AppID, userInfo.ID)
if err != nil {
handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "Failed to get latest commit", err.Error())
return
Expand Down
4 changes: 2 additions & 2 deletions server/api/handlers/deployments/AddDeployHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

"github.com/corecollectives/mist/api/handlers"
"github.com/corecollectives/mist/api/middleware"
"github.com/corecollectives/mist/github"
"github.com/corecollectives/mist/git"
"github.com/corecollectives/mist/models"
"github.com/corecollectives/mist/queue"
"github.com/rs/zerolog/log"
Expand Down Expand Up @@ -39,7 +39,7 @@ func AddDeployHandler(w http.ResponseWriter, r *http.Request) {

if app.AppType != models.AppTypeDatabase {
userId := int64(user.ID)
commit, err := github.GetLatestCommit(int64(req.AppId), userId)
commit, err := git.GetLatestCommit(int64(req.AppId), userId)
if err != nil {
log.Error().Err(err).Int("app_id", req.AppId).Msg("Error getting latest commit")
handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "failed to get latest commit", err.Error())
Expand Down
94 changes: 93 additions & 1 deletion server/git/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import (
"context"
"fmt"
"os"
"time"

"github.com/corecollectives/mist/github"
"github.com/corecollectives/mist/models"
"github.com/go-git/go-git/v6"
"github.com/go-git/go-git/v6/plumbing"
"github.com/rs/zerolog/log"
)

func CloneRepo(ctx context.Context, url string, branch string, logFile *os.File, path string) error {
func CloneGitRepo(ctx context.Context, url string, branch string, logFile *os.File, path string) error {
_, err := fmt.Fprintf(logFile, "[GIT]: Cloning into %s\n", path)
if err != nil {
log.Warn().Msg("error logging into log file")
Expand All @@ -30,3 +33,92 @@ func CloneRepo(ctx context.Context, url string, branch string, logFile *os.File,

return nil
}

func CloneRepo(ctx context.Context, appId int64, logFile *os.File) error {
log.Info().Int64("app_id", appId).Msg("Starting repository clone")

userId, err := models.GetUserIDByAppID(appId)
if err != nil {
return fmt.Errorf("failed to get user id by app id: %w", err)
}

cloneURL, accessToken, shouldMigrate, err := models.GetAppCloneURL(appId, *userId)
if err != nil {
return fmt.Errorf("failed to get clone URL: %w", err)
}

_, _, branch, _, projectId, name, err := models.GetAppGitInfo(appId)
if err != nil {
return fmt.Errorf("failed to fetch app: %w", err)
}

if shouldMigrate {
log.Info().Int64("app_id", appId).Msg("Migrating legacy app to new git format")
// for legacy GitHub apps, we don't have a git_provider_id
// we just update the git_clone_url
err = models.UpdateAppGitCloneURL(appId, cloneURL, nil)
if err != nil {
log.Warn().Err(err).Int64("app_id", appId).Msg("Failed to migrate app git info, continuing anyway")
}
}

// construct authenticated clone URL if we have an access token
repoURL := cloneURL
if accessToken != "" {
// insert token into the URL
// for GitHub: https://x-access-token:TOKEN@github.com/user/repo.git
// for GitLab: https://oauth2:TOKEN@gitlab.com/user/repo.git
// for Bitbucket: https://x-token-auth:TOKEN@bitbucket.org/user/repo.git
// for Gitea: https://TOKEN@gitea.com/user/repo.git

// simple approach: insert token after https://
// if len(cloneURL) > 8 && cloneURL[:8] == "https://" {
// repoURL = fmt.Sprintf("https://x-access-token:%s@%s", accessToken, cloneURL[8:])
// }
repoURL = github.CreateCloneUrl(accessToken, repoURL)
}

path := fmt.Sprintf("/var/lib/mist/projects/%d/apps/%s", projectId, name)

if _, err := os.Stat(path + "/.git"); err == nil {
log.Info().Str("path", path).Msg("Repository already exists, removing directory")

if err := os.RemoveAll(path); err != nil {
return fmt.Errorf("failed to remove existing repository: %w", err)

}
}

if err := os.MkdirAll(path, 0o755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}

log.Info().Str("clone_url", cloneURL).Str("branch", branch).Str("path", path).Msg("Cloning repository")

ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
defer cancel()

// old command implementation
//
//
// cmd := exec.CommandContext(ctx, "git", "clone", "--branch", branch, repoURL, path)
// output, err := cmd.CombinedOutput()
// lines := strings.Split(string(output), "\n")
// for _, line := range lines {
// if len(line) > 0 {
// fmt.Fprintf(logFile, "[GIT] %s\n", line)
// }
// }

// new git sdk implementation
err = CloneGitRepo(ctx, repoURL, branch, logFile, path)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
return fmt.Errorf("git clone timed out after 10 minutes")
}
return fmt.Errorf("error cloning repository: %v\n", err)
}

log.Info().Int64("app_id", appId).Str("path", path).Msg("Repository cloned successfully")
return nil
}
Loading
Loading