1use crate::{Callback, CommandFailed, Error};
2
3use std::env::{self, current_dir};
4use std::ffi::OsStr;
5use std::fs::{create_dir_all, File};
6use std::io::{self, Write};
7use std::os::unix::fs::symlink;
8use std::path::{Path, PathBuf};
9use std::process::{Command, Output};
10use std::sync::atomic::{AtomicBool, Ordering};
11
12use crossbeam::channel;
13use url::Url;
14
15static SEEN: &str = "AUR_SEEN";
16
17pub type Result<T> = std::result::Result<T, Error>;
19
20pub struct Repo {
22 pub url: Url,
24 pub name: String,
26}
27
28#[derive(Clone, Debug)]
33pub struct Fetch {
34 pub clone_dir: PathBuf,
36 pub diff_dir: PathBuf,
38 pub git: PathBuf,
40 pub git_flags: Vec<String>,
42 pub aur_url: Url,
44}
45
46fn command_err(cmd: &Command, stderr: Option<String>) -> Error {
47 Error::CommandFailed(CommandFailed {
48 dir: cmd.get_current_dir().unwrap().to_owned(),
49 command: cmd.get_program().to_owned().into(),
50 args: cmd
51 .get_args()
52 .map(|s| s.to_string_lossy().into_owned())
53 .collect(),
54 stderr,
55 })
56}
57
58impl Fetch {
59 pub fn new() -> Result<Self> {
64 Ok(Self {
65 clone_dir: env::current_dir()?,
66 diff_dir: env::current_dir()?,
67 git: "git".into(),
68 git_flags: Vec::new(),
69 aur_url: "https://aur.archlinux.org".parse().unwrap(),
70 })
71 }
72
73 pub fn with_cache_dir<P: AsRef<Path>>(path: P) -> Self {
78 let path = path.as_ref();
79
80 Self {
81 clone_dir: path.join("clone"),
82 diff_dir: path.join("diff"),
83 git: "git".into(),
84 git_flags: Vec::new(),
85 aur_url: "https://aur.archlinux.org".parse().unwrap(),
86 }
87 }
88
89 pub fn with_combined_cache_dir<P: AsRef<Path>>(path: P) -> Self {
93 let path = path.as_ref();
94
95 Self {
96 clone_dir: path.into(),
97 diff_dir: path.into(),
98 git: "git".into(),
99 git_flags: Vec::new(),
100 aur_url: "https://aur.archlinux.org".parse().unwrap(),
101 }
102 }
103
104 pub fn download<S: AsRef<str> + Send + Sync>(&self, pkgs: &[S]) -> Result<Vec<String>> {
120 self.download_cb(pkgs, |_| ())
121 }
122
123 pub fn download_cb<S: AsRef<str> + Send + Sync, F: Fn(Callback)>(
127 &self,
128 pkgs: &[S],
129 f: F,
130 ) -> Result<Vec<String>> {
131 let repos = pkgs
132 .iter()
133 .map(|p| {
134 let mut url = self.aur_url.clone();
135 url.set_path(p.as_ref());
136 Repo {
137 url,
138 name: p.as_ref().to_string(),
139 }
140 })
141 .collect::<Vec<_>>();
142 self.download_repos_cb(&repos, f)
143 }
144
145 pub fn download_repos<F: Fn(Callback)>(&self, repos: &[Repo]) -> Result<Vec<String>> {
147 self.download_repos_cb(repos, |_| ())
148 }
149
150 pub fn download_repos_cb<F: Fn(Callback)>(&self, repos: &[Repo], f: F) -> Result<Vec<String>> {
154 let (pkg_send, pkg_rec) = channel::bounded(0);
155 let (fetched_send, fetched_rec) = channel::bounded(32);
156 let f = &f;
157 let stop = &AtomicBool::new(false);
158 let mut fetched = Vec::with_capacity(repos.len());
159
160 std::thread::scope(|scope| {
161 scope.spawn(move || {
162 for repo in repos {
163 if pkg_send.send(repo).is_err() {
164 break;
165 }
166 }
167 });
168
169 for _ in 0..20.min(repos.len()) {
170 let fetched_send = fetched_send.clone();
171 let pkg_rec = pkg_rec.clone();
172 scope.spawn(move || {
173 for repo in &pkg_rec {
174 if stop.load(Ordering::Acquire) {
175 break;
176 }
177 match self.download_pkg(&repo.url, &repo.name) {
178 Ok((fetched, out)) => {
179 let _ = fetched_send.send(Ok((repo.name.clone(), fetched, out)));
180 }
181 Err(e) => {
182 stop.store(true, Ordering::Release);
183 let _ = fetched_send.send(Err(e));
184 break;
185 }
186 }
187 }
188 });
189 }
190
191 drop(pkg_rec);
192 drop(fetched_send);
193
194 for (n, msg) in fetched_rec.into_iter().enumerate() {
195 let (pkg, was_fetched, out) = msg?;
196 f(Callback {
197 pkg: &pkg,
198 n: n + 1,
199 output: String::from_utf8_lossy(&out).trim(),
200 });
201 if was_fetched {
202 fetched.push(pkg)
203 }
204 }
205
206 Ok(fetched)
207 })
208 }
209
210 fn download_pkg<S: AsRef<str>>(&self, url: &Url, dir: S) -> Result<(bool, Vec<u8>)> {
211 self.mk_clone_dir()?;
212
213 let dir = dir.as_ref();
214 let is_git_repo = self.is_git_repo(dir);
215
216 let mut command = Command::new(&self.git);
217
218 let fetched = if is_git_repo {
219 command.current_dir(self.clone_dir.join(dir));
220 command.args(["fetch", "-v"]);
221 true
222 } else {
223 command.current_dir(&self.clone_dir);
224 command.args(["clone", "--no-progress", "--", url.as_str(), dir]);
225 false
226 };
227 log_cmd(&command);
228 let output = command
229 .output()
230 .map_err(|e| command_err(&command, Some(e.to_string())))?;
231
232 if !output.status.success() {
233 return Err(command_err(
234 &command,
235 Some(String::from_utf8_lossy(&output.stderr).into_owned()),
236 ));
237 }
238
239 Ok((fetched, output.stderr))
240 }
241
242 pub fn has_diff<'a, S: AsRef<str>>(&self, pkgs: &'a [S]) -> Result<Vec<&'a str>> {
246 let mut ret = Vec::new();
247
248 for pkg in pkgs {
249 if git_has_diff(
250 &self.git,
251 &self.git_flags,
252 self.clone_dir.join(pkg.as_ref()),
253 )? {
254 ret.push(pkg.as_ref());
255 }
256 }
257
258 Ok(ret)
259 }
260
261 pub fn unseen<'a, S: AsRef<str>>(&self, pkgs: &'a [S]) -> Result<Vec<&'a str>> {
265 let mut ret = Vec::new();
266
267 for pkg in pkgs {
268 if git_unseen(
269 &self.git,
270 &self.git_flags,
271 self.clone_dir.join(pkg.as_ref()),
272 )? {
273 ret.push(pkg.as_ref());
274 }
275 }
276
277 Ok(ret)
278 }
279
280 pub fn diff<S: AsRef<str>>(&self, pkgs: &[S], color: bool) -> Result<Vec<String>> {
287 let pkgs = pkgs.iter();
288 let mut ret = Vec::new();
289
290 for pkg in pkgs {
291 let output = git_log(
292 &self.git,
293 &self.git_flags,
294 self.clone_dir.join(pkg.as_ref()),
295 color,
296 )?;
297 let mut s: String = String::from_utf8_lossy(&output.stdout).into();
298 let output = git_diff(
299 &self.git,
300 &self.git_flags,
301 self.clone_dir.join(pkg.as_ref()),
302 color,
303 )?;
304 s.push_str(&String::from_utf8_lossy(&output.stdout));
305 s.push('\n');
306 ret.push(s);
307 }
308
309 Ok(ret)
310 }
311
312 pub fn print_diff<S: AsRef<str>>(&self, pkg: S) -> Result<()> {
317 show_git_diff(
318 &self.git,
319 &self.git_flags,
320 self.clone_dir.join(pkg.as_ref()),
321 )
322 }
323
324 pub fn save_diffs<S: AsRef<str>>(&self, pkgs: &[S]) -> Result<()> {
328 self.mk_diff_dir()?;
329
330 for pkg in pkgs {
331 let mut path = self.diff_dir.join(pkg.as_ref());
332 path.set_extension("diff");
333
334 let mut file = File::create(path)?;
335
336 file.write_all(
337 &git_log(
338 &self.git,
339 &self.git_flags,
340 self.clone_dir.join(pkg.as_ref()),
341 false,
342 )?
343 .stdout,
344 )?;
345 file.write_all(b"\n")?;
346 file.write_all(
347 &git_diff(
348 &self.git,
349 &self.git_flags,
350 self.clone_dir.join(pkg.as_ref()),
351 false,
352 )?
353 .stdout,
354 )?;
355 }
356
357 Ok(())
358 }
359
360 pub fn make_view<P: AsRef<Path>, S1: AsRef<str>, S2: AsRef<str>>(
367 &self,
368 dir: P,
369 pkgs: &[S1],
370 diffs: &[S2],
371 ) -> Result<()> {
372 let dir = dir.as_ref();
373
374 for pkg in diffs {
375 let pkg = format!("{}.diff", pkg.as_ref());
376 let dest = dir.join(&pkg);
377 let src = self.diff_dir.join(&pkg);
378 if src.is_file() {
379 symlink(src, dest)?;
380 }
381 }
382
383 for pkg in pkgs {
384 let dest = dir.join(pkg.as_ref());
385 let pkgbuild_dest = dir.join(format!("{}.PKGBUILD", pkg.as_ref()));
386 let srcinfo_dest = dir.join(format!("{}.SRCINFO", pkg.as_ref()));
387
388 let src = self.clone_dir.join(pkg.as_ref());
389 if src.is_dir() {
390 symlink(src, dest)?;
391 }
392
393 let src = self.clone_dir.join(pkg.as_ref()).join("PKGBUILD");
394 if src.is_file() {
395 symlink(src, pkgbuild_dest)?;
396 }
397
398 let src = self.clone_dir.join(pkg.as_ref()).join(".SRCINFO");
399 if src.is_file() {
400 symlink(src, srcinfo_dest)?;
401 }
402 }
403
404 Ok(())
405 }
406
407 pub fn merge<S: AsRef<str>>(&self, pkgs: &[S]) -> Result<()> {
409 self.merge_cb(pkgs, |_| ())
410 }
411
412 pub fn merge_cb<S: AsRef<str>, F: Fn(Callback)>(&self, pkgs: &[S], cb: F) -> Result<()> {
414 let pkgs = pkgs.iter();
415
416 for (n, pkg) in pkgs.enumerate() {
417 let path = self.clone_dir.join(pkg.as_ref());
418 let output = git_rebase(&self.git, &self.git_flags, path)?;
419 cb(Callback {
420 pkg: pkg.as_ref(),
421 n,
422 output: String::from_utf8_lossy(&output.stdout).trim(),
423 });
424 }
425
426 Ok(())
427 }
428
429 pub fn mark_seen<S: AsRef<str>>(&self, pkgs: &[S]) -> Result<()> {
433 for pkg in pkgs {
434 let path = self.clone_dir.join(pkg.as_ref());
435 git_mark_seen(&self.git, &self.git_flags, path)?;
436 }
437
438 Ok(())
439 }
440
441 pub fn commit<S1: AsRef<str>, S2: AsRef<str>>(&self, pkgs: &[S1], message: S2) -> Result<()> {
445 for pkg in pkgs {
446 let path = self.clone_dir.join(pkg.as_ref());
447 git_commit(&self.git, &self.git_flags, path, message.as_ref())?;
448 }
449
450 Ok(())
451 }
452
453 pub fn is_git_repo<S: AsRef<str>>(&self, pkg: S) -> bool {
455 self.clone_dir.join(pkg.as_ref()).join(".git").is_dir()
456 }
457
458 fn mk_clone_dir(&self) -> io::Result<()> {
459 create_dir_all(&self.clone_dir)
460 }
461
462 fn mk_diff_dir(&self) -> io::Result<()> {
463 create_dir_all(&self.diff_dir)
464 }
465}
466
467fn color_str(color: bool) -> &'static str {
468 if color {
469 "--color=always"
470 } else {
471 "--color=never"
472 }
473}
474
475fn git_command<S: AsRef<OsStr>, P: AsRef<Path>>(
476 git: S,
477 path: P,
478 flags: &[String],
479 args: &[&str],
480) -> Result<Output> {
481 let mut command = Command::new(git.as_ref());
482 command
483 .current_dir(path.as_ref())
484 .args(flags)
485 .args(args)
486 .env("GIT_TERMINAL_PROMPT", "0");
487
488 log_cmd(&command);
489 let output = command
490 .output()
491 .map_err(|e| command_err(&command, Some(e.to_string())))?;
492
493 if output.status.success() {
494 Ok(output)
495 } else {
496 Err(command_err(
497 &command,
498 Some(String::from_utf8_lossy(&output.stderr).into()),
499 ))
500 }
501}
502
503fn show_git_command<S: AsRef<OsStr>, P: AsRef<Path>>(
504 git: S,
505 path: P,
506 flags: &[String],
507 args: &[&str],
508) -> Result<()> {
509 let mut command = Command::new(git.as_ref());
510 command
511 .current_dir(path.as_ref())
512 .args(flags)
513 .args(args)
514 .env("GIT_TERMINAL_PROMPT", "0");
515
516 log_cmd(&command);
517 let status = command
518 .spawn()
519 .map_err(|e| command_err(&command, Some(e.to_string())))?
520 .wait()
521 .map_err(|e| command_err(&command, Some(e.to_string())))?;
522
523 if status.success() {
524 Ok(())
525 } else {
526 Err(command_err(&command, None))
527 }
528}
529
530fn git_mark_seen<S: AsRef<OsStr>, P: AsRef<Path>>(
531 git: S,
532 flags: &[String],
533 path: P,
534) -> Result<Output> {
535 git_command(&git, &path, flags, &["update-ref", SEEN, "HEAD"])
536}
537
538fn git_rebase<S: AsRef<OsStr>, P: AsRef<Path>>(
539 git: S,
540 flags: &[String],
541 path: P,
542) -> Result<Output> {
543 git_command(&git, &path, flags, &["reset", "--hard", "-q", "HEAD"])?;
544 if git_command(&git, &path, flags, &["symbolic-ref", "-q", "HEAD"]).is_err() {
545 git_command(&git, &path, flags, &["checkout", "master"])?;
546 }
547 git_command(&git, &path, flags, &["rebase", "--stat"])
548}
549
550fn git_unseen<S: AsRef<OsStr>, P: AsRef<Path>>(git: S, flags: &[String], path: P) -> Result<bool> {
551 if git_has_seen(&git, flags, &path)? {
552 let is_unseen = git_command(
553 git,
554 path,
555 flags,
556 &["merge-base", "--is-ancestor", "HEAD@{u}", "AUR_SEEN"],
557 )
558 .is_err();
559 Ok(is_unseen)
560 } else {
561 Ok(true)
562 }
563}
564
565fn git_has_diff<S: AsRef<OsStr>, P: AsRef<Path>>(
566 git: S,
567 flags: &[String],
568 path: P,
569) -> Result<bool> {
570 if git_has_seen(&git, flags, &path)? {
571 let output = git_command(git, path, flags, &["rev-parse", SEEN, "HEAD@{u}"])?;
572
573 let s = String::from_utf8_lossy(&output.stdout);
574 let mut s = s.split('\n');
575
576 let head = s.next().unwrap();
577 let upstream = s.next().unwrap();
578
579 Ok(head != upstream)
580 } else {
581 Ok(false)
582 }
583}
584
585fn git_log<S: AsRef<OsStr>, P: AsRef<Path>>(
586 git: S,
587 flags: &[String],
588 path: P,
589 color: bool,
590) -> Result<Output> {
591 let color = color_str(color);
592 git_command(git, path, flags, &["log", "..HEAD@{u}", color])
593}
594
595fn git_has_seen<S: AsRef<OsStr>, P: AsRef<Path>>(
596 git: S,
597 flags: &[String],
598 path: P,
599) -> Result<bool> {
600 let output = git_command(&git, &path, flags, &["rev-parse", "--verify", SEEN]).is_ok();
601 Ok(output)
602}
603
604fn git_head<S: AsRef<OsStr>, P: AsRef<Path>>(git: S, flags: &[String], path: P) -> Result<String> {
605 let output = git_command(git, path, flags, &["rev-parse", "HEAD"])?;
606 let output = String::from_utf8_lossy(&output.stdout);
607 Ok(output.trim().to_string())
608}
609
610fn git_diff<S: AsRef<OsStr>, P: AsRef<Path>>(
611 git: S,
612 flags: &[String],
613 path: P,
614 color: bool,
615) -> Result<Output> {
616 let color = color_str(color);
617 let head = git_head(&git, flags, &path)?;
618 let output = if git_has_seen(&git, flags, &path)? {
619 git_command(&git, &path, flags, &["reset", "--hard", SEEN])?;
620 git_command(
621 &git,
622 &path,
623 flags,
624 &[
625 "-c",
626 "user.email=aur",
627 "-c",
628 "user.name=aur",
629 "merge",
630 "--no-edit",
631 "--no-ff",
632 "--no-commit",
633 ],
634 )?;
635 Ok(git_command(
636 &git,
637 &path,
638 flags,
639 &[
640 "diff",
641 "--stat",
642 "--patch",
643 "--cached",
644 color,
645 "--",
646 ":!.SRCINFO",
647 ],
648 )?)
649 } else {
650 Ok(git_command(
651 &git,
652 &path,
653 flags,
654 &[
655 "diff",
656 "--stat",
657 "--patch",
658 color,
659 "4b825dc642cb6eb9a060e54bf8d69288fbee4904..HEAD@{u}",
660 "--",
661 ":!.SRCINFO",
662 ],
663 )?)
664 };
665
666 git_command(&git, &path, flags, &["reset", "--hard", &head])?;
667 output
668}
669
670fn show_git_diff<S: AsRef<OsStr>, P: AsRef<Path>>(git: S, flags: &[String], path: P) -> Result<()> {
671 let head = git_head(&git, flags, &path)?;
672 if git_has_seen(&git, flags, &path)? {
673 git_command(&git, &path, flags, &["reset", "--hard", SEEN])?;
674 git_command(
675 &git,
676 &path,
677 flags,
678 &[
679 "-c",
680 "user.email=aur",
681 "-c",
682 "user.name=aur",
683 "merge",
684 "--no-edit",
685 "--no-ff",
686 "--no-commit",
687 ],
688 )?;
689 show_git_command(
690 &git,
691 &path,
692 flags,
693 &["diff", "--stat", "--patch", "--cached", "--", ":!.SRCINFO"],
694 )?;
695 } else {
696 show_git_command(
697 &git,
698 &path,
699 flags,
700 &[
701 "diff",
702 "--stat",
703 "--patch",
704 "4b825dc642cb6eb9a060e54bf8d69288fbee4904..HEAD@{u}",
705 "--",
706 ":!.SRCINFO",
707 ],
708 )?;
709 }
710
711 git_command(&git, &path, flags, &["reset", "--hard", &head])?;
712 Ok(())
713}
714
715fn git_commit<S: AsRef<OsStr>, P: AsRef<Path>>(
716 git: S,
717 flags: &[String],
718 path: P,
719 message: &str,
720) -> Result<()> {
721 let path = path.as_ref();
722 let git = git.as_ref();
723
724 let has_user = git_command(git, path, flags, &["config", "user.name"]).is_ok()
725 && git_command(git, path, flags, &["config", "user.email"]).is_ok();
726
727 if git_command(git, path, flags, &["diff", "--exit-code"]).is_err() {
728 if has_user {
729 git_command(git, path, flags, &["commit", "-am", message])?;
730 } else {
731 git_command(
732 git,
733 path,
734 flags,
735 &[
736 "-c",
737 "user.email=aur",
738 "-c",
739 "user.name=aur",
740 "commit",
741 "-am",
742 "AUR",
743 ],
744 )?;
745 }
746 }
747
748 Ok(())
749}
750
751fn log_cmd(cmd: &Command) {
752 if log::log_enabled!(log::Level::Debug) {
753 let bin = cmd.get_program().to_string_lossy().to_string();
754 let args = cmd
755 .get_args()
756 .map(|s| s.to_string_lossy().to_string())
757 .collect::<Vec<_>>()
758 .join(" ");
759 let dir = cmd
760 .get_current_dir()
761 .map(|p| p.to_owned())
762 .unwrap_or_else(|| current_dir().unwrap_or_else(|_| "?".into()));
763 let dir = dir.display();
764 log::debug!("running: CWD={dir} {bin} {args}")
765 }
766}