1
use super::{Section, VCS};
2
use crate::error::Error;
3
use crate::utils::get_absolute_file_path;
4
use git2::{Delta, DiffOptions, Repository};
5
use std::path::Path;
6

7
pub struct Git {
8
    target_branch: String,
9
}
10

11
impl Default for Git {
12
    #[must_use]
13 1
    fn default() -> Self {
14
        Self {
15 1
            target_branch: "HEAD".to_string(),
16
        }
17
    }
18
}
19

20
impl Git {
21
    #[must_use]
22 1
    pub fn with_target(target_branch: String) -> Self {
23
        Self { target_branch }
24
    }
25
}
26

27
impl VCS for Git {
28 1
    fn sections<P>(&self, repo_path: P) -> Result<Vec<Section>, Error>
29
    where
30
        P: AsRef<Path>,
31
    {
32 1
        println!("[VCS] - Getting diff with target {}", &self.target_branch);
33 1
        let repo = Repository::discover(repo_path)?;
34 1
        let tree = repo.revparse_single(&self.target_branch)?.peel_to_tree()?;
35 1
        let mut config = DiffOptions::default();
36 1
        config
37
            .context_lines(0)
38
            .show_untracked_content(true)
39
            .recurse_untracked_dirs(true);
40 1
        let diff = repo.diff_tree_to_workdir_with_index(Some(&tree), Some(&mut config))?;
41 1
        let mut sections = Vec::new();
42 1
        diff.foreach(
43 1
            &mut |_delta, _progress| true,
44 1
            None,
45 1
            Some(&mut |delta, hunk| {
46 1
                match delta.status() {
47 1
                    Delta::Modified | Delta::Added | Delta::Untracked => {
48 1
                        if let Some(file_path) = delta.new_file().path() {
49
                            // Path returns the path of the entry relative to the working directory.
50
                            // We can get the absolute path
51 1
                            if let Ok(file_name) = get_absolute_file_path(&file_path) {
52 1
                                if file_name.ends_with(".rs") {
53 1
                                    sections.push(Section {
54 1
                                        file_name,
55 1
                                        line_start: hunk.new_start(),
56 1
                                        line_end: hunk.new_start() + hunk.new_lines(),
57
                                    });
58
                                }
59
                            }
60
                        }
61
                    }
62 0
                    _ => {}
63
                }
64 0
                true
65
            }),
66 1
            None,
67
        )?;
68 1
        Ok(sections)
69
    }
70
}
71

