Merge pull request #20 from dexoron/dev

v0.7.3
This commit is contained in:
2026-06-15 22:36:12 +08:00
committed by GitHub
17 changed files with 553 additions and 1057 deletions

View File

@@ -1,5 +1,37 @@
# Changelog # Changelog
## [0.7.3] - 2026-06-13 "Оптимизация Зависимостей и VCS / Dependency Optimization & VCS"
### RU
**Добавлено:**
- **Параметр `--vcs` для команд `new` и `init`** — добавлена возможность явно указывать систему контроля версий через `--vcs <git|none>`.
- **Интеграция Git-информации в `--version`** — утилита теперь выводит текущий короткий хэш коммита и суффикс `-dirty`, если в репозитории DCR есть незакоммиченные изменения.
- **Автоматическая генерация `.gitignore`** — при инициализации Git-репозитория автоматически создается файл `.gitignore` с исключением директории `/target`.
- **Защита от вложенных репозиториев** — автоматическая инициализация Git теперь пропускается, если `dcr new` или `dcr init` запускаются внутри уже существующего Git-репозитория.
- **Тесты для верификации VCS** — добавлены интеграционные тесты `new_vcs_options_work` и `init_vcs_options_work` для проверки логики инициализации репозиториев.
**Изменено:**
- **Полный отказ от библиотеки `git2`** — работа с Git (клонирование зависимостей, инициализация VCS, проверка статуса) переведена на вызовы системного `git` через `Command`. Это облегчает сборку проекта и убирает необходимость в фичах `vendored` и `openssl`.
- **Миграция с `reqwest` на `ureq`** — в модуле обновления (`flag_update.rs`) тяжелый `reqwest` заменен на легковесный синхронный клиент `ureq 2.10`, что уменьшило оверхед и размер итогового бинарника.
### EN
**Added:**
- **`--vcs` option for `new` and `init` commands** — introduced explicit version control system selection via `--vcs <git|none>`.
- **Git metadata integration in `--version`** — the CLI now appends the current short commit hash and a `-dirty` suffix if there are uncommitted changes in the DCR repository.
- **Automatic `.gitignore` generation** — Git repository initialization now automatically writes a `.gitignore` file excluding the `/target` directory.
- **Nested repository prevention** — automatic Git initialization is now skipped if `dcr new` or `dcr init` is executed inside an already existing Git repository.
- **VCS verification tests** — added `new_vcs_options_work` and `init_vcs_options_work` integration tests to ensure reliable repository behavior.
**Changed:**
- **Complete removal of `git2` dependency** — all Git actions (dependency fetching, VCS setup, status checks) are now delegated to the system `git` executable via `Command`. This simplifies compilation and drops the need for `vendored` and `openssl` features.
- **Migrated from `reqwest` to `ureq`** — replaced the heavy `reqwest` crate in `flag_update.rs` with the lightweight synchronous `ureq 2.10` client, reducing overhead and the final binary size.
## [0.7.2] - 2026-06-07 "Исправление Таргетов и Линковки / Target & Linker Fixes" ## [0.7.2] - 2026-06-07 "Исправление Таргетов и Линковки / Target & Linker Fixes"
### RU ### RU

