wok_dev/
repo.rs

1use std::{fmt, path};
2
3use anyhow::*;
4use std::result::Result::Ok;
5
6#[derive(Debug, Clone, PartialEq)]
7pub enum MergeResult {
8    UpToDate,
9    FastForward,
10    Merged,
11    Conflicts,
12}
13
14pub struct Repo {
15    pub git_repo: git2::Repository,
16    pub work_dir: path::PathBuf,
17    pub head: String,
18    pub subrepos: Vec<Repo>,
19}
20
21impl Repo {
22    pub fn new(work_dir: &path::Path, head_name: Option<&str>) -> Result<Self> {
23        println!("Reading repo at `{}`", work_dir.display());
24
25        let git_repo = git2::Repository::open(work_dir)
26            .with_context(|| format!("Cannot open repo at `{}`", work_dir.display()))?;
27
28        let head = match head_name {
29            Some(name) => String::from(name),
30            None => {
31                if git_repo.head_detached().with_context(|| {
32                    format!(
33                        "Cannot determine head state for repo at `{}`",
34                        work_dir.display()
35                    )
36                })? {
37                    bail!(
38                        "Cannot operate on a detached head for repo at `{}`",
39                        work_dir.display()
40                    )
41                }
42
43                String::from(git_repo.head().with_context(|| {
44                    format!(
45                        "Cannot find the head branch for repo at `{}`. Is it detached?",
46                        work_dir.display()
47                    )
48                })?.shorthand().with_context(|| {
49                    format!(
50                        "Cannot find a human readable representation of the head ref for repo at `{}`",
51                        work_dir.display(),
52                    )
53                })?)
54            },
55        };
56
57        let subrepos = git_repo
58            .submodules()
59            .with_context(|| {
60                format!(
61                    "Cannot load submodules for repo at `{}`",
62                    work_dir.display()
63                )
64            })?
65            .iter()
66            .map(|submodule| Repo::new(&work_dir.join(submodule.path()), Some(&head)))
67            .collect::<Result<Vec<Repo>>>()?;
68
69        println!("Successfully read repo at `{}`", work_dir.display());
70
71        Ok(Repo {
72            git_repo,
73            work_dir: path::PathBuf::from(work_dir),
74            head,
75            subrepos,
76        })
77    }
78
79    pub fn get_subrepo_by_path(&self, subrepo_path: &path::PathBuf) -> Option<&Repo> {
80        self.subrepos
81            .iter()
82            .find(|subrepo| subrepo.work_dir == self.work_dir.join(subrepo_path))
83    }
84
85    pub fn sync(&self) -> Result<()> {
86        self.switch(&self.head)?;
87        Ok(())
88    }
89
90    pub fn switch(&self, head: &str) -> Result<()> {
91        self.git_repo.set_head(&self.resolve_reference(head)?)?;
92        self.git_repo.checkout_head(None)?;
93        Ok(())
94    }
95
96    pub fn fetch(&self) -> Result<()> {
97        // Get the remote for the current branch
98        let head_ref = self.git_repo.head()?;
99        let branch_name = head_ref.shorthand().with_context(|| {
100            format!(
101                "Cannot get branch name for repo at `{}`",
102                self.work_dir.display()
103            )
104        })?;
105
106        let remote_name = self.get_remote_name_for_branch(branch_name)?;
107
108        // Check if remote exists
109        match self.git_repo.find_remote(&remote_name) {
110            Ok(mut remote) => {
111                let refspecs = &[format!(
112                    "refs/heads/{}:refs/remotes/{}/{}",
113                    branch_name, remote_name, branch_name
114                )];
115
116                remote.fetch(refspecs, None, None).with_context(|| {
117                    format!(
118                        "Failed to fetch from remote '{}' for repo at `{}`",
119                        remote_name,
120                        self.work_dir.display()
121                    )
122                })?;
123            },
124            Err(_) => {
125                // No remote configured, skip fetch
126                return Ok(());
127            },
128        }
129
130        Ok(())
131    }
132
133    pub fn merge(&self, branch_name: &str) -> Result<MergeResult> {
134        // First, fetch the latest changes
135        self.fetch()?;
136
137        // Get the remote branch reference
138        let remote_name = self.get_remote_name_for_branch(branch_name)?;
139        let remote_branch_ref = format!("{}/{}", remote_name, branch_name);
140
141        // Check if remote branch exists
142        let remote_branch_oid = match self.git_repo.refname_to_id(&remote_branch_ref) {
143            Ok(oid) => oid,
144            Err(_) => {
145                // No remote branch, just return up to date
146                return Ok(MergeResult::UpToDate);
147            },
148        };
149
150        let remote_commit = self.git_repo.find_commit(remote_branch_oid)?;
151        let local_commit = self.git_repo.head()?.peel_to_commit()?;
152
153        // Check if we're already up to date
154        if local_commit.id() == remote_commit.id() {
155            return Ok(MergeResult::UpToDate);
156        }
157
158        // Check if we can fast-forward
159        if self
160            .git_repo
161            .graph_descendant_of(local_commit.id(), remote_commit.id())?
162        {
163            // Fast-forward merge
164            self.git_repo
165                .set_head(&format!("refs/heads/{}", branch_name))?;
166            self.git_repo.checkout_head(None)?;
167            return Ok(MergeResult::FastForward);
168        }
169
170        // Perform a merge
171        let mut merge_opts = git2::MergeOptions::new();
172        merge_opts.fail_on_conflict(false); // Don't fail on conflicts, we'll handle them
173
174        let _merge_result = self.git_repo.merge_commits(
175            &local_commit,
176            &remote_commit,
177            Some(&merge_opts),
178        )?;
179
180        // Check if there are conflicts by examining the index
181        let mut index = self.git_repo.index()?;
182        let has_conflicts = index.has_conflicts();
183
184        if !has_conflicts {
185            // No conflicts, merge was successful
186            let signature = self.git_repo.signature()?;
187            let tree_id = index.write_tree()?;
188            let tree = self.git_repo.find_tree(tree_id)?;
189
190            self.git_repo.commit(
191                Some(&format!("refs/heads/{}", branch_name)),
192                &signature,
193                &signature,
194                &format!("Merge remote-tracking branch '{}'", remote_branch_ref),
195                &tree,
196                &[&local_commit, &remote_commit],
197            )?;
198
199            Ok(MergeResult::Merged)
200        } else {
201            // There are conflicts
202            Ok(MergeResult::Conflicts)
203        }
204    }
205
206    pub fn get_remote_name_for_branch(&self, _branch_name: &str) -> Result<String> {
207        // For now, simplify by always using 'origin' as the remote
208        // TODO: Implement proper upstream detection
209        Ok("origin".to_string())
210    }
211
212    fn resolve_reference(&self, short_name: &str) -> Result<String> {
213        Ok(self
214            .git_repo
215            .resolve_reference_from_short_name(short_name)?
216            .name()
217            .with_context(|| {
218                format!(
219                    "Cannot resolve head reference for repo at `{}`",
220                    self.work_dir.display()
221                )
222            })?
223            .to_owned())
224    }
225}
226
227impl fmt::Debug for Repo {
228    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
229        f.debug_struct("Repo")
230            .field("work_dir", &self.work_dir)
231            .field("head", &self.head)
232            .field("subrepos", &self.subrepos)
233            .finish()
234    }
235}