Skip to content

Commit df281ef

Browse files
committed
Add support for optional repository URL in Tapping
1 parent 154c8f5 commit df281ef

18 files changed

Lines changed: 119 additions & 45 deletions

File tree

app.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -627,8 +627,8 @@ func (a *App) UpdateAllBrewPackages() string {
627627
return a.brewService.UpdateAllBrewPackages(a.ctx)
628628
}
629629

630-
func (a *App) TapBrewRepository(repositoryName string) string {
631-
return a.brewService.TapBrewRepository(a.ctx, repositoryName)
630+
func (a *App) TapBrewRepository(repositoryName, repositoryURL string) string {
631+
return a.brewService.TapBrewRepository(a.ctx, repositoryName, repositoryURL)
632632
}
633633

634634
func (a *App) UntapBrewRepository(repositoryName string) string {

backend/brew/service.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ type Service interface {
5353
UpdateAllBrewPackages(ctx context.Context) string
5454

5555
// Tap operations
56-
TapBrewRepository(ctx context.Context, repositoryName string) string
56+
TapBrewRepository(ctx context.Context, repositoryName, repositoryURL string) string
5757
UntapBrewRepository(ctx context.Context, repositoryName string) string
5858

5959
// Package info
@@ -298,8 +298,8 @@ func (s *serviceImpl) UpdateAllBrewPackages(ctx context.Context) string {
298298
}
299299

300300
// Tap methods
301-
func (s *serviceImpl) TapBrewRepository(ctx context.Context, repositoryName string) string {
302-
return s.tapService.TapBrewRepository(ctx, repositoryName)
301+
func (s *serviceImpl) TapBrewRepository(ctx context.Context, repositoryName, repositoryURL string) string {
302+
return s.tapService.TapBrewRepository(ctx, repositoryName, repositoryURL)
303303
}
304304

305305
func (s *serviceImpl) UntapBrewRepository(ctx context.Context, repositoryName string) string {

backend/brew/tap.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,18 @@ func NewTapService(
3333
}
3434
}
3535

36-
// TapBrewRepository taps a repository with live progress updates
37-
func (s *TapService) TapBrewRepository(ctx context.Context, repositoryName string) string {
36+
// TapBrewRepository taps a repository with live progress updates.
37+
// repositoryURL is optional; when provided it is passed as the second argument to brew tap.
38+
func (s *TapService) TapBrewRepository(ctx context.Context, repositoryName, repositoryURL string) string {
3839
// Emit initial progress
3940
startMessage := s.getBackendMsg("tapStart", map[string]string{"name": repositoryName})
4041
s.eventEmitter.Emit("repositoryTapProgress", startMessage)
4142

42-
cmd := exec.Command(s.brewPath, "tap", repositoryName)
43+
args := []string{"tap", repositoryName}
44+
if url := strings.TrimSpace(repositoryURL); url != "" {
45+
args = append(args, url)
46+
}
47+
cmd := exec.Command(s.brewPath, args...)
4348
cmd.Env = append(os.Environ(), s.getBrewEnvFunc()...)
4449

4550
// Create pipes for real-time output

frontend/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1675,7 +1675,7 @@ const WailBrewApp = () => {
16751675
}
16761676
};
16771677

1678-
const handleTapConfirmed = async (tapName: string) => {
1678+
const handleTapConfirmed = async (tapName: string, tapURL: string) => {
16791679
setShowTapInput(false);
16801680
setTappingRepository(tapName);
16811681
setTapLogs(t('dialogs.tapping', { name: tapName }));
@@ -1839,7 +1839,7 @@ const WailBrewApp = () => {
18391839
});
18401840

18411841
// Start the tap process
1842-
await TapBrewRepository(tapName);
1842+
await TapBrewRepository(tapName, tapURL);
18431843
};
18441844

18451845
const handleInstallPackage = (pkg: PackageEntry) => {

frontend/src/components/TapInputDialog.tsx

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,53 +3,70 @@ import { useTranslation } from "react-i18next";
33

44
interface TapInputDialogProps {
55
open: boolean;
6-
onConfirm: (tapName: string) => void;
6+
onConfirm: (tapName: string, tapURL: string) => void;
77
onCancel: () => void;
88
}
99

10+
const tapNamePattern = /^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?\/[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
11+
const tapURLPattern = /^(https?:\/\/|git@|ssh:\/\/|git:\/\/|rsync:\/\/).+/;
12+
13+
const inputStyle = (hasError: boolean): React.CSSProperties => ({
14+
width: '100%',
15+
padding: '0.5rem',
16+
fontSize: '1rem',
17+
border: hasError ? '2px solid #dc2626' : '1px solid #444',
18+
borderRadius: '4px',
19+
backgroundColor: '#1e293b',
20+
color: '#fff',
21+
outline: 'none',
22+
});
23+
1024
const TapInputDialog: React.FC<TapInputDialogProps> = ({ open, onConfirm, onCancel }) => {
1125
const { t } = useTranslation();
1226
const [tapName, setTapName] = useState("");
13-
const [error, setError] = useState<string | null>(null);
27+
const [tapURL, setTapURL] = useState("");
28+
const [nameError, setNameError] = useState<string | null>(null);
29+
const [urlError, setUrlError] = useState<string | null>(null);
1430
const inputRef = useRef<HTMLInputElement>(null);
1531

16-
// Focus input when dialog opens
1732
useEffect(() => {
1833
if (open && inputRef.current) {
1934
inputRef.current.focus();
2035
}
2136
}, [open]);
2237

23-
// Reset state when dialog closes
2438
useEffect(() => {
2539
if (!open) {
2640
setTapName("");
27-
setError(null);
41+
setTapURL("");
42+
setNameError(null);
43+
setUrlError(null);
2844
}
2945
}, [open]);
3046

31-
const validateTapName = (name: string): boolean => {
32-
// Tap name should be in format: user/repo
33-
// Examples: homebrew/cask, user/tap-name
34-
const tapPattern = /^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?\/[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
35-
return tapPattern.test(name);
36-
};
37-
3847
const handleConfirm = () => {
3948
const trimmedName = tapName.trim();
40-
49+
const trimmedURL = tapURL.trim();
50+
51+
setNameError(null);
52+
setUrlError(null);
53+
4154
if (!trimmedName) {
42-
setError(t('dialogs.tapInputEmpty'));
55+
setNameError(t('dialogs.tapInputEmpty'));
56+
return;
57+
}
58+
59+
if (!tapNamePattern.test(trimmedName)) {
60+
setNameError(t('dialogs.tapInputInvalid'));
4361
return;
4462
}
4563

46-
if (!validateTapName(trimmedName)) {
47-
setError(t('dialogs.tapInputInvalid'));
64+
if (trimmedURL && !tapURLPattern.test(trimmedURL)) {
65+
setUrlError(t('dialogs.tapInputUrlInvalid'));
4866
return;
4967
}
5068

51-
setError(null);
52-
onConfirm(trimmedName);
69+
onConfirm(trimmedName, trimmedURL);
5370
};
5471

5572
const handleKeyDown = (e: React.KeyboardEvent) => {
@@ -93,29 +110,49 @@ const TapInputDialog: React.FC<TapInputDialogProps> = ({ open, onConfirm, onCanc
93110
value={tapName}
94111
onChange={(e) => {
95112
setTapName(e.target.value);
96-
setError(null);
113+
setNameError(null);
97114
}}
98115
onKeyDown={handleKeyDown}
99116
placeholder={t('dialogs.tapInputPlaceholder')}
100-
style={{
101-
width: '100%',
102-
padding: '0.5rem',
103-
fontSize: '1rem',
104-
border: error ? '2px solid #dc2626' : '1px solid #444',
105-
borderRadius: '4px',
106-
backgroundColor: '#1e293b',
107-
color: '#fff',
108-
outline: 'none',
117+
style={inputStyle(!!nameError)}
118+
/>
119+
{nameError && (
120+
<p style={{
121+
color: '#dc2626',
122+
fontSize: '0.875rem',
123+
marginTop: '0.5rem',
124+
marginBottom: 0
125+
}}>
126+
{nameError}
127+
</p>
128+
)}
129+
<p style={{
130+
fontSize: '0.75rem',
131+
color: '#888',
132+
marginTop: '0.5rem',
133+
marginBottom: '0.75rem'
134+
}}>
135+
{t('dialogs.tapInputHint')}
136+
</p>
137+
<input
138+
type="text"
139+
value={tapURL}
140+
onChange={(e) => {
141+
setTapURL(e.target.value);
142+
setUrlError(null);
109143
}}
144+
onKeyDown={handleKeyDown}
145+
placeholder={t('dialogs.tapInputUrlPlaceholder')}
146+
style={inputStyle(!!urlError)}
110147
/>
111-
{error && (
148+
{urlError && (
112149
<p style={{
113150
color: '#dc2626',
114151
fontSize: '0.875rem',
115152
marginTop: '0.5rem',
116153
marginBottom: 0
117154
}}>
118-
{error}
155+
{urlError}
119156
</p>
120157
)}
121158
<p style={{
@@ -124,7 +161,7 @@ const TapInputDialog: React.FC<TapInputDialogProps> = ({ open, onConfirm, onCanc
124161
marginTop: '0.5rem',
125162
marginBottom: 0
126163
}}>
127-
{t('dialogs.tapInputHint')}
164+
{t('dialogs.tapInputUrlHint')}
128165
</p>
129166
</div>
130167
<div className="confirm-actions">
@@ -139,4 +176,3 @@ const TapInputDialog: React.FC<TapInputDialogProps> = ({ open, onConfirm, onCanc
139176
};
140177

141178
export default TapInputDialog;
142-

frontend/src/i18n/locales/de.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,9 @@
210210
"tapInputHint": "Repository-Name im Format eingeben: benutzer/repo",
211211
"tapInputEmpty": "Bitte geben Sie einen Repository-Namen ein",
212212
"tapInputInvalid": "Ungültiges Format. Repository-Name muss im Format sein: benutzer/repo",
213+
"tapInputUrlPlaceholder": "https://github.com/benutzer/repo (optional)",
214+
"tapInputUrlHint": "Optional. Erforderlich, wenn die Tap-Quelle nicht am Standard-GitHub-Ort homebrew-* liegt.",
215+
"tapInputUrlInvalid": "Ungültiges URL-Format. Verwenden Sie https://, git@, ssh:// oder ähnliches.",
213216
"repositoryInfo": "Info für {{name}}",
214217
"confirmUntapUninstall": "Cannot untap \"{{name}}\" because it contains {{count}} installed package(s): {{packages}}\n\nDo you want to uninstall them first and then untap?",
215218
"uninstallingPackagesBeforeUntap": "Uninstalling {{count}} package(s) before untapping \"{{name}}\"...\nPlease wait...",

frontend/src/i18n/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,9 @@
210210
"tapInputHint": "Enter repository name in format: user/repo",
211211
"tapInputEmpty": "Please enter a repository name",
212212
"tapInputInvalid": "Invalid format. Repository name must be in format: user/repo",
213+
"tapInputUrlPlaceholder": "https://github.com/user/repo (optional)",
214+
"tapInputUrlHint": "Optional. Required when the tap source is not at the default GitHub homebrew-* location.",
215+
"tapInputUrlInvalid": "Invalid URL format. Use https://, git@, ssh://, or similar.",
213216
"repositoryInfo": "Info for {{name}}",
214217
"confirmUntapUninstall": "Cannot untap \"{{name}}\" because it contains {{count}} installed package(s): {{packages}}\n\nDo you want to uninstall them first and then untap?",
215218
"uninstallingPackagesBeforeUntap": "Uninstalling {{count}} package(s) before untapping \"{{name}}\"...\nPlease wait...",

frontend/src/i18n/locales/es.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,9 @@
210210
"tapInputHint": "Ingrese el nombre del repositorio en el formato: user/repo",
211211
"tapInputEmpty": "Por favor, ingrese un nombre de repositorio",
212212
"tapInputInvalid": "Formato inválido. El nombre del repositorio debe estar en el formato: user/repo",
213+
"tapInputUrlPlaceholder": "https://github.com/usuario/repo (opcional)",
214+
"tapInputUrlHint": "Opcional. Requerido cuando la fuente del tap no está en la ubicación predeterminada de GitHub homebrew-*.",
215+
"tapInputUrlInvalid": "Formato de URL inválido. Use https://, git@, ssh:// o similar.",
213216
"repositoryInfo": "Info para {{name}}",
214217
"confirmUntapUninstall": "No se puede remover \"{{name}}\" porque contiene {{count}} paquete(s) instalado(s): {{packages}}\n\n¿Desea desinstalarlos primero y luego remover?",
215218
"uninstallingPackagesBeforeUntap": "Desinstalando {{count}} paquete(s) antes de remover \"{{name}}\"...\nPor favor, espere...",

frontend/src/i18n/locales/fr.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,9 @@
210210
"tapInputHint": "Entrez le nom du dépôt au format: utilisateur/repo",
211211
"tapInputEmpty": "Veuillez entrer un nom de dépôt",
212212
"tapInputInvalid": "Format invalide. Le nom du dépôt doit être au format: utilisateur/repo",
213+
"tapInputUrlPlaceholder": "https://github.com/utilisateur/repo (optionnel)",
214+
"tapInputUrlHint": "Optionnel. Requis lorsque la source du tap n'est pas à l'emplacement GitHub par défaut homebrew-*.",
215+
"tapInputUrlInvalid": "Format d'URL invalide. Utilisez https://, git@, ssh:// ou similaire.",
213216
"repositoryInfo": "Informations pour {{name}}",
214217
"confirmUntapUninstall": "Cannot untap \"{{name}}\" because it contains {{count}} installed package(s): {{packages}}\n\nDo you want to uninstall them first and then untap?",
215218
"uninstallingPackagesBeforeUntap": "Uninstalling {{count}} package(s) before untapping \"{{name}}\"...\nPlease wait...",

frontend/src/i18n/locales/he.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,9 @@
210210
"tapInputHint": "הזן שם מאגר בפורמט: user/repo",
211211
"tapInputEmpty": "אנא הזן שם מאגר",
212212
"tapInputInvalid": "פורמט לא תקין. שם המאגר חייב להיות בפורמט: user/repo",
213+
"tapInputUrlPlaceholder": "https://github.com/user/repo (אופציונלי)",
214+
"tapInputUrlHint": "אופציונלי. נדרש כאשר מקור ה-tap אינו במיקום ברירת המחדל של GitHub homebrew-*.",
215+
"tapInputUrlInvalid": "פורמט URL לא תקין. השתמש ב-https://, git@, ssh:// או דומה.",
213216
"repositoryInfo": "מידע על {{name}}",
214217
"confirmUntapUninstall": "לא ניתן להסיר את המאגר \"{{name}}\" כיוון שהוא מכיל {{count}} חבילות מותקנות: {{packages}}\n\nהאם ברצונך להסיר אותן תחילה ואז להסיר את המאגר?",
215218
"uninstallingPackagesBeforeUntap": "מסיר {{count}} חבילות לפני הסרת המאגר \"{{name}}\"...\nאנא המתן...",

0 commit comments

Comments
 (0)