1037
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "dcr" name = "dcr"
version = "0.7.2" version = "0.7.3"
edition = "2024" edition = "2024"
build = "build.rs" build = "build.rs"
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style." description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
@@ -61,7 +61,7 @@ assets = [
[dependencies] [dependencies]
glob = "0.3" glob = "0.3"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } ureq = { version = "2.10", default-features = false, features = ["json", "native-certs"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
self-replace = "1" self-replace = "1"
sha2 = "0.10" sha2 = "0.10"
@@ -69,21 +69,6 @@ toml = "1.0.3"
serde_json = "1" serde_json = "1"
ctrlc = "3" ctrlc = "3"
[features]
default = ["vendored"]
vendored = [
"git2/vendored-libgit2",
"git2/vendored-openssl",
"openssl/vendored",
]
[target.'cfg(not(windows))'.dependencies]
git2 = "0.20.4"
openssl = "0.10"
[target.'cfg(windows)'.dependencies]
git2 = "0.20.4"
[profile.release] [profile.release]
opt-level = "z" opt-level = "z"
lto = "thin" lto = "thin"

View File

@@ -37,7 +37,7 @@
<td><b>Linux (Dexoron Packages)</b></td> <td><b>Linux (Dexoron Packages)</b></td>
<td> <td>
See https://dcr.dexoron.su for settings youre packages manager See https://pkg.dexoron.su for settings youre packages manager
**Arch Linux:** **Arch Linux:**

View File

@@ -18,4 +18,37 @@
fn main() { fn main() {
let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown-target".to_string()); let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown-target".to_string());
println!("cargo:rustc-env=DCR_TARGET={target}"); println!("cargo:rustc-env=DCR_TARGET={target}");
// Let's try to retrieve the commit hash for the version
let commit = std::process::Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
.ok()
.and_then(|o| {
if o.status.success() {
String::from_utf8(o.stdout)
.ok()
.map(|s| s.trim().to_string())
} else {
None
}
});
// Check if there are any uncommitted changes in the repository
let is_dirty = std::process::Command::new("git")
.args(["status", "--porcelain"])
.output()
.ok()
.map(|o| o.status.success() && !o.stdout.is_empty())
.unwrap_or(false);
if let Some(commit_hash) = commit {
let dirty_suffix = if is_dirty { "-dirty" } else { "" };
println!(
"cargo:rustc-env=DCR_GIT_INFO= (git {}{})",
commit_hash, dirty_suffix
);
} else {
println!("cargo:rustc-env=DCR_GIT_INFO=");
}
} }

View File

@@ -6,7 +6,7 @@ sidebar_label: Installation
## Linux (Dexoron Packages) ## Linux (Dexoron Packages)
Configure your package manager at https://dcr.dexoron.su and install: Configure your package manager at https://pkg.dexoron.su and install:
**Arch Linux** **Arch Linux**
```sh ```sh

View File

@@ -17,9 +17,9 @@
use crate::utils::log::{error, warn}; use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc}; use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use reqwest::blocking::Client;
use serde::Deserialize; use serde::Deserialize;
use std::fs; use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use std::process::Command; use std::process::Command;
@@ -72,15 +72,8 @@ pub fn flag_update(args: &[String]) -> i32 {
let current_version = env!("CARGO_PKG_VERSION"); let current_version = env!("CARGO_PKG_VERSION");
let target = env!("DCR_TARGET"); let target = env!("DCR_TARGET");
let client = match Client::builder().user_agent("dcr-updater").build() {
Ok(client) => client,
Err(_) => {
error("Failed to initialize HTTP client");
return 1;
}
};
let release = match fetch_latest_release(&client) { let release = match fetch_latest_release() {
Ok(release) => release, Ok(release) => release,
Err(err) => { Err(err) => {
error(&format!("Failed to check for updates: {err}")); error(&format!("Failed to check for updates: {err}"));
@@ -104,7 +97,7 @@ pub fn flag_update(args: &[String]) -> i32 {
return 1; return 1;
}; };
let bytes = match download_asset(&client, &asset.browser_download_url) { let bytes = match download_asset(&asset.browser_download_url) {
Ok(bytes) => bytes, Ok(bytes) => bytes,
Err(err) => { Err(err) => {
error(&format!("Failed to download update: {err}")); error(&format!("Failed to download update: {err}"));
@@ -131,35 +124,35 @@ pub fn flag_update(args: &[String]) -> i32 {
0 0
} }
fn fetch_latest_release(client: &Client) -> Result<Release, String> { fn fetch_latest_release() -> Result<Release, String> {
let response = client let response = ureq::get(LATEST_RELEASE_URL)
.get(LATEST_RELEASE_URL) .set("User-Agent", "dcr-updater")
.send() .call()
.map_err(|_| "GitHub API request failed".to_string())?; .map_err(|e| match e {
ureq::Error::Status(code, _) => format!("GitHub API returned status {code}"),
if !response.status().is_success() { ureq::Error::Transport(e) => format!("GitHub API request failed: {e}"),
return Err(format!("GitHub API returned status {}", response.status())); })?;
}
response response
.json::<Release>() .into_json::<Release>()
.map_err(|_| "GitHub API response has an unexpected format".to_string()) .map_err(|_| "GitHub API response has an unexpected format".to_string())
} }
fn download_asset(client: &Client, url: &str) -> Result<Vec<u8>, String> { fn download_asset(url: &str) -> Result<Vec<u8>, String> {
let response = client let response = ureq::get(url)
.get(url) .set("User-Agent", "dcr-updater")
.send() .call()
.map_err(|_| "Download request failed".to_string())?; .map_err(|e| match e {
ureq::Error::Status(code, _) => format!("Download returned status {code}"),
ureq::Error::Transport(e) => format!("Download request failed: {e}"),
})?;
if !response.status().is_success() { let mut reader = response.into_reader();
return Err(format!("Download returned status {}", response.status())); let mut data = Vec::new();
} reader
.read_to_end(&mut data)
response .map_err(|_| "Failed to read downloaded data".to_string())?;
.bytes() Ok(data)
.map(|bytes| bytes.to_vec())
.map_err(|_| "Failed to read downloaded data".to_string())
} }
fn asset_candidates(target: &str) -> Vec<String> { fn asset_candidates(target: &str) -> Vec<String> {

View File

@@ -17,6 +17,7 @@
use crate::config::FILE_MAIN_C; use crate::config::FILE_MAIN_C;
use crate::core::config::{Config, validate_package_name}; use crate::core::config::{Config, validate_package_name};
use crate::core::vcs::VcsKind;
use crate::utils::fs::check_dir; use crate::utils::fs::check_dir;
use crate::utils::log::{error, warn}; use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc}; use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
@@ -27,7 +28,7 @@ use toml::Value;
pub fn init(args: &[String]) -> i32 { pub fn init(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN); printc("USAGE:", BOLD_GREEN);
printc(" dcr init", BOLD_CYAN); printc(" dcr init [--vcs <git|none>]", BOLD_CYAN);
println!(); println!();
printc("DESCRIPTION:", BOLD_GREEN); printc("DESCRIPTION:", BOLD_GREEN);
println!(" Initializes the current directory as a DCR project."); println!(" Initializes the current directory as a DCR project.");
@@ -35,7 +36,25 @@ pub fn init(args: &[String]) -> i32 {
return 0; return 0;
} }
if !args.is_empty() { let mut vcs_str = None;
let mut clean_args = Vec::new();
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if arg == "--vcs" {
if let Some(val) = iter.next() {
vcs_str = Some(val.clone());
} else {
error("--vcs requires a value");
return 1;
}
} else if let Some(stripped) = arg.strip_prefix("--vcs=") {
vcs_str = Some(stripped.to_string());
} else {
clean_args.push(arg.clone());
}
}
if !clean_args.is_empty() {
warn("Command does not support additional arguments"); warn("Command does not support additional arguments");
return 1; return 1;
} }
@@ -106,6 +125,40 @@ pub fn init(args: &[String]) -> i32 {
colored("src/main.c", BOLD_CYAN) colored("src/main.c", BOLD_CYAN)
); );
// settings VCS
let mut vcs_kind = VcsKind::Git;
if let Some(ref vcs_val) = vcs_str {
match VcsKind::parse(vcs_val) {
Ok(kind) => vcs_kind = kind,
Err(e) => {
error(&e);
return 1;
}
}
} else if let Ok(cwd_path) = std::env::current_dir()
&& crate::core::vcs::find_existing_vcs(&cwd_path).is_some()
{
vcs_kind = VcsKind::None;
}
if vcs_kind == VcsKind::Git {
if crate::utils::git::is_git_available() {
let project_path = std::path::Path::new(".");
if let Err(e) = crate::core::vcs::init_vcs(vcs_kind, project_path) {
warn(&format!("Failed to initialize git repository: {}", e));
} else {
println!(
" {} Initialized git repository",
colored("✔", BOLD_GREEN)
);
}
} else {
warn(
"Git is not installed or not found in PATH. Skipping Git repository initialization.",
);
}
}
println!( println!(
"Project `{}` successfully created\n", "Project `{}` successfully created\n",
colored(project_name.as_str(), BOLD_GREEN) colored(project_name.as_str(), BOLD_GREEN)

View File

@@ -17,6 +17,7 @@
use crate::config::FILE_MAIN_C; use crate::config::FILE_MAIN_C;
use crate::core::config::{Config, validate_package_name}; use crate::core::config::{Config, validate_package_name};
use crate::core::vcs::VcsKind;
use crate::utils::fs::check_dir; use crate::utils::fs::check_dir;
use crate::utils::log::{error, warn}; use crate::utils::log::{error, warn};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc}; use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
@@ -27,7 +28,7 @@ use toml::Value;
pub fn new(args: &[String]) -> i32 { pub fn new(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
printc("USAGE:", BOLD_GREEN); printc("USAGE:", BOLD_GREEN);
printc(" dcr new <name>", BOLD_CYAN); printc(" dcr new <name> [--vcs <git|none>]", BOLD_CYAN);
println!(); println!();
printc("DESCRIPTION:", BOLD_GREEN); printc("DESCRIPTION:", BOLD_GREEN);
println!(" Creates a new C/C++ project with the given name."); println!(" Creates a new C/C++ project with the given name.");
@@ -37,16 +38,34 @@ pub fn new(args: &[String]) -> i32 {
let items = check_dir(None).unwrap_or_default(); let items = check_dir(None).unwrap_or_default();
if args.is_empty() { let mut vcs_str = None;
let mut clean_args = Vec::new();
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if arg == "--vcs" {
if let Some(val) = iter.next() {
vcs_str = Some(val.clone());
} else {
error("--vcs requires a value");
return 1;
}
} else if let Some(stripped) = arg.strip_prefix("--vcs=") {
vcs_str = Some(stripped.to_string());
} else {
clean_args.push(arg.clone());
}
}
if clean_args.is_empty() {
error("Project name not specified"); error("Project name not specified");
return 1; return 1;
} }
if args.len() > 1 { if clean_args.len() > 1 {
warn("Command does not support additional arguments"); warn("Command does not support additional arguments");
return 1; return 1;
} }
let project_name = &args[0]; let project_name = &clean_args[0];
println!( println!(
"Creating a Project `{}`...", "Creating a Project `{}`...",
colored(project_name, BOLD_CYAN) colored(project_name, BOLD_CYAN)
@@ -127,6 +146,40 @@ pub fn new(args: &[String]) -> i32 {
colored("src/main.c", BOLD_CYAN) colored("src/main.c", BOLD_CYAN)
); );
// settings VCS
let mut vcs_kind = VcsKind::Git;
if let Some(ref vcs_val) = vcs_str {
match VcsKind::parse(vcs_val) {
Ok(kind) => vcs_kind = kind,
Err(e) => {
error(&e);
return 1;
}
}
} else if let Ok(cwd) = std::env::current_dir()
&& crate::core::vcs::find_existing_vcs(&cwd).is_some()
{
vcs_kind = VcsKind::None;
}
if vcs_kind == VcsKind::Git {
if crate::utils::git::is_git_available() {
let project_path = std::path::Path::new(project_name);
if let Err(e) = crate::core::vcs::init_vcs(vcs_kind, project_path) {
warn(&format!("Failed to initialize git repository: {}", e));
} else {
println!(
" {} Initialized git repository",
colored("✔", BOLD_GREEN)
);
}
} else {
warn(
"Git is not installed or not found in PATH. Skipping Git repository initialization.",
);
}
}
println!( println!(
"Project `{}` successfully created\n", "Project `{}` successfully created\n",
colored(project_name, BOLD_GREEN) colored(project_name, BOLD_GREEN)

View File

@@ -29,43 +29,5 @@ pub fn fetch_git_dep(
return Ok(()); return Ok(());
} }
let mut builder = git2::build::RepoBuilder::new(); crate::utils::git::git_clone_and_checkout(url, target_dir, branch, tag, rev)
if let Some(b) = branch {
builder.branch(b);
}
let repo = builder
.clone(url, target_dir)
.map_err(|e| format!("failed to clone {}: {}", url, e))?;
if let Some(t) = tag {
checkout_ref(&repo, &format!("refs/tags/{}", t))?;
} else if let Some(r) = rev {
checkout_ref(&repo, r)?;
}
Ok(())
}
#[allow(dead_code)]
fn checkout_ref(repo: &git2::Repository, reference_str: &str) -> Result<(), String> {
let (object, reference) = repo
.revparse_ext(reference_str)
.map_err(|e| format!("failed to find ref {}: {}", reference_str, e))?;
repo.checkout_tree(&object, None).map_err(|e| {
let ref_name = reference.as_ref().and_then(|r| r.name()).unwrap_or("HEAD");
format!("failed to checkout {}: {}", ref_name, e)
})?;
match reference {
Some(gref) => {
let ref_name = gref.name().unwrap_or("HEAD");
repo.set_head(ref_name)
}
None => repo.set_head_detached(object.id()),
}
.map_err(|e| format!("failed to set HEAD: {}", e))?;
Ok(())
} }

View File

@@ -20,4 +20,5 @@ pub mod config;
pub mod deps; pub mod deps;
pub mod registry; pub mod registry;
pub mod runner; pub mod runner;
pub mod vcs;
pub mod workspace; pub mod workspace;

32
src/core/vcs/git.rs Normal file
View File

@@ -0,0 +1,32 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::fs;
use std::path::Path;
// creating git init and .gitignore
pub fn init(path: &Path) -> Result<(), String> {
crate::utils::git::git_init(path)?;
let gitignore_path = path.join(".gitignore");
if !gitignore_path.exists() {
fs::write(&gitignore_path, "/target\n")
.map_err(|e| format!("failed to create .gitignore: {}", e))?;
}
Ok(())
}

61
src/core/vcs/mod.rs Normal file
View File

@@ -0,0 +1,61 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod git;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VcsKind {
Git,
None,
}
impl VcsKind {
pub fn parse(s: &str) -> Result<Self, String> {
match s.to_lowercase().as_str() {
"git" => Ok(Self::Git),
"none" => Ok(Self::None),
_ => Err(format!("Unknown VCS: {}", s)),
}
}
}
pub fn init_vcs(kind: VcsKind, path: &Path) -> Result<(), String> {
match kind {
VcsKind::Git => git::init(path),
VcsKind::None => Ok(()),
}
}
// look for git repo in parents to avoid nested repos
pub fn find_existing_vcs(path: &Path) -> Option<VcsKind> {
let mut current = if path.is_file() {
path.parent()
} else {
Some(path)
};
while let Some(dir) = current {
if dir.join(".git").exists() {
return Some(VcsKind::Git);
}
current = dir.parent();
}
None
}

View File

@@ -44,7 +44,12 @@ fn main() {
"clean" => cli::clean::clean(rest), "clean" => cli::clean::clean(rest),
"gen" => cli::r#gen::r#gen(rest), "gen" => cli::r#gen::r#gen(rest),
"--version" => { "--version" => {
println!("dcr {} ({})", env!("CARGO_PKG_VERSION"), env!("DCR_TARGET")); println!(
"dcr {}{} ({})",
env!("CARGO_PKG_VERSION"),
env!("DCR_GIT_INFO"),
env!("DCR_TARGET")
);
0 0
} }
"--help" => cli::help::help(), "--help" => cli::help::help(),

112
src/utils/git.rs Normal file
View File

@@ -0,0 +1,112 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::path::Path;
use std::process::Command;
// checking installed git on system
pub fn is_git_available() -> bool {
Command::new("git")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
// warning of git not in system
#[allow(dead_code)]
pub fn warn_if_git_missing() {
if !is_git_available() {
crate::utils::log::warn(
"Git is not installed or not found in PATH. Some features (like dependency cloning and VCS initialization) may fail.",
);
}
}
// init repo in folder
pub fn git_init(path: &Path) -> Result<(), String> {
if !is_git_available() {
return Err("git executable not found in PATH".to_string());
}
let status = Command::new("git")
.arg("init")
.current_dir(path)
.status()
.map_err(|e| format!("failed to execute git init: {}", e))?;
if !status.success() {
return Err("git init failed".to_string());
}
Ok(())
}
// Clone the dependency and switch to the branch/tag/commit
#[allow(dead_code)]
pub fn git_clone_and_checkout(
url: &str,
target_dir: &Path,
branch: Option<&str>,
tag: Option<&str>,
rev: Option<&str>,
) -> Result<(), String> {
if !is_git_available() {
return Err("git executable not found in PATH".to_string());
}
let mut args = vec!["clone"];
if let Some(b) = branch {
args.push("--branch");
args.push(b);
args.push("--single-branch");
} else if let Some(t) = tag {
args.push("--branch");
args.push(t);
args.push("--single-branch");
}
args.push(url);
let target_dir_str = target_dir.to_string_lossy().to_string();
args.push(&target_dir_str);
let status = Command::new("git")
.args(&args)
.status()
.map_err(|e| format!("failed to execute git clone: {}", e))?;
if !status.success() {
return Err(format!("git clone of {} failed", url));
}
if let Some(r) = rev {
let checkout_status = Command::new("git")
.arg("checkout")
.arg(r)
.current_dir(target_dir)
.status()
.map_err(|e| format!("failed to execute git checkout: {}", e))?;
if !checkout_status.success() {
return Err(format!("git checkout to revision {} failed", r));
}
}
Ok(())
}

View File

@@ -17,5 +17,6 @@
pub mod build; pub mod build;
pub mod fs; pub mod fs;
pub mod git;
pub mod log; pub mod log;
pub mod text; pub mod text;

View File

@@ -99,6 +99,60 @@ fn new_creates_project_layout() {
); );
} }
fn is_git_in_path() -> bool {
std::process::Command::new("git")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[test]
fn new_vcs_options_work() {
// 1. Test --vcs none
let dir = unique_sandbox_dir("new_vcs_none");
let out = run_dcr(&["new", "hello_none", "--vcs", "none"], &dir);
assert!(out.status.success(), "dcr new --vcs none should succeed");
let project_dir = dir.join("hello_none");
assert!(project_dir.is_dir());
assert!(!project_dir.join(".git").exists());
assert!(!project_dir.join(".gitignore").exists());
// 2. Test --vcs git
if is_git_in_path() {
let dir = unique_sandbox_dir("new_vcs_git");
let out = run_dcr(&["new", "hello_git", "--vcs", "git"], &dir);
assert!(out.status.success(), "dcr new --vcs git should succeed");
let project_dir = dir.join("hello_git");
assert!(project_dir.is_dir());
assert!(project_dir.join(".git").is_dir());
assert!(project_dir.join(".gitignore").is_file());
let gitignore = std::fs::read_to_string(project_dir.join(".gitignore")).unwrap();
assert!(gitignore.contains("/target"));
}
}
#[test]
fn init_vcs_options_work() {
// 1. Test --vcs none
let dir = unique_sandbox_dir("init_vcs_none");
let out = run_dcr(&["init", "--vcs", "none"], &dir);
assert!(out.status.success(), "dcr init --vcs none should succeed");
assert!(!dir.join(".git").exists());
assert!(!dir.join(".gitignore").exists());
// 2. Test --vcs git
if is_git_in_path() {
let dir = unique_sandbox_dir("init_vcs_git");
let out = run_dcr(&["init", "--vcs", "git"], &dir);
assert!(out.status.success(), "dcr init --vcs git should succeed");
assert!(dir.join(".git").is_dir());
assert!(dir.join(".gitignore").is_file());
let gitignore = std::fs::read_to_string(dir.join(".gitignore")).unwrap();
assert!(gitignore.contains("/target"));
}
}
#[test] #[test]
fn init_and_clean_remove_target() { fn init_and_clean_remove_target() {
let dir = unique_sandbox_dir("init"); let dir = unique_sandbox_dir("init");