mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
feat(vcs): redesign the Clone Repository dialog and support shallow clones (#866)
Clone Repository gets IntelliJ's layout: a source list on the left, a version-control selector, URL history, a directory picker, and a shallow-clone option (history truncated to N commits, git clone --depth N). Entries for hosting platforms without backend support were removed. Co-authored-by: whyareyouserious <114078441+whyareyouserious@users.noreply.github.com>
This commit is contained in:
@@ -552,12 +552,13 @@ pub async fn vcs_clone(
|
||||
request: RequestId,
|
||||
url: String,
|
||||
dest: PathBuf,
|
||||
depth: Option<u32>,
|
||||
on_output: Channel<String>,
|
||||
) -> IdeResult<()> {
|
||||
apply_git_executable(&host.global_settings.effective());
|
||||
let cancel = host.begin_request(request);
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
ide_vcs::clone(&url, &dest, &cancel, &|line| {
|
||||
ide_vcs::clone(&url, &dest, depth, &cancel, &|line| {
|
||||
let _ = on_output.send(line);
|
||||
})
|
||||
})
|
||||
@@ -1070,7 +1071,16 @@ pub(crate) fn register(d: &mut Dispatcher) {
|
||||
|c| async move { vcs_stage_hunks(&c.host, &c.window, c.arg("path")?, c.arg("hunks")?).await },
|
||||
);
|
||||
d.add("vcs_clone", |c| async move {
|
||||
vcs_clone(&c.host, &c.window, c.arg("request")?, c.arg("url")?, c.arg("dest")?, c.sender("onOutput")?).await
|
||||
vcs_clone(
|
||||
&c.host,
|
||||
&c.window,
|
||||
c.arg("request")?,
|
||||
c.arg("url")?,
|
||||
c.arg("dest")?,
|
||||
c.arg("depth")?,
|
||||
c.sender("onOutput")?,
|
||||
)
|
||||
.await
|
||||
});
|
||||
d.add("vcs_init", |c| async move { vcs_init(&c.host, &c.window, c.arg("dir")?).await });
|
||||
d.add("vcs_git_version", |c| async move { vcs_git_version(&c.host, &c.window, c.arg("path")?).await });
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! hooks, credentials and signing behave exactly as on the command line.
|
||||
|
||||
use ide_core::ignored::IgnoredNames;
|
||||
use ide_core::{CancellationToken, IdeResult};
|
||||
use ide_core::{CancellationToken, IdeError, IdeResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -485,20 +485,64 @@ pub use manage::{MergeOptions, RebaseOptions, RemoteEdit, Worktree};
|
||||
pub use remotes::Remote;
|
||||
pub use roots::Repos;
|
||||
|
||||
fn clone_args(url: &str, dest: &str, depth: Option<u32>) -> IdeResult<Vec<String>> {
|
||||
if depth == Some(0) {
|
||||
return Err(IdeError::invalid("clone depth must be greater than zero"));
|
||||
}
|
||||
let mut args = vec!["-c".into(), "core.hooksPath=".into(), "clone".into(), "--progress".into()];
|
||||
if let Some(depth) = depth {
|
||||
args.extend(["--depth".into(), depth.to_string()]);
|
||||
}
|
||||
args.extend(["--".into(), url.into(), dest.into()]);
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
/// Clones `url` into `dest` (which must not exist or be empty), streaming progress.
|
||||
pub fn clone(url: &str, dest: &Path, cancel: &CancellationToken, on_output: &dyn Fn(String)) -> IdeResult<()> {
|
||||
/// `depth` creates a shallow clone containing at most that many commits.
|
||||
pub fn clone(
|
||||
url: &str,
|
||||
dest: &Path,
|
||||
depth: Option<u32>,
|
||||
cancel: &CancellationToken,
|
||||
on_output: &dyn Fn(String),
|
||||
) -> IdeResult<()> {
|
||||
let parent = dest.parent().filter(|p| !p.as_os_str().is_empty()).unwrap_or(Path::new("."));
|
||||
std::fs::create_dir_all(parent)?;
|
||||
let dest = if dest.is_absolute() { dest.to_path_buf() } else { std::env::current_dir()?.join(dest) };
|
||||
let dest = dest.to_string_lossy();
|
||||
// Cloning must not inherit an enclosing project's executable Git settings.
|
||||
let cwd = tempfile::tempdir()?;
|
||||
cli::run_streaming(
|
||||
cwd.path(),
|
||||
&["-c", "core.hooksPath=", "clone", "--progress", "--", url, &dest],
|
||||
cancel,
|
||||
on_output,
|
||||
)
|
||||
let args = clone_args(url, &dest, depth)?;
|
||||
let args: Vec<&str> = args.iter().map(String::as_str).collect();
|
||||
cli::run_streaming(cwd.path(), &args, cancel, on_output)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod clone_tests {
|
||||
use super::clone_args;
|
||||
|
||||
#[test]
|
||||
fn builds_full_and_shallow_clone_commands() {
|
||||
assert_eq!(
|
||||
clone_args("https://example.test/repo.git", "/tmp/repo", None).unwrap(),
|
||||
["-c", "core.hooksPath=", "clone", "--progress", "--", "https://example.test/repo.git", "/tmp/repo"]
|
||||
);
|
||||
assert_eq!(
|
||||
clone_args("https://example.test/repo.git", "/tmp/repo", Some(3)).unwrap(),
|
||||
[
|
||||
"-c",
|
||||
"core.hooksPath=",
|
||||
"clone",
|
||||
"--progress",
|
||||
"--depth",
|
||||
"3",
|
||||
"--",
|
||||
"https://example.test/repo.git",
|
||||
"/tmp/repo",
|
||||
]
|
||||
);
|
||||
assert!(clone_args("https://example.test/repo.git", "/tmp/repo", Some(0)).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an empty repository in `dir` (`git init`).
|
||||
|
||||
@@ -355,9 +355,10 @@ pub async fn vcs_clone(
|
||||
request: RequestId,
|
||||
url: String,
|
||||
dest: PathBuf,
|
||||
depth: Option<u32>,
|
||||
on_output: Channel<String>,
|
||||
) -> IdeResult<()> {
|
||||
ide_host::ops::vcs::vcs_clone(&state.host, "", request, url, dest, crate::commands::sender(on_output)).await
|
||||
ide_host::ops::vcs::vcs_clone(&state.host, "", request, url, dest, depth, crate::commands::sender(on_output)).await
|
||||
}
|
||||
|
||||
/// `git init` in `dir` (File › New Project… "Create Git repository").
|
||||
|
||||
+1
-1
@@ -504,7 +504,7 @@ export const commands = {
|
||||
*/
|
||||
vcsStageHunks: (path: string, hunks: number[]) => typedError<null, IdeError>(__TAURI_INVOKE("vcs_stage_hunks", { path, hunks })),
|
||||
/** Clones a repository (Get from VCS). Works without an open project. */
|
||||
vcsClone: (request: RequestId, url: string, dest: string, onOutput: Channel<string>) => typedError<null, IdeError>(__TAURI_INVOKE("vcs_clone", { request, url, dest, onOutput })),
|
||||
vcsClone: (request: RequestId, url: string, dest: string, depth: number | null, onOutput: Channel<string>) => typedError<null, IdeError>(__TAURI_INVOKE("vcs_clone", { request, url, dest, depth, onOutput })),
|
||||
/** `git init` in `dir` (File › New Project… "Create Git repository"). */
|
||||
vcsInit: (dir: string) => typedError<null, IdeError>(__TAURI_INVOKE("vcs_init", { dir })),
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { cloneDirName, joinPath, validateClone } from "../clone";
|
||||
import { cloneDirName, joinPath, parseCloneDepth, validateClone } from "../clone";
|
||||
import { outgoingQuery } from "../remoteUi";
|
||||
|
||||
describe("cloneDirName", () => {
|
||||
@@ -31,6 +31,17 @@ describe("joinPath / validateClone", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseCloneDepth", () => {
|
||||
it("accepts positive Git depths only", () => {
|
||||
expect(parseCloneDepth("1")).toBe(1);
|
||||
expect(parseCloneDepth(" 25 ")).toBe(25);
|
||||
expect(parseCloneDepth("0")).toBeNull();
|
||||
expect(parseCloneDepth("1.5")).toBeNull();
|
||||
expect(parseCloneDepth("abc")).toBeNull();
|
||||
expect(parseCloneDepth("4294967296")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("outgoingQuery", () => {
|
||||
it("uses the upstream range when tracked", () => {
|
||||
expect(outgoingQuery("origin/main")).toEqual({ branch: "HEAD", range: "@{u}..HEAD", limit: 1000 });
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
.dialog {
|
||||
height: min(660px, calc(100vh - 48px));
|
||||
min-height: min(480px, calc(100vh - 48px));
|
||||
}
|
||||
.dialog [data-dialog-body] {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dialog [data-dialog-footer] {
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.dialogTitle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.dialogTitle img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.layout {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
}
|
||||
.sidebar {
|
||||
flex: 0 0 224px;
|
||||
padding: 8px 0;
|
||||
border-right: 1px solid var(--border);
|
||||
background: color-mix(in srgb, var(--bg-dialog, var(--bg-elev)) 96%, var(--fg-strong));
|
||||
overflow: auto;
|
||||
}
|
||||
.sourceItem {
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 9px 14px;
|
||||
color: var(--fg-strong);
|
||||
}
|
||||
.sourceItem[data-selected] {
|
||||
background: var(--bg-tree-selected);
|
||||
}
|
||||
.sourceName {
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
}
|
||||
.form {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 28px 24px;
|
||||
}
|
||||
.versionRow,
|
||||
.fieldRow {
|
||||
display: grid;
|
||||
grid-template-columns: 112px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 10px 12px;
|
||||
}
|
||||
.versionRow {
|
||||
grid-template-columns: 112px 138px minmax(0, 1fr);
|
||||
}
|
||||
.versionRow > label,
|
||||
.fieldRow > label {
|
||||
color: var(--fg-strong);
|
||||
text-align: right;
|
||||
}
|
||||
.versionSelect {
|
||||
width: 138px;
|
||||
}
|
||||
.comboField {
|
||||
min-width: 0;
|
||||
height: var(--h-field, 28px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: var(--radius-control, 4px);
|
||||
background: var(--bg-field, var(--bg-button));
|
||||
box-shadow: inset 0 0 0 1px var(--border-button);
|
||||
}
|
||||
.comboField:focus-within {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--focus-ring),
|
||||
0 0 0 1px var(--focus-ring);
|
||||
}
|
||||
.fieldInput {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
padding: 0 8px;
|
||||
background: transparent;
|
||||
}
|
||||
.fieldButton {
|
||||
width: 34px;
|
||||
height: 26px;
|
||||
border-radius: 0 var(--radius-control, 4px) var(--radius-control, 4px) 0;
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
.shallowRow {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding-left: 124px;
|
||||
}
|
||||
.depthInput {
|
||||
width: 84px;
|
||||
}
|
||||
.feedback {
|
||||
min-width: 0;
|
||||
padding-left: 124px;
|
||||
}
|
||||
.progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
.progress > span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.hint {
|
||||
color: var(--fg-subtle);
|
||||
font-size: var(--fs-small);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.error {
|
||||
color: var(--danger);
|
||||
font-size: var(--fs-small);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.sidebar {
|
||||
flex-basis: 172px;
|
||||
}
|
||||
.form {
|
||||
padding-inline: 16px;
|
||||
}
|
||||
.versionRow,
|
||||
.fieldRow {
|
||||
grid-template-columns: 86px minmax(0, 1fr);
|
||||
}
|
||||
.versionRow {
|
||||
grid-template-columns: 86px 118px minmax(0, 1fr);
|
||||
}
|
||||
.versionSelect {
|
||||
width: 118px;
|
||||
}
|
||||
.shallowRow,
|
||||
.feedback {
|
||||
padding-left: 98px;
|
||||
}
|
||||
}
|
||||
+178
-49
@@ -4,13 +4,16 @@ import { Channel } from "@tauri-apps/api/core";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { call, commands, isCancelled, isTauri, nextRequestId } from "@/platform/ipc";
|
||||
import { openProject } from "@/platform/project";
|
||||
import { Button, Dialog, Input, ProgressBar, T } from "@/ui";
|
||||
import { Checkbox, Dialog, Icon, IconButton, Input, ProgressBar, Select, T, showContextMenu } from "@/ui";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
import { openDialog } from "./dialogs";
|
||||
import { progressFraction } from "./model";
|
||||
import styles from "./common.module.css";
|
||||
import logo from "../../../src-tauri/icons/128x128.png";
|
||||
import styles from "./clone.module.css";
|
||||
|
||||
const PARENT_KEY = "openidea.vcs.cloneParent";
|
||||
const RECENT_URLS_KEY = "openidea.vcs.cloneRecentUrls";
|
||||
const MAX_RECENT_URLS = 10;
|
||||
|
||||
/** Folder name git would pick for `url` (`https://h/o/repo.git` → `repo`, `git@h:o/r` → `r`). */
|
||||
export function cloneDirName(url: string): string {
|
||||
@@ -45,6 +48,14 @@ export function validateClone(url: string, dir: string): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A valid `git clone --depth` value, or null while the field is invalid. */
|
||||
export function parseCloneDepth(value: string): number | null {
|
||||
const text = value.trim();
|
||||
if (!/^\d+$/.test(text)) return null;
|
||||
const depth = Number(text);
|
||||
return Number.isSafeInteger(depth) && depth > 0 && depth <= 4_294_967_295 ? depth : null;
|
||||
}
|
||||
|
||||
function readParent(): string {
|
||||
try {
|
||||
return localStorage.getItem(PARENT_KEY) ?? "";
|
||||
@@ -61,6 +72,26 @@ function writeParent(path: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function readRecentUrls(): string[] {
|
||||
try {
|
||||
const value = JSON.parse(localStorage.getItem(RECENT_URLS_KEY) ?? "[]");
|
||||
return Array.isArray(value)
|
||||
? value.filter((item): item is string => typeof item === "string").slice(0, MAX_RECENT_URLS)
|
||||
: [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function rememberUrl(url: string) {
|
||||
try {
|
||||
const urls = [url, ...readRecentUrls().filter((item) => item !== url)].slice(0, MAX_RECENT_URLS);
|
||||
localStorage.setItem(RECENT_URLS_KEY, JSON.stringify(urls));
|
||||
} catch {
|
||||
// URL history is a convenience only
|
||||
}
|
||||
}
|
||||
|
||||
const parentOf = (p: string) => p.replace(/[/\\]+$/, "").replace(/[/\\][^/\\]*$/, "");
|
||||
|
||||
async function defaultParent(): Promise<string> {
|
||||
@@ -89,9 +120,13 @@ function CloneDialog({ onClose, initialUrl }: { onClose: () => void; initialUrl:
|
||||
/** Directory edited by hand: stop deriving it from the URL. */
|
||||
const [customDir, setCustomDir] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [shallow, setShallow] = useState(false);
|
||||
const [depthInput, setDepthInput] = useState("1");
|
||||
const [progress, setProgress] = useState<{ line: string; fraction: number | null }>({ line: "", fraction: null });
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const request = useRef<number | null>(null);
|
||||
const urlField = useRef<HTMLDivElement>(null);
|
||||
const [recentUrls] = useState(readRecentUrls);
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
@@ -102,7 +137,17 @@ function CloneDialog({ onClose, initialUrl }: { onClose: () => void; initialUrl:
|
||||
}, []);
|
||||
|
||||
const dir = customDir ?? joinPath(parent, cloneDirName(url));
|
||||
const invalid = validateClone(url, dir);
|
||||
const depth = shallow ? parseCloneDepth(depthInput) : null;
|
||||
const invalid = validateClone(url, dir) ?? (shallow && depth === null ? "Enter a positive number of commits" : null);
|
||||
|
||||
const showUrlHistory = () => {
|
||||
const rect = urlField.current?.getBoundingClientRect();
|
||||
if (!rect || !recentUrls.length) return;
|
||||
showContextMenu(
|
||||
{ x: rect.left, y: rect.bottom },
|
||||
recentUrls.map((item) => ({ label: item, verbatim: true, run: () => setUrl(item) })),
|
||||
);
|
||||
};
|
||||
|
||||
const browse = async () => {
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
@@ -136,8 +181,9 @@ function CloneDialog({ onClose, initialUrl }: { onClose: () => void; initialUrl:
|
||||
if (text) setProgress({ line: text, fraction: progressFraction(text) });
|
||||
};
|
||||
try {
|
||||
await call(commands.vcsClone(id, url.trim(), target, channel));
|
||||
await call(commands.vcsClone(id, url.trim(), target, depth, channel));
|
||||
writeParent(parentOf(target));
|
||||
rememberUrl(url.trim());
|
||||
onClose();
|
||||
try {
|
||||
await openProject(target);
|
||||
@@ -162,58 +208,141 @@ function CloneDialog({ onClose, initialUrl }: { onClose: () => void; initialUrl:
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
title="Get from Version Control"
|
||||
title={
|
||||
<span className={styles.dialogTitle}>
|
||||
<img src={logo} alt="" draggable={false} />
|
||||
<T>Clone Repository</T>
|
||||
</span>
|
||||
}
|
||||
aria-label="Clone Repository"
|
||||
onClose={cancel}
|
||||
width={560}
|
||||
width="min(920px, calc(100vw - 48px))"
|
||||
resizable="both"
|
||||
className={styles.dialog}
|
||||
buttons={[
|
||||
{ label: "Clone", role: "default", disabled: !!invalid || busy, onClick: () => void clone() },
|
||||
{ label: "Cancel", role: "cancel", onClick: cancel },
|
||||
]}
|
||||
>
|
||||
<form
|
||||
className={styles.form}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
void clone();
|
||||
}}
|
||||
>
|
||||
<label className={styles.fieldLabel} htmlFor="vcs-clone-url">
|
||||
<T>URL:</T>
|
||||
</label>
|
||||
<Input
|
||||
id="vcs-clone-url"
|
||||
data-autofocus
|
||||
mono
|
||||
value={url}
|
||||
disabled={busy}
|
||||
placeholder="https://github.com/user/repository.git"
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
/>
|
||||
<label className={styles.fieldLabel} htmlFor="vcs-clone-dir">
|
||||
<T>Directory:</T>
|
||||
</label>
|
||||
<div className={styles.cloneDirRow}>
|
||||
<Input
|
||||
id="vcs-clone-dir"
|
||||
className={styles.cloneDirInput}
|
||||
mono
|
||||
value={dir}
|
||||
disabled={busy}
|
||||
onChange={(e) => setCustomDir(e.target.value)}
|
||||
/>
|
||||
<Button disabled={busy} onClick={() => void browse()}>
|
||||
<T>Browse…</T>
|
||||
</Button>
|
||||
</div>
|
||||
{busy && (
|
||||
<div className={styles.cloneProgress} aria-live="polite">
|
||||
<ProgressBar value={progress.fraction} aria-label="Clone progress" />
|
||||
<span className={styles.hint}>{progress.line || tx("Cloning…")}</span>
|
||||
<div className={styles.layout}>
|
||||
<aside className={styles.sidebar} aria-label={tx("Clone source")}>
|
||||
<div className={styles.sourceItem} data-selected="">
|
||||
<Icon name="branch" size={22} />
|
||||
<span className={styles.sourceName}>
|
||||
<T>Repository URL</T>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{error && <div className={styles.error}>{error}</div>}
|
||||
{!busy && !error && url.trim() && invalid && <div className={styles.hint}>{invalid}</div>}
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
<form
|
||||
className={styles.form}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
void clone();
|
||||
}}
|
||||
>
|
||||
<div className={styles.versionRow}>
|
||||
<label id="vcs-clone-version-label">
|
||||
<T>Version control:</T>
|
||||
</label>
|
||||
<Select
|
||||
aria-label="Version control"
|
||||
className={styles.versionSelect}
|
||||
value="git"
|
||||
options={[{ value: "git", label: "Git" }]}
|
||||
onChange={() => {}}
|
||||
disabled={busy}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.fieldRow}>
|
||||
<label htmlFor="vcs-clone-url">
|
||||
<T>URL:</T>
|
||||
</label>
|
||||
<div ref={urlField} className={styles.comboField}>
|
||||
<Input
|
||||
id="vcs-clone-url"
|
||||
data-autofocus
|
||||
variant="bare"
|
||||
className={styles.fieldInput}
|
||||
value={url}
|
||||
disabled={busy}
|
||||
placeholder="https://github.com/user/repository.git"
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
/>
|
||||
<IconButton
|
||||
icon="chevron-down"
|
||||
size="normal"
|
||||
className={styles.fieldButton}
|
||||
title={recentUrls.length ? "Recent repository URLs" : "No recent repository URLs"}
|
||||
disabled={busy || !recentUrls.length}
|
||||
onClick={showUrlHistory}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.fieldRow}>
|
||||
<label htmlFor="vcs-clone-dir">
|
||||
<T>Directory:</T>
|
||||
</label>
|
||||
<div className={styles.comboField}>
|
||||
<Input
|
||||
id="vcs-clone-dir"
|
||||
variant="bare"
|
||||
className={styles.fieldInput}
|
||||
value={dir}
|
||||
disabled={busy}
|
||||
onChange={(e) => setCustomDir(e.target.value)}
|
||||
/>
|
||||
<IconButton
|
||||
icon="folder-open"
|
||||
size="normal"
|
||||
className={styles.fieldButton}
|
||||
title="Browse…"
|
||||
disabled={busy}
|
||||
onClick={() => void browse()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.shallowRow}>
|
||||
<Checkbox
|
||||
checked={shallow}
|
||||
disabled={busy}
|
||||
onChange={setShallow}
|
||||
label={<T>Shallow clone with a history truncated to</T>}
|
||||
/>
|
||||
<Input
|
||||
aria-label="Clone depth"
|
||||
className={styles.depthInput}
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
min={1}
|
||||
step={1}
|
||||
value={depthInput}
|
||||
disabled={busy || !shallow}
|
||||
invalid={shallow && depth === null}
|
||||
onChange={(e) => setDepthInput(e.target.value)}
|
||||
/>
|
||||
<T>commits</T>
|
||||
</div>
|
||||
|
||||
<div className={styles.feedback}>
|
||||
{busy && (
|
||||
<div className={styles.progress} aria-live="polite">
|
||||
<ProgressBar value={progress.fraction} aria-label="Clone progress" />
|
||||
<span className={styles.hint}>{progress.line || tx("Cloning…")}</span>
|
||||
</div>
|
||||
)}
|
||||
{error && <div className={styles.error}>{error}</div>}
|
||||
{!busy && !error && url.trim() && invalid && (
|
||||
<div className={styles.hint}>
|
||||
<T>{invalid}</T>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,27 +125,6 @@
|
||||
flex-direction: column;
|
||||
padding: 0 14px 14px;
|
||||
}
|
||||
.cloneDirRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.cloneDirInput {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.cloneProgress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding-top: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
.cloneProgress > span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.todoList {
|
||||
margin: 8px 0 0;
|
||||
padding-left: 18px;
|
||||
|
||||
@@ -410,9 +410,11 @@
|
||||
"Clear language server workspaces (Java import data and indexes)": "清除语言服务器工作区(Java 导入数据和索引)",
|
||||
"Clipboard": "剪贴板",
|
||||
"Clone": "克隆",
|
||||
"Clone depth": "克隆深度",
|
||||
"Clone Repository": "克隆仓库",
|
||||
"Clone Repository…": "克隆仓库…",
|
||||
"Clone progress": "克隆进度",
|
||||
"Clone source": "克隆来源",
|
||||
"Cloned, but the project could not be opened": "已克隆,但无法打开项目",
|
||||
"Cloning requires the desktop app": "克隆需要桌面应用",
|
||||
"Close": "关闭",
|
||||
@@ -470,6 +472,7 @@
|
||||
"Commits": "提交",
|
||||
"Commits are applied from top to bottom.": "提交将从上到下依次应用。",
|
||||
"Commits to rebase": "要变基的提交",
|
||||
"commits": "个提交",
|
||||
"Commit…": "提交…",
|
||||
"Compact mode": "紧凑模式",
|
||||
"Companion object": "伴生对象",
|
||||
@@ -746,6 +749,7 @@
|
||||
"Encoding of the output of processes started from Run configurations.": "从运行配置启动的进程的输出编码。",
|
||||
"Enhanced for loop": "增强型 for 循环",
|
||||
"Ensure every saved file ends with a line break": "确保每个已保存的文件都以换行符结尾",
|
||||
"Enter a positive number of commits": "请输入正整数提交数",
|
||||
"Enter a tag, branch or commit hash:": "输入标签、分支或提交哈希:",
|
||||
"Enter the name of new tag:": "输入新标签的名称:",
|
||||
"Enum": "枚举",
|
||||
@@ -912,7 +916,6 @@
|
||||
"Generate Request in HTTP Client": "在 HTTP 客户端中生成请求",
|
||||
"Generate Requests for Controller": "为控制器生成请求",
|
||||
"Generate…": "生成…",
|
||||
"Get from Version Control": "从版本控制获取",
|
||||
"Get from Version Control…": "从版本控制获取…",
|
||||
"Gevent compatible": "兼容 Gevent",
|
||||
"Git Branches": "Git 分支",
|
||||
@@ -1294,6 +1297,7 @@
|
||||
"No Maven projects": "没有 Maven 项目",
|
||||
"No profile for '{name}'": "没有 '{name}' 的分析数据",
|
||||
"No Python found": "未找到 Python",
|
||||
"No recent repository URLs": "没有最近使用的仓库 URL",
|
||||
"No recent searches": "无最近搜索",
|
||||
"No TODO items found": "未找到 TODO 项",
|
||||
"No TODO items in this file": "此文件中没有 TODO 项",
|
||||
@@ -1626,6 +1630,7 @@
|
||||
"Recent files": "最近的文件",
|
||||
"Recent locations": "最近的位置",
|
||||
"Recent projects": "最近的项目",
|
||||
"Recent repository URLs": "最近使用的仓库 URL",
|
||||
"Recent Searches": "最近搜索",
|
||||
"Recently Viewed Files": "最近查看的文件",
|
||||
"Record": "记录",
|
||||
@@ -1739,6 +1744,7 @@
|
||||
"Replacements": "替换项",
|
||||
"Reports errors and warnings from language servers.": "报告来自语言服务器的错误和警告。",
|
||||
"Repository": "仓库",
|
||||
"Repository URL": "仓库 URL",
|
||||
"Request": "请求",
|
||||
"Require": "必需",
|
||||
"Rerun": "重新运行",
|
||||
@@ -1971,6 +1977,7 @@
|
||||
"Shelf": "搁置",
|
||||
"Shell integration": "Shell 集成",
|
||||
"Shell path": "Shell 路径",
|
||||
"Shallow clone with a history truncated to": "浅克隆,历史记录截断为",
|
||||
"Shelve Changes": "搁置更改",
|
||||
"Shelve Changes…": "搁置更改…",
|
||||
"Shelved changes": "已搁置的更改",
|
||||
@@ -2379,6 +2386,8 @@
|
||||
"Variables:": "变量:",
|
||||
"Version": "版本",
|
||||
"Version Control": "版本控制",
|
||||
"Version control": "版本控制",
|
||||
"Version control:": "版本控制:",
|
||||
"Version control is not configured": "未配置版本控制",
|
||||
"Version used by vtsls / typescript-language-server (typescript.tsdk, tsserver.path).": "vtsls / typescript-language-server 使用的版本(typescript.tsdk、tsserver.path)。",
|
||||
"Vertical": "竖线",
|
||||
|
||||
Reference in New Issue
Block a user