Skip to content

Commit 800a2f4

Browse files
committed
A version of organize which works; in theory
More journey tests are needed to evaluate the actual EXECUTE mode.
1 parent e4dc964 commit 800a2f4

File tree

1 file changed

+108
-7
lines changed

1 file changed

+108
-7
lines changed

gitoxide-core/src/organize.rs

Lines changed: 108 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use bstr::ByteSlice;
12
use git_features::{fs, progress::Progress};
23
use std::path::{Path, PathBuf};
34

@@ -13,30 +14,130 @@ impl Default for Mode {
1314
}
1415
}
1516

16-
// TODO: handle nested repos, skip everything inside a parent directory.
17-
fn find_git_repositories(root: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
17+
// TODO: handle nested repos, skip everything inside a parent directory, stop recursing into git workdirs
18+
fn find_git_repository_workdirs(root: impl AsRef<Path>, mut progress: impl Progress) -> impl Iterator<Item = PathBuf> {
19+
progress.init(None, git_features::progress::count("filesystem items"));
1820
fn is_repository(path: &PathBuf) -> bool {
19-
path.is_dir() && path.ends_with(".git")
21+
if !(path.is_dir() && path.ends_with(".git")) {
22+
return false;
23+
}
24+
path.join("HEAD").is_file() && path.join("config").is_file()
25+
}
26+
fn into_workdir(path: PathBuf) -> PathBuf {
27+
fn is_bare(path: &Path) -> bool {
28+
!path.join("index").exists()
29+
}
30+
if is_bare(&path) {
31+
path
32+
} else {
33+
path.parent().expect("git is never in the root").to_owned()
34+
}
2035
}
2136

2237
let walk = fs::sorted(fs::WalkDir::new(root).follow_links(false));
2338
walk.into_iter()
24-
.filter_map(Result::ok)
39+
.filter_map(move |entry| {
40+
progress.step();
41+
match entry {
42+
Ok(entry) => Some(entry),
43+
Err(err) => {
44+
progress.fail(format!("Ignored: {}", err.to_string()));
45+
None
46+
}
47+
}
48+
})
2549
.map(|entry: fs::DirEntry| fs::direntry_path(&entry))
2650
.filter(is_repository)
51+
.map(into_workdir)
2752
}
2853

29-
pub fn run(_mode: Mode, source_dir: PathBuf, _destination: PathBuf, _progress: impl Progress) -> anyhow::Result<()> {
30-
let _repo_paths = find_git_repositories(source_dir);
54+
fn find_origin_remote(repo: &Path) -> anyhow::Result<Option<git_url::Url>> {
55+
let out = std::process::Command::new("git")
56+
.args(&["remote", "--verbose"])
57+
.current_dir(repo)
58+
.output()?;
59+
if out.status.success() {
60+
Ok(parse::remotes_from_git_remote_verbose(&out.stdout)?
61+
.into_iter()
62+
.find_map(|(origin, url)| if origin == "origin" { Some(url) } else { None }))
63+
} else {
64+
anyhow::bail!(
65+
"git invocation failed with code {:?}: {}",
66+
out.status.code(),
67+
out.stderr.as_bstr()
68+
)
69+
}
70+
}
71+
72+
fn handle(mode: Mode, git_workdir: &Path, destination: &Path, progress: &mut impl Progress) -> anyhow::Result<()> {
73+
fn to_relative(path: PathBuf) -> PathBuf {
74+
std::iter::once(std::path::Component::CurDir)
75+
.chain(path.components())
76+
.collect()
77+
}
78+
79+
let url = match find_origin_remote(git_workdir)? {
80+
None => {
81+
progress.info(format!(
82+
"Skipping repository {:?} as it does not have any remote",
83+
git_workdir.display()
84+
));
85+
return Ok(());
86+
}
87+
Some(url) => url,
88+
};
89+
if url.path.is_empty() {
90+
progress.info(format!(
91+
"Skipping repository at {:?} whose remote does not have a path: {:?}",
92+
git_workdir.display(),
93+
url.to_string()
94+
));
95+
return Ok(());
96+
}
97+
98+
let destination = destination.join(to_relative(git_url::expand_path(None, url.path.as_bstr())?));
99+
match mode {
100+
Mode::Simulate => progress.info(format!(
101+
"WOULD move {} to {}",
102+
git_workdir.display(),
103+
destination.display()
104+
)),
105+
Mode::Execute => {
106+
std::fs::rename(git_workdir, &destination)?;
107+
progress.info(format!("Moved {} to {}", git_workdir.display(), destination.display()))
108+
}
109+
}
31110
Ok(())
32111
}
33112

113+
pub fn run(mode: Mode, source_dir: PathBuf, destination: PathBuf, mut progress: impl Progress) -> anyhow::Result<()> {
114+
let search_progress = progress.add_child("Searching repositories");
115+
116+
let mut num_errors = 0usize;
117+
for path_to_move in find_git_repository_workdirs(source_dir, search_progress) {
118+
if let Err(err) = handle(mode, &path_to_move, &destination, &mut progress) {
119+
progress.fail(format!(
120+
"Error when handling directory {:?}: {}",
121+
path_to_move.display(),
122+
err.to_string()
123+
));
124+
num_errors += 1;
125+
}
126+
}
127+
128+
if num_errors > 0 {
129+
anyhow::bail!("Failed to handle {} repositories", num_errors)
130+
} else {
131+
Ok(())
132+
}
133+
}
134+
34135
mod parse {
35136
use anyhow::{bail, Context};
36137
use bstr::{BStr, ByteSlice};
37138

38139
#[allow(unused)]
39-
fn remotes_from_git_remote_verbose(input: &[u8]) -> anyhow::Result<Vec<(&BStr, git_url::Url)>> {
140+
pub fn remotes_from_git_remote_verbose(input: &[u8]) -> anyhow::Result<Vec<(&BStr, git_url::Url)>> {
40141
fn parse_line(line: &BStr) -> anyhow::Result<(&BStr, git_url::Url)> {
41142
let mut tokens = line.splitn(2, |b| *b == b'\t');
42143
Ok(match (tokens.next(), tokens.next(), tokens.next()) {

0 commit comments

Comments
 (0)