72
#[cfg(test)]
73
mod tests {
74
    use super::{get_absolute_file_path, Error, Git, Path, Repository, Section, VCS};
75
    use std::fs::{self, File};
76
    use std::io::Write;
77
    use tempfile::TempDir;
78

79
    type Result<T> = std::result::Result<T, Error>;
80

81
    #[test]
82 1
    fn no_changes() -> Result<()> {
83 1
        let repo = RepoFixture::new()?;
84 1
        let git = Git::default();
85 1
        let sections = git.sections(repo.path())?;
86 1
        assert!(sections.is_empty());
87 1
        Ok(())
88
    }
89

90
    #[test]
91 1
    fn added_files() -> Result<()> {
92 1
        let repo = RepoFixture::new()?
93 1
            .write("foo.rs", "test_files/git/added/foo.rs")?
94 1
            .write("bar.rs", "test_files/git/added/bar.rs")?
95 1
            .stage(&["foo.rs", "bar.rs"])?;
96

97 1
        let expected = vec![
98 1
            Section {
99 1
                file_name: get_absolute_file_path(&"bar.rs")?,
100
                line_start: 1,
101
                line_end: 5,
102
            },
103 1
            Section {
104 1
                file_name: get_absolute_file_path(&"foo.rs")?,
105
                line_start: 1,
106
                line_end: 7,
107
            },
108
        ];
109

110 1
        let git = Git::default();
111 1
        let actual = git.sections(repo.path())?;
112 1
        assert_eq!(expected, actual);
113 1
        Ok(())
114
    }
115

116
    #[test]
117 1
    fn untracked_files() -> Result<()> {
118 1
        let repo = RepoFixture::new()?
119 1
            .write("foo.rs", "test_files/git/added/foo.rs")?
120 1
            .write("inside/some/dir/bar.rs", "test_files/git/added/bar.rs")?;
121

122 1
        let expected = vec![
123 1
            Section {
124 1
                file_name: get_absolute_file_path(&"foo.rs")?,
125
                line_start: 1,
126
                line_end: 7,
127
            },
128 1
            Section {
129 1
                file_name: get_absolute_file_path(&"inside/some/dir/bar.rs")?,
130
                line_start: 1,
131
                line_end: 5,
132
            },
133
        ];
134

135 1
        let git = Git::default();
136 1
        let actual = git.sections(repo.path())?;
137 1
        assert_eq!(expected, actual);
138 1
        Ok(())
139
    }
140

141
    #[test]
142 1
    fn modified_files() -> Result<()> {
143 1
        let files = &["foo.rs", "bar.rs"];
144 1
        let repo = RepoFixture::new()?
145 1
            .write("foo.rs", "test_files/git/modified/old/foo.rs")?
146 1
            .write("bar.rs", "test_files/git/modified/old/bar.rs")?
147 1
            .stage(files)?
148 1
            .commit("master", files)?
149 1
            .write("foo.rs", "test_files/git/modified/new/foo.rs")?
150 1
            .write("bar.rs", "test_files/git/modified/new/bar.rs")?;
151

152 1
        let expected = vec![
153 1
            Section {
154 1
                file_name: get_absolute_file_path(&"bar.rs")?,
155
                line_start: 1,
156
                line_end: 2,
157
            },
158 1
            Section {
159 1
                file_name: get_absolute_file_path(&"bar.rs")?,
160
                line_start: 5,
161
                line_end: 9,
162
            },
163 1
            Section {
164 1
                file_name: get_absolute_file_path(&"foo.rs")?,
165
                line_start: 3,
166
                line_end: 4,
167
            },
168 1
            Section {
169 1
                file_name: get_absolute_file_path(&"foo.rs")?,
170
                line_start: 6,
171
                line_end: 7,
172
            },
173
        ];
174

175 1
        let git = Git::default();
176 1
        let actual = git.sections(repo.path())?;
177 1
        assert_eq!(expected, actual);
178 1
        Ok(())
179
    }
180

181
    #[test]
182 1
    fn mixed_extensions() -> Result<()> {
183 1
        let repo = RepoFixture::new()?
184 1
            .write("foo.rs", "test_files/git/mixed/foo.rs")?
185 1
            .write("bar.txt", "test_files/git/mixed/bar.txt")?
186 1
            .stage(&["foo.rs", "bar.txt"])?;
187

188 1
        let expected = vec![Section {
189 1
            file_name: get_absolute_file_path(&"foo.rs")?,
190
            line_start: 1,
191
            line_end: 7,
192
        }];
193

194 1
        let git = Git::default();
195 1
        let actual = git.sections(repo.path())?;
196 1
        assert_eq!(expected, actual);
197 1
        Ok(())
198
    }
199

200
    #[test]
201 1
    fn other_branch() -> Result<()> {
202 1
        let repo = RepoFixture::new()?
203 1
            .branch("other")?
204 1
            .write("foo.rs", "test_files/git/modified/old/foo.rs")?
205 1
            .stage(&["foo.rs"])?
206 1
            .commit("other", &["foo.rs"])?
207 1
            .write("foo.rs", "test_files/git/modified/new/foo.rs")?;
208

209 1
        let expected = vec![
210 1
            Section {
211 1
                file_name: get_absolute_file_path(&"foo.rs")?,
212
                line_start: 3,
213
                line_end: 4,
214
            },
215 1
            Section {
216 1
                file_name: get_absolute_file_path(&"foo.rs")?,
217
                line_start: 6,
218
                line_end: 7,
219
            },
220
        ];
221

222 1
        let git = Git::with_target("other".to_string());
223 1
        let actual = git.sections(repo.path())?;
224 1
        assert_eq!(expected, actual);
225 1
        Ok(())
226
    }
227

228
    struct RepoFixture {
229
        dir: TempDir,
230
        repo: Repository,
231
    }
232

233
    impl RepoFixture {
234 1
        pub fn new() -> Result<Self> {
235 1
            let dir = TempDir::new()?;
236 1
            let repo = Repository::init(dir.path())?;
237
            {
238
                // Set mandatory configuration
239 1
                let mut config = repo.config()?;
240 1
                config.set_str("user.name", "name")?;
241 1
                config.set_str("user.email", "email")?;
242

243
                // Write initial commit
244 1
                let id = repo.index()?.write_tree()?;
245 1
                let tree = repo.find_tree(id)?;
246 1
                let sig = repo.signature()?;
247 1
                repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[])?;
248
            }
249 1
            Ok(Self { dir, repo })
250
        }
251

252 1
        pub fn write<P: AsRef<Path>>(self, path: P, test_file: &str) -> Result<Self> {
253 1
            let parent_dir = path.as_ref().parent().unwrap();
254 1
            fs::create_dir_all(self.dir.path().join(parent_dir))?;
255

256 1
            let contents = fs::read_to_string(test_file)?;
257 1
            let mut file = File::create(self.dir.path().join(path))?;
258 1
            file.write_all(contents.as_bytes())?;
259 1
            Ok(self)
260
        }
261

262 1
        pub fn stage(self, paths: &[&str]) -> Result<Self> {
263 1
            let mut index = self.repo.index()?;
264 1
            for path in paths {
265 1
                index.add_path(path.as_ref())?;
266
            }
267 1
            index.write()?;
268 1
            Ok(self)
269
        }
270

271 1
        pub fn commit(self, branch: &str, paths: &[&str]) -> Result<Self> {
272
            {
273 1
                let mut index = self.repo.index()?;
274 1
                for path in paths {
275 1
                    index.add_path(path.as_ref())?;
276
                }
277

278 1
                let id = index.write_tree()?;
279 1
                let tree = self.repo.find_tree(id)?;
280 1
                let sig = self.repo.signature()?;
281

282 1
                let target = self.repo.head()?.target().unwrap();
283 1
                let parent = self.repo.find_commit(target)?;
284

285 1
                let name = format!("refs/heads/{}", branch);
286 1
                self.repo
287 1
                    .commit(Some(&name), &sig, &sig, "some commit", &tree, &[&parent])?;
288
            }
289 1
            Ok(self)
290
        }
291

292 1
        pub fn branch(self, name: &str) -> Result<Self> {
293
            {
294 1
                let target = self.repo.head()?.target().unwrap();
295 1
                let parent = self.repo.find_commit(target)?;
296

297 1
                self.repo.branch(name, &parent, false)?;
298
            }
299 1
            Ok(self)
300
        }
301

302 1
        pub fn path(&self) -> &Path {
303 1
            self.dir.path()
304
        }
305
    }
306
}

Read our documentation on viewing source code .

Loading