Skip to content

Commit b409924

Browse files
feat(context-provider): add support for indexing multiple branches (#4419)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 702240a commit b409924

40 files changed

Lines changed: 2021 additions & 1030 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/llama-cpp-server/src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,11 @@ impl CompletionServer {
107107

108108
#[async_trait]
109109
impl CompletionStream for CompletionServer {
110-
async fn generate(&self, prompt: &str, options: CompletionOptions) -> BoxStream<'life0, String> {
110+
async fn generate(
111+
&self,
112+
prompt: &str,
113+
options: CompletionOptions,
114+
) -> BoxStream<'life0, String> {
111115
self.completion.generate(prompt, options).await
112116
}
113117
}

crates/tabby-common/src/axum.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ impl AllowedCodeRepository {
4747
.into_iter()
4848
.enumerate()
4949
.map(|(i, repo)| {
50-
CodeRepository::new(repo.git_url(), &crate::config::config_index_to_id(i))
50+
CodeRepository::new(
51+
repo.git_url(),
52+
&crate::config::config_index_to_id(i),
53+
repo.git_refs(),
54+
)
5155
})
5256
.collect()
5357
})
@@ -85,7 +89,9 @@ mod tests {
8589
let candidates: Vec<_> = $candidates
8690
.into_iter()
8791
.enumerate()
88-
.map(|(i, x)| CodeRepository::new(&x, &crate::config::config_index_to_id(i)))
92+
.map(|(i, x)| {
93+
CodeRepository::new(&x, &crate::config::config_index_to_id(i), vec![])
94+
})
8995
.collect();
9096
let expect = &candidates[0];
9197
assert_eq!(
@@ -100,7 +106,9 @@ mod tests {
100106
let candidates: Vec<_> = $candidates
101107
.into_iter()
102108
.enumerate()
103-
.map(|(i, x)| CodeRepository::new(&x, &crate::config::config_index_to_id(i)))
109+
.map(|(i, x)| {
110+
CodeRepository::new(&x, &crate::config::config_index_to_id(i), vec![])
111+
})
104112
.collect();
105113
assert_eq!(closest_match($query, &candidates), None);
106114
};

crates/tabby-common/src/config.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,19 @@ pub fn config_id_to_index(id: &str) -> Result<usize, anyhow::Error> {
156156
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
157157
pub struct RepositoryConfig {
158158
git_url: String,
159+
#[serde(default)]
160+
pub refs: Vec<String>,
159161
}
160162

161163
impl RepositoryConfig {
162164
pub fn git_url(&self) -> &str {
163165
&self.git_url
164166
}
165167

168+
pub fn git_refs(&self) -> Vec<String> {
169+
self.refs.clone()
170+
}
171+
166172
pub fn canonicalize_url(url: &str) -> String {
167173
let url = url.strip_suffix(".git").unwrap_or(url);
168174
url::Url::parse(url)
@@ -472,13 +478,15 @@ impl AnswerConfig {
472478
pub struct CodeRepository {
473479
pub git_url: String,
474480
pub source_id: String,
481+
pub git_refs: Vec<String>,
475482
}
476483

477484
impl CodeRepository {
478-
pub fn new(git_url: &str, source_id: &str) -> Self {
485+
pub fn new(git_url: &str, source_id: &str, git_refs: Vec<String>) -> Self {
479486
Self {
480487
git_url: git_url.to_owned(),
481488
source_id: source_id.to_owned(),
489+
git_refs,
482490
}
483491
}
484492

@@ -643,6 +651,7 @@ mod tests {
643651
fn it_parses_local_dir() {
644652
let repo = RepositoryConfig {
645653
git_url: "file:///home/user".to_owned(),
654+
refs: vec![],
646655
};
647656
let _ = repo.dir();
648657
}
@@ -651,6 +660,7 @@ mod tests {
651660
fn test_repository_config_name() {
652661
let repo = RepositoryConfig {
653662
git_url: "https://github.com/TabbyML/tabby.git".to_owned(),
663+
refs: vec![],
654664
};
655665
assert!(repo.dir().ends_with("https_github.com_TabbyML_tabby"));
656666
}

crates/tabby-git/src/lib.rs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ mod file_search;
33
mod grep;
44
mod serve_git;
55

6-
use std::path::Path;
6+
use std::{fs, path::Path, process::Command};
77

8+
use anyhow::bail;
89
use axum::{
910
body::Body,
1011
http::{Response, StatusCode},
@@ -13,6 +14,7 @@ pub use commit::{stream_commits, Commit};
1314
use file_search::GitFileSearch;
1415
use futures::Stream;
1516
pub use grep::{GrepFile, GrepLine, GrepSubMatch, GrepTextOrBase64};
17+
use tracing::warn;
1618

1719
pub async fn search_files(
1820
root: &Path,
@@ -56,6 +58,7 @@ pub fn serve_file(
5658
serve_git::serve(&repository, commit, path)
5759
}
5860

61+
#[derive(Debug)]
5962
pub struct GitReference {
6063
pub name: String,
6164
pub commit: String,
@@ -76,6 +79,78 @@ pub fn list_refs(root: &Path) -> anyhow::Result<Vec<GitReference>> {
7679
.collect())
7780
}
7881

82+
pub fn get_head_name(root: &Path) -> anyhow::Result<String> {
83+
let repository = git2::Repository::open(root)?;
84+
let head = repository.head()?;
85+
let name = head.name().ok_or(anyhow::anyhow!("HEAD has no name"))?;
86+
Ok(name.to_string())
87+
}
88+
89+
pub fn sync_refs(root: &Path, url: &str, refs: &Vec<String>) -> anyhow::Result<()> {
90+
if !root.exists() {
91+
fs::create_dir_all(root)?;
92+
let status = Command::new("git")
93+
.current_dir(root.parent().expect("Must not be in root directory"))
94+
.arg("clone")
95+
.arg(url)
96+
.arg(root)
97+
.status()?;
98+
99+
if let Some(code) = status.code() {
100+
if code != 0 {
101+
warn!(
102+
"Failed to clone `{}`. Please check your repository configuration.",
103+
url
104+
);
105+
fs::remove_dir_all(root).expect("Failed to remove directory");
106+
107+
bail!("Failed to clone `{}`", url);
108+
}
109+
}
110+
}
111+
112+
for ref_name in refs {
113+
let branch = ref_name.rsplit('/').next().unwrap_or(ref_name);
114+
// get the current branch name without refs/ prefix
115+
let output = Command::new("git")
116+
.current_dir(root)
117+
.arg("symbolic-ref")
118+
.arg("--short")
119+
.arg("HEAD")
120+
.output()
121+
.ok();
122+
123+
let current_branch = output
124+
.filter(|o| o.status.success())
125+
.and_then(|o| String::from_utf8(o.stdout).ok())
126+
.map(|s| s.trim().to_string());
127+
128+
let status = if current_branch.as_deref() == Some(branch) {
129+
Command::new("git")
130+
.current_dir(root)
131+
.arg("pull")
132+
.arg("origin")
133+
.arg(branch)
134+
.status()?
135+
} else {
136+
// Use `git fetch origin +ref:ref` to create or update the local branch from the remote.
137+
// The + ensures that the local branch is updated (forced) even if it's not a fast-forward,
138+
// and it creates the branch if it doesn't exist locally.
139+
Command::new("git")
140+
.current_dir(root)
141+
.arg("fetch")
142+
.arg("origin")
143+
.arg(format!("+{branch}:{branch}"))
144+
.status()?
145+
};
146+
if !status.success() {
147+
return Err(anyhow::anyhow!("Failed to fetch origin {}", branch));
148+
}
149+
}
150+
151+
Ok(())
152+
}
153+
79154
fn rev_to_commit<'a>(
80155
repository: &'a git2::Repository,
81156
rev: Option<&str>,

crates/tabby-index/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ homepage.workspace = true
88
[dependencies]
99
anyhow = { workspace = true }
1010
tabby-common = { path = "../tabby-common" }
11+
tabby-git ={ path = "../tabby-git" }
1112
tantivy = { workspace = true }
1213
tracing = { workspace = true }
1314
tree-sitter-tags = "0.22.6"

crates/tabby-index/src/code/index.rs

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@ use tracing::warn;
1111
use super::{
1212
create_code_builder,
1313
intelligence::{CodeIntelligence, SourceCode},
14-
CodeRepository,
14+
repository, CodeRepository,
15+
};
16+
use crate::{
17+
code::repository::resolve_commits,
18+
indexer::{Indexer, TantivyDocBuilder},
1519
};
16-
use crate::indexer::{Indexer, TantivyDocBuilder};
1720

1821
// Magic numbers
1922
static MAX_LINE_LENGTH_THRESHOLD: usize = 300;
@@ -22,13 +25,30 @@ static MIN_ALPHA_NUM_FRACTION: f32 = 0.25f32;
2225
static MAX_NUMBER_OF_LINES: usize = 100000;
2326
static MAX_NUMBER_FRACTION: f32 = 0.5f32;
2427

25-
pub async fn index_repository(
26-
embedding: Arc<dyn Embedding>,
27-
repository: &CodeRepository,
28-
commit: &str,
29-
) {
30-
let total_files = Walk::new(repository.dir()).count();
31-
let file_stream = stream! {
28+
pub async fn index_repository(embedding: Arc<dyn Embedding>, repository: &CodeRepository) {
29+
let refs = resolve_commits(repository);
30+
// resolve_commits would return the current default branch,
31+
// so it should never be empty here.
32+
if refs.is_empty() {
33+
logkit::error!(
34+
"no branches found for repository {}",
35+
repository.canonical_git_url()
36+
);
37+
return;
38+
}
39+
40+
let mut count_files = 0;
41+
let mut count_chunks = 0;
42+
43+
for (ref_name, sha) in refs {
44+
if let Err(e) = repository::checkout(repository, &ref_name) {
45+
warn!("Failed to checkout ref {}: {}", ref_name, e);
46+
continue;
47+
}
48+
49+
logkit::info!("Indexing branch {} with commit {}", ref_name, &sha);
50+
51+
let file_stream = stream! {
3252
for file in Walk::new(repository.dir()) {
3353
let file = match file {
3454
Ok(file) => file,
@@ -40,21 +60,22 @@ pub async fn index_repository(
4060

4161
yield file;
4262
}
43-
}
44-
// Commit every 100 files
45-
.chunks(100);
63+
}
64+
// Commit every 100 files
65+
.chunks(100);
4666

47-
let mut file_stream = pin!(file_stream);
67+
let mut file_stream = pin!(file_stream);
4868

49-
let mut count_files = 0;
50-
let mut count_chunks = 0;
51-
while let Some(files) = file_stream.next().await {
52-
count_files += files.len();
53-
count_chunks += add_changed_documents(repository, commit, embedding.clone(), files).await;
54-
logkit::info!("Processed {count_files}/{total_files} files, updated {count_chunks} chunks",);
69+
while let Some(files) = file_stream.next().await {
70+
count_files += files.len();
71+
count_chunks += add_changed_documents(repository, &sha, embedding.clone(), files).await;
72+
logkit::info!("Processed {count_files} files, updated {count_chunks} chunks",);
73+
}
5574
}
5675
}
5776

77+
// garbage collection use blob id to check files,
78+
// does NOT have to checkout branch locally.
5879
pub async fn garbage_collection() {
5980
let index = Indexer::new(corpus::CODE);
6081
stream! {

crates/tabby-index/src/code/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ impl CodeIndexer {
3434
embedding: Arc<dyn Embedding>,
3535
repository: &CodeRepository,
3636
) -> anyhow::Result<()> {
37+
repository::sync_repository(repository)?;
38+
3739
logkit::info!(
3840
"Building source code index: {}",
3941
repository.canonical_git_url()
4042
);
41-
let commit = repository::sync_repository(repository)?;
42-
43-
index::index_repository(embedding, repository, &commit).await;
43+
index::index_repository(embedding, repository).await;
4444
index::garbage_collection().await;
4545

4646
Ok(())

0 commit comments

Comments
 (0)