1#![doc = include_str!("../README.md")]
2
3pub mod cli;
4pub mod exe;
5
6use git_filter_tree::FilterTree;
7use git_set_attr::SetAttr;
8use std::{
9 collections::{HashMap, HashSet},
10 path::{Path, PathBuf},
11 str::FromStr,
12};
13
14use git2::Repository;
15
16fn to_git_path(p: &Path) -> String {
21 let s = p.to_string_lossy().replace('\\', "/");
22 s.strip_prefix("./").unwrap_or(&s).to_string()
23}
24
25fn build_glob_matcher(patterns: &[impl AsRef<str>]) -> Result<globset::GlobSet, git2::Error> {
28 let mut builder = globset::GlobSetBuilder::new();
29 for pat in patterns {
30 let pat = pat.as_ref();
31 let normalized = if pat.ends_with('/') {
32 format!("{}**", pat)
33 } else {
34 pat.to_string()
35 };
36 let g = globset::Glob::new(&normalized)
37 .map_err(|e| git2::Error::from_str(&format!("Invalid pattern '{}': {}", pat, e)))?;
38 builder.add(g);
39 }
40 builder
41 .build()
42 .map_err(|e| git2::Error::from_str(&e.to_string()))
43}
44
45#[derive(Clone, Hash, PartialEq, Eq)]
47pub struct VendorSource {
48 pub name: String,
50 pub url: String,
51 pub branch: Option<String>,
54 pub base: Option<String>,
58 pub patterns: Vec<String>,
60}
61
62impl VendorSource {
63 pub fn to_config(&self, cfg: &mut git2::Config) -> Result<(), git2::Error> {
64 cfg.set_str(&format!("vendor.{}.url", &self.name), &self.url)?;
65
66 if let Some(branch) = &self.branch {
67 cfg.set_str(&format!("vendor.{}.branch", &self.name), branch)?;
68 }
69
70 if let Some(base) = &self.base {
71 cfg.set_str(&format!("vendor.{}.base", &self.name), base)?;
72 }
73
74 let pattern_key = format!("vendor.{}.pattern", &self.name);
76 let _ = cfg.remove_multivar(&pattern_key, ".*");
77 for pattern in &self.patterns {
78 cfg.set_multivar(&pattern_key, "^$", pattern)?;
79 }
80
81 Ok(())
82 }
83
84 pub fn from_config(cfg: &git2::Config, name: &str) -> Result<Option<Self>, git2::Error> {
85 let name = name.to_string();
86 let mut entries = cfg.entries(Some(&format!("vendor.{name}")))?;
87
88 if entries.next().is_none() {
89 return Ok(None);
90 }
91
92 let url = cfg.get_string(&format!("vendor.{name}.url"))?;
93 let branch = cfg.get_string(&format!("vendor.{name}.branch")).ok();
94 let base = cfg.get_string(&format!("vendor.{name}.base")).ok();
95
96 let mut patterns = Vec::new();
97 let pattern_entries = cfg.multivar(&format!("vendor.{name}.pattern"), None);
98 if let Ok(pattern_entries) = pattern_entries {
99 pattern_entries.for_each(|entry| {
100 if let Some(value) = entry.value() {
101 patterns.push(value.to_string());
102 }
103 })?;
104 }
105
106 Ok(Some(Self {
107 name,
108 url,
109 branch,
110 base,
111 patterns,
112 }))
113 }
114
115 pub fn head_ref(&self) -> String {
117 format!("refs/vendor/{}", self.name)
118 }
119
120 pub fn tracking_branch(&self) -> String {
122 match &self.branch {
123 Some(branch) => branch.clone(),
124 None => "HEAD".into(),
125 }
126 }
127}
128
129fn vendors_from_config(cfg: &git2::Config) -> Result<Vec<VendorSource>, git2::Error> {
130 let mut entries = cfg.entries(Some("vendor.*"))?;
131 let mut vendor_names = std::collections::HashSet::new();
132
133 while let Some(entry) = entries.next() {
134 let entry = entry?;
135 if let Some(name) = entry.name() {
136 let parts: Vec<&str> = name.splitn(3, '.').collect();
138 if parts.len() == 3 && parts[0] == "vendor" {
139 vendor_names.insert(parts[1].to_string());
140 }
141 }
142 }
143
144 let mut vendors = Vec::new();
145 for name in vendor_names {
146 let vendor = VendorSource::from_config(cfg, &name)?;
147 if let Some(vendor) = vendor {
148 vendors.push(vendor);
149 } else {
150 return Err(git2::Error::from_str("vendor not found"));
151 }
152 }
153
154 Ok(vendors)
155}
156
157pub trait Vendor {
159 fn vendor_config(&self) -> Result<git2::Config, git2::Error>;
168
169 fn vendored_subtree(&self) -> Result<git2::Tree<'_>, git2::Error>;
171
172 fn list_vendors(&self) -> Result<Vec<VendorSource>, git2::Error>;
174
175 fn check_vendors(&self) -> Result<HashMap<VendorSource, Option<git2::Oid>>, git2::Error>;
178
179 fn track_vendor_pattern(
181 &self,
182 vendor: &VendorSource,
183 globs: &[&str],
184 path: &Path,
185 ) -> Result<(), git2::Error>;
186
187 fn refresh_vendor_attrs(
191 &self,
192 vendor: &VendorSource,
193 merged_index: &git2::Index,
194 path: &Path,
195 ) -> Result<(), git2::Error>;
196
197 fn fetch_vendor<'a>(
200 &'a self,
201 source: &VendorSource,
202 maybe_opts: Option<&mut git2::FetchOptions>,
203 ) -> Result<git2::Reference<'a>, git2::Error>;
204
205 fn add_vendor(
215 &self,
216 vendor: &VendorSource,
217 globs: &[&str],
218 path: &Path,
219 file_favor: Option<git2::FileFavor>,
220 ) -> Result<git2::Index, git2::Error>;
221
222 fn merge_vendor(
229 &self,
230 vendor: &VendorSource,
231 maybe_opts: Option<&mut git2::FetchOptions>,
232 file_favor: Option<git2::FileFavor>,
233 ) -> Result<git2::Index, git2::Error>;
234
235 fn find_vendor_base(
239 &self,
240 vendor: &VendorSource,
241 ) -> Result<Option<git2::Commit<'_>>, git2::Error>;
242
243 fn get_vendor_by_name(&self, name: &str) -> Result<Option<VendorSource>, git2::Error>;
246}
247
248fn bail_if_bare(repo: &Repository) -> Result<(), git2::Error> {
249 if repo.is_bare() {
253 return Err(git2::Error::from_str(
254 "a working tree is required; bare repositories are not supported",
255 ));
256 }
257
258 Ok(())
259}
260
261impl Vendor for Repository {
262 fn vendor_config(&self) -> Result<git2::Config, git2::Error> {
263 bail_if_bare(self)?;
264 let workdir = self
265 .workdir()
266 .ok_or_else(|| git2::Error::from_str("repository has no working directory"))?;
267
268 let mut cfg = git2::Config::new()?;
269
270 if let Some(global_path) = git2::Config::find_global()
274 .ok()
275 .and_then(|p| p.parent().map(|h| h.join(".gitvendors")))
276 .filter(|p| p.exists())
277 {
278 cfg.add_file(&global_path, git2::ConfigLevel::Global, false)?;
279 }
280
281 let local_path = self.path().join("gitvendors");
283 if local_path.exists() {
284 cfg.add_file(&local_path, git2::ConfigLevel::Local, false)?;
285 }
286
287 let index_path = workdir.join(".gitvendors");
289 cfg.add_file(&index_path, git2::ConfigLevel::App, false)?;
290
291 Ok(cfg)
292 }
293
294 fn vendored_subtree(&self) -> Result<git2::Tree<'_>, git2::Error> {
295 let head = self.head()?.peel_to_tree()?;
296
297 let mut vendored_entries: Vec<git2::TreeEntry> = Vec::new();
298
299 head.walk(git2::TreeWalkMode::PreOrder, |_, entry| {
300 if let Some(attrs) = entry.name().and_then(|name| {
301 self.get_attr(
302 &PathBuf::from_str(name).ok()?,
303 "vendored",
304 git2::AttrCheckFlags::FILE_THEN_INDEX,
305 )
306 .ok()
307 }) {
308 if attrs == Some("true") || attrs == Some("set") {
309 vendored_entries.push(entry.to_owned());
310 }
311 }
312 git2::TreeWalkResult::Ok
313 })?;
314
315 todo!()
316 }
317
318 fn list_vendors(&self) -> Result<Vec<VendorSource>, git2::Error> {
319 let cfg = self.vendor_config()?;
320 vendors_from_config(&cfg)
321 }
322
323 fn fetch_vendor<'a>(
324 &'a self,
325 vendor: &VendorSource,
326 maybe_opts: Option<&mut git2::FetchOptions>,
327 ) -> Result<git2::Reference<'a>, git2::Error> {
328 let mut remote = self.remote_anonymous(&vendor.url)?;
329 let refspec = format!("{}:{}", vendor.tracking_branch(), vendor.head_ref());
330 remote.fetch(&[&refspec], maybe_opts, None)?;
331
332 let head = self.find_reference(&vendor.head_ref())?;
333
334 Ok(head)
335 }
336
337 fn check_vendors(&self) -> Result<HashMap<VendorSource, Option<git2::Oid>>, git2::Error> {
338 let vendors = self.list_vendors()?;
339 let mut updates = HashMap::new();
340
341 for vendor in vendors {
342 match vendor.base.as_ref() {
343 Some(base) => {
344 let base = git2::Oid::from_str(base)?;
345 let head = self.find_reference(&vendor.head_ref())?.target().ok_or(
346 git2::Error::from_str("head ref was not found; this is an internal error"),
347 )?;
348
349 if base == head {
350 updates.insert(vendor, None);
351 } else {
352 updates.insert(vendor, Some(head));
353 }
354 }
355 None => {
356 let head = self.find_reference(&vendor.head_ref())?.target().ok_or(
357 git2::Error::from_str("head ref was not found; this is an internal error"),
358 )?;
359 updates.insert(vendor, Some(head));
360 }
361 }
362 }
363
364 Ok(updates)
365 }
366
367 fn track_vendor_pattern(
368 &self,
369 vendor: &VendorSource,
370 globs: &[&str],
371 path: &Path,
372 ) -> Result<(), git2::Error> {
373 let workdir = self
374 .workdir()
375 .ok_or_else(|| git2::Error::from_str("repository has no working directory"))?;
376 let gitattributes = workdir.join(path).join(".gitattributes");
377 let tree = self.find_reference(&vendor.head_ref())?.peel_to_tree()?;
378
379 for glob in globs {
380 let glob_patterns: Vec<String> = vec![glob.to_string()];
381 let matcher = build_glob_matcher(&glob_patterns)?;
382
383 let mut matched_files: Vec<String> = Vec::new();
384
385 tree.walk(git2::TreeWalkMode::PreOrder, |dir, entry| {
386 if entry.kind() != Some(git2::ObjectType::Blob) {
387 return git2::TreeWalkResult::Ok;
388 }
389 let remote_path = format!("{}{}", dir, entry.name().unwrap());
390 if matcher.is_match(&remote_path) {
391 matched_files.push(remote_path);
392 }
393 git2::TreeWalkResult::Ok
394 })?;
395
396 if matched_files.is_empty() {
397 continue;
398 }
399
400 let vendor_attr = format!("vendor={}", vendor.name);
401
402 for file in &matched_files {
403 let local_pattern = to_git_path(&path.join(file));
404 self.set_attr(&local_pattern, &[&vendor_attr], &gitattributes)?;
405 }
406 }
407
408 Ok(())
409 }
410
411 fn add_vendor(
412 &self,
413 vendor: &VendorSource,
414 globs: &[&str],
415 _path: &Path,
416 file_favor: Option<git2::FileFavor>,
417 ) -> Result<git2::Index, git2::Error> {
418 let matcher = build_glob_matcher(globs)?;
419
420 let theirs = self.find_reference(&vendor.head_ref())?.peel_to_tree()?;
422 let theirs_filtered =
423 self.filter_by_predicate(&theirs, |_repo, entry_path| matcher.is_match(entry_path))?;
424
425 let mut upstream_paths: HashSet<String> = HashSet::new();
430 theirs_filtered.walk(git2::TreeWalkMode::PreOrder, |dir, entry| {
431 if entry.kind() == Some(git2::ObjectType::Blob) {
432 upstream_paths.insert(format!("{}{}", dir, entry.name().unwrap()));
433 }
434 git2::TreeWalkResult::Ok
435 })?;
436
437 let ours = self.head()?.peel_to_tree()?;
438 let ours_filtered =
439 self.filter_by_predicate(&ours, |_repo, p| upstream_paths.contains(&*to_git_path(p)))?;
440
441 let empty_tree = {
445 let empty_oid = self.treebuilder(None)?.write()?;
446 self.find_tree(empty_oid)?
447 };
448
449 let mut opts = git2::MergeOptions::new();
450 opts.find_renames(true);
451 opts.rename_threshold(50);
452 if let Some(favor) = file_favor {
453 opts.file_favor(favor);
454 }
455
456 self.merge_trees(&empty_tree, &ours_filtered, &theirs_filtered, Some(&opts))
457 }
458
459 fn merge_vendor(
460 &self,
461 vendor: &VendorSource,
462 _maybe_opts: Option<&mut git2::FetchOptions>,
463 file_favor: Option<git2::FileFavor>,
464 ) -> Result<git2::Index, git2::Error> {
465 let matcher = build_glob_matcher(&vendor.patterns)?;
468 let theirs = self.find_reference(&vendor.head_ref())?.peel_to_tree()?;
469 let theirs_filtered =
470 self.filter_by_predicate(&theirs, |_repo, path| matcher.is_match(path))?;
471
472 let expected_vendor = vendor.name.clone();
476 let ours = self.head()?.peel_to_tree()?;
477 let ours_filtered = self.filter_by_predicate(&ours, |repo, path| {
478 match repo.get_attr(path, "vendor", git2::AttrCheckFlags::FILE_THEN_INDEX) {
479 Ok(Some(value)) if value == expected_vendor => true,
480 _ => matcher.is_match(path),
481 }
482 })?;
483
484 let mut opts = git2::MergeOptions::new();
485 opts.find_renames(true);
486 opts.rename_threshold(50);
487 if let Some(favor) = file_favor {
488 opts.file_favor(favor);
489 }
490
491 let base_commit = self.find_vendor_base(&vendor)?;
492 let base_full_tree;
493 let base = match &base_commit {
494 Some(c) => {
495 base_full_tree = c.as_object().peel_to_tree()?;
496 self.filter_by_predicate(&base_full_tree, |_repo, path| matcher.is_match(path))?
497 }
498 None => self.find_tree(ours_filtered.id())?,
499 };
500
501 self.merge_trees(&base, &ours_filtered, &theirs_filtered, Some(&opts))
502 }
503
504 fn refresh_vendor_attrs(
505 &self,
506 vendor: &VendorSource,
507 merged_index: &git2::Index,
508 path: &Path,
509 ) -> Result<(), git2::Error> {
510 let workdir = self
511 .workdir()
512 .ok_or_else(|| git2::Error::from_str("repository has no working directory"))?;
513 let gitattributes = workdir.join(path).join(".gitattributes");
514 let vendor_attr = format!("vendor={}", vendor.name);
515 let matcher = build_glob_matcher(&vendor.patterns)?;
516
517 let mut merged_paths: HashSet<PathBuf> = HashSet::new();
519 for entry in merged_index.iter() {
520 let stage = (entry.flags >> 12) & 0x3;
521 if stage != 0 {
522 continue;
523 }
524 if let Ok(entry_path) = std::str::from_utf8(&entry.path) {
525 let p = PathBuf::from(entry_path);
526 if matcher.is_match(&p) {
527 merged_paths.insert(p);
528 }
529 }
530 }
531
532 let needle = format!("vendor={}", vendor.name);
535 let mut lines: Vec<String> = if gitattributes.exists() {
536 let content = std::fs::read_to_string(&gitattributes)
537 .map_err(|e| git2::Error::from_str(&format!("read .gitattributes: {e}")))?;
538 content
539 .lines()
540 .filter(|line| {
541 !line.split_whitespace().any(|tok| tok == needle)
543 })
544 .map(String::from)
545 .collect()
546 } else {
547 Vec::new()
548 };
549
550 let mut sorted: Vec<_> = merged_paths.into_iter().collect();
552 sorted.sort();
553 for file in sorted {
554 let local_pattern = path.join(&file);
555 let line = format!("{} {}", to_git_path(&local_pattern), vendor_attr);
556 lines.push(line);
557 }
558
559 if let Some(parent) = gitattributes.parent() {
561 std::fs::create_dir_all(parent).map_err(|e| {
562 git2::Error::from_str(&format!("create dir for .gitattributes: {e}"))
563 })?;
564 }
565 let mut content = lines.join("\n");
566 if !content.is_empty() && !content.ends_with('\n') {
567 content.push('\n');
568 }
569 std::fs::write(&gitattributes, &content)
570 .map_err(|e| git2::Error::from_str(&format!("write .gitattributes: {e}")))?;
571 Ok(())
572 }
573
574 fn find_vendor_base(
575 &self,
576 vendor: &VendorSource,
577 ) -> Result<Option<git2::Commit<'_>>, git2::Error> {
578 match vendor.base.as_ref() {
579 Some(base) => {
580 let oid = git2::Oid::from_str(base)?;
581 let commit = self.find_commit(oid)?;
582 return Ok(Some(commit));
583 }
584 _ => Ok(None),
585 }
586 }
587
588 fn get_vendor_by_name(&self, name: &str) -> Result<Option<VendorSource>, git2::Error> {
589 let gitvendors = self.vendor_config()?;
590 VendorSource::from_config(&gitvendors, name)
591 }
592}
593
594#[cfg(test)]
595mod tests;