Skip to content

Commit abca47f

Browse files
authored
Merge pull request #121 from trymist/public-git
Public git
2 parents 943241d + fb4f886 commit abca47f

13 files changed

Lines changed: 392 additions & 133 deletions

File tree

dash/src/components/applications/git-config.tsx

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { Label } from "@/components/ui/label"
44
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
55
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"
66
import { Button } from "@/components/ui/button"
7+
import { Input } from "@/components/ui/input"
78
import { toast } from "sonner"
89
import type { App } from "@/types/app"
9-
import { Github } from "lucide-react"
10+
import { Github, GitBranch } from "lucide-react"
1011
import { Skeleton } from "@/components/ui/skeleton"
1112

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

35+
// Public Git state
36+
const [publicGitUrl, setPublicGitUrl] = useState(app.gitCloneUrl || "")
37+
const [publicGitBranch, setPublicGitBranch] = useState(app.gitBranch || "main")
38+
const [isSavingPublicGit, setIsSavingPublicGit] = useState(false)
39+
3440
// this is for github app fetching
3541
// FIX: name of this function should be changed
3642
const fetchApp = async () => {
@@ -149,6 +155,38 @@ export const GitProviderTab = ({ app }: GitProviderTabProps) => {
149155
}
150156
}
151157

158+
const savePublicGitConfig = async () => {
159+
if (!publicGitUrl.trim()) {
160+
toast.error("Please enter a Git URL")
161+
return
162+
}
163+
164+
try {
165+
setIsSavingPublicGit(true)
166+
const res = await fetch("/api/apps/update", {
167+
method: "PUT",
168+
headers: { "Content-Type": "application/json" },
169+
credentials: "include",
170+
body: JSON.stringify({
171+
appId: app.id,
172+
gitProviderId: null,
173+
gitRepository: null,
174+
gitBranch: publicGitBranch || "main",
175+
gitCloneUrl: publicGitUrl.trim(),
176+
}),
177+
})
178+
179+
const data = await res.json()
180+
if (!data.success) throw new Error(data.error)
181+
182+
toast.success("Public Git configuration saved")
183+
} catch {
184+
toast.error("Failed to save configuration")
185+
} finally {
186+
setIsSavingPublicGit(false)
187+
}
188+
}
189+
152190
return (
153191
<Tabs defaultValue="github" value={provider} onValueChange={setProvider} className="w-full space-y-8">
154192

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

201+
<TabsTrigger value="public-git" className="flex items-center gap-2">
202+
<GitBranch className="h-4 w-4" />
203+
Public Git
204+
</TabsTrigger>
205+
163206
{/* <TabsTrigger value="gitlab" disabled className="flex items-center gap-2 opacity-70"> */}
164207
{/* <Gitlab className="h-4 w-4" /> */}
165208
{/* GitLab */}
@@ -298,6 +341,52 @@ export const GitProviderTab = ({ app }: GitProviderTabProps) => {
298341
)}
299342
</TabsContent>
300343

344+
{/* ✅ PUBLIC GIT TAB CONTENT */}
345+
<TabsContent value="public-git">
346+
<Card>
347+
<CardHeader>
348+
<CardTitle>Public Git Repository</CardTitle>
349+
<CardDescription>
350+
Deploy from any public Git repository by providing the URL and branch.
351+
</CardDescription>
352+
</CardHeader>
353+
354+
<CardContent className="space-y-6">
355+
<div className="flex flex-col md:flex-row gap-6">
356+
<div className="flex-1">
357+
<Label className="text-muted-foreground">Git URL</Label>
358+
<Input
359+
className="mt-2"
360+
placeholder="https://github.com/user/repo.git"
361+
value={publicGitUrl}
362+
onChange={(e) => setPublicGitUrl(e.target.value)}
363+
/>
364+
<p className="text-xs text-muted-foreground mt-1">
365+
Enter the full clone URL of your public repository
366+
</p>
367+
</div>
368+
369+
<div className="flex-1">
370+
<Label className="text-muted-foreground">Branch</Label>
371+
<Input
372+
className="mt-2"
373+
placeholder="main"
374+
value={publicGitBranch}
375+
onChange={(e) => setPublicGitBranch(e.target.value)}
376+
/>
377+
<p className="text-xs text-muted-foreground mt-1">
378+
The branch to deploy from (default: main)
379+
</p>
380+
</div>
381+
</div>
382+
383+
<Button onClick={savePublicGitConfig} disabled={isSavingPublicGit} className="w-fit">
384+
{isSavingPublicGit ? "Saving..." : "Save Configuration"}
385+
</Button>
386+
</CardContent>
387+
</Card>
388+
</TabsContent>
389+
301390
{/* ✅ OTHER PROVIDERS (Disabled) */}
302391
<TabsContent value="gitlab">
303392
<Card>

dash/src/features/applications/AppPage.tsx

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -97,18 +97,18 @@ export const AppPage = () => {
9797
<div className="w-full overflow-x-auto mb-6 pb-1">
9898
<TabsList className="inline-flex w-full min-w-fit">
9999
<TabsTrigger value="info">Info</TabsTrigger>
100-
{app.appType !== 'database' && <TabsTrigger value="git">Git</TabsTrigger>}
100+
{app.appType !== 'database' && <TabsTrigger value="sources">Sources</TabsTrigger>}
101101
<TabsTrigger value="environment">Environment</TabsTrigger>
102102
{app.appType === 'web' && <TabsTrigger value="domains">Domains</TabsTrigger>}
103103
<TabsTrigger value="deployments">Deployments</TabsTrigger>
104104
<TabsTrigger value="stats">Stats</TabsTrigger>
105105
<TabsTrigger value="logs">Logs</TabsTrigger>
106106
<TabsTrigger value="settings">Settings</TabsTrigger>
107-
</TabsList>
108-
</div>
107+
</TabsList >
108+
</div >
109109

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

122-
{app.appType !== 'database' && (
123-
<TabsContent value="git" className="space-y-6">
124-
<GitProviderTab app={app} />
125-
</TabsContent>
126-
)}
122+
{
123+
app.appType !== 'database' && (
124+
<TabsContent value="sources" className="space-y-6">
125+
<GitProviderTab app={app} />
126+
</TabsContent>
127+
)
128+
}
127129

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

133135
{/* ✅ DOMAINS TAB */}
134-
{app.appType === 'web' && (
135-
<TabsContent value="domains" className="space-y-6">
136-
<Domains appId={app.id} />
137-
</TabsContent>
138-
)}
136+
{
137+
app.appType === 'web' && (
138+
<TabsContent value="domains" className="space-y-6">
139+
<Domains appId={app.id} />
140+
</TabsContent>
141+
)
142+
}
139143

140144
{/* ✅ DEPLOYMENTS TAB */}
141145
<TabsContent value="deployments">
@@ -155,20 +159,21 @@ export const AppPage = () => {
155159
<AppSettings app={app} onUpdate={refreshApp} />
156160
<Volumes appId={app.id} appType={app.appType} />
157161
</TabsContent>
158-
</Tabs>
159-
</main>
162+
</Tabs >
163+
</main >
160164

161165
{/* Edit Modal */}
162-
<FormModal
166+
< FormModal
163167
isOpen={isModalOpen}
164168
onClose={() => setIsModalOpen(false)}
165169
title="Edit App"
166-
fields={[
167-
{ label: "App Name", name: "name", type: "text", defaultValue: app.name },
168-
{ label: "Description", name: "description", type: "textarea", defaultValue: app.description || "" },
169-
]}
170+
fields={
171+
[
172+
{ label: "App Name", name: "name", type: "text", defaultValue: app.name },
173+
{ label: "Description", name: "description", type: "textarea", defaultValue: app.description || "" },
174+
]}
170175
onSubmit={(data) => handleUpdateApp(data as { name: string; description: string })}
171176
/>
172-
</div>
177+
</div >
173178
);
174179
};

server/api/handlers/applications/getLatestCommit.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66

77
"github.com/corecollectives/mist/api/handlers"
88
"github.com/corecollectives/mist/api/middleware"
9-
"github.com/corecollectives/mist/github"
9+
"github.com/corecollectives/mist/git"
1010
"github.com/corecollectives/mist/models"
1111
)
1212

@@ -56,7 +56,7 @@ func GetLatestCommit(w http.ResponseWriter, r *http.Request) {
5656
return
5757
}
5858

59-
commit, err := github.GetLatestCommit(req.AppID, userInfo.ID)
59+
commit, err := git.GetLatestCommit(req.AppID, userInfo.ID)
6060
if err != nil {
6161
handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "Failed to get latest commit", err.Error())
6262
return

server/api/handlers/deployments/AddDeployHandler.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66

77
"github.com/corecollectives/mist/api/handlers"
88
"github.com/corecollectives/mist/api/middleware"
9-
"github.com/corecollectives/mist/github"
9+
"github.com/corecollectives/mist/git"
1010
"github.com/corecollectives/mist/models"
1111
"github.com/corecollectives/mist/queue"
1212
"github.com/rs/zerolog/log"
@@ -39,7 +39,7 @@ func AddDeployHandler(w http.ResponseWriter, r *http.Request) {
3939

4040
if app.AppType != models.AppTypeDatabase {
4141
userId := int64(user.ID)
42-
commit, err := github.GetLatestCommit(int64(req.AppId), userId)
42+
commit, err := git.GetLatestCommit(int64(req.AppId), userId)
4343
if err != nil {
4444
log.Error().Err(err).Int("app_id", req.AppId).Msg("Error getting latest commit")
4545
handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "failed to get latest commit", err.Error())

server/git/clone.go

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ import (
44
"context"
55
"fmt"
66
"os"
7+
"time"
78

9+
"github.com/corecollectives/mist/github"
10+
"github.com/corecollectives/mist/models"
811
"github.com/go-git/go-git/v6"
912
"github.com/go-git/go-git/v6/plumbing"
1013
"github.com/rs/zerolog/log"
1114
)
1215

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

3134
return nil
3235
}
36+
37+
func CloneRepo(ctx context.Context, appId int64, logFile *os.File) error {
38+
log.Info().Int64("app_id", appId).Msg("Starting repository clone")
39+
40+
userId, err := models.GetUserIDByAppID(appId)
41+
if err != nil {
42+
return fmt.Errorf("failed to get user id by app id: %w", err)
43+
}
44+
45+
cloneURL, accessToken, shouldMigrate, err := models.GetAppCloneURL(appId, *userId)
46+
if err != nil {
47+
return fmt.Errorf("failed to get clone URL: %w", err)
48+
}
49+
50+
_, _, branch, _, projectId, name, err := models.GetAppGitInfo(appId)
51+
if err != nil {
52+
return fmt.Errorf("failed to fetch app: %w", err)
53+
}
54+
55+
if shouldMigrate {
56+
log.Info().Int64("app_id", appId).Msg("Migrating legacy app to new git format")
57+
// for legacy GitHub apps, we don't have a git_provider_id
58+
// we just update the git_clone_url
59+
err = models.UpdateAppGitCloneURL(appId, cloneURL, nil)
60+
if err != nil {
61+
log.Warn().Err(err).Int64("app_id", appId).Msg("Failed to migrate app git info, continuing anyway")
62+
}
63+
}
64+
65+
// construct authenticated clone URL if we have an access token
66+
repoURL := cloneURL
67+
if accessToken != "" {
68+
// insert token into the URL
69+
// for GitHub: https://x-access-token:TOKEN@github.com/user/repo.git
70+
// for GitLab: https://oauth2:TOKEN@gitlab.com/user/repo.git
71+
// for Bitbucket: https://x-token-auth:TOKEN@bitbucket.org/user/repo.git
72+
// for Gitea: https://TOKEN@gitea.com/user/repo.git
73+
74+
// simple approach: insert token after https://
75+
// if len(cloneURL) > 8 && cloneURL[:8] == "https://" {
76+
// repoURL = fmt.Sprintf("https://x-access-token:%s@%s", accessToken, cloneURL[8:])
77+
// }
78+
repoURL = github.CreateCloneUrl(accessToken, repoURL)
79+
}
80+
81+
path := fmt.Sprintf("/var/lib/mist/projects/%d/apps/%s", projectId, name)
82+
83+
if _, err := os.Stat(path + "/.git"); err == nil {
84+
log.Info().Str("path", path).Msg("Repository already exists, removing directory")
85+
86+
if err := os.RemoveAll(path); err != nil {
87+
return fmt.Errorf("failed to remove existing repository: %w", err)
88+
89+
}
90+
}
91+
92+
if err := os.MkdirAll(path, 0o755); err != nil {
93+
return fmt.Errorf("failed to create directory: %w", err)
94+
}
95+
96+
log.Info().Str("clone_url", cloneURL).Str("branch", branch).Str("path", path).Msg("Cloning repository")
97+
98+
ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
99+
defer cancel()
100+
101+
// old command implementation
102+
//
103+
//
104+
// cmd := exec.CommandContext(ctx, "git", "clone", "--branch", branch, repoURL, path)
105+
// output, err := cmd.CombinedOutput()
106+
// lines := strings.Split(string(output), "\n")
107+
// for _, line := range lines {
108+
// if len(line) > 0 {
109+
// fmt.Fprintf(logFile, "[GIT] %s\n", line)
110+
// }
111+
// }
112+
113+
// new git sdk implementation
114+
err = CloneGitRepo(ctx, repoURL, branch, logFile, path)
115+
if err != nil {
116+
if ctx.Err() == context.DeadlineExceeded {
117+
return fmt.Errorf("git clone timed out after 10 minutes")
118+
}
119+
return fmt.Errorf("error cloning repository: %v\n", err)
120+
}
121+
122+
log.Info().Int64("app_id", appId).Str("path", path).Msg("Repository cloned successfully")
123+
return nil
124+
}

0 commit comments

Comments
 (0)