• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kitplummer / goa / 20872009936

10 Jan 2026 03:23AM UTC coverage: 66.038%. First build
20872009936

Pull #120

github

web-flow
Merge 840e48085 into d56589fd8
Pull Request #120: fix: stabilization phase 1 - bugs and dependency updates

9 of 14 new or added lines in 4 files covered. (64.29%)

350 of 530 relevant lines covered (66.04%)

3.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

42.65
/src/git.rs
1
/*
2
 * libgit2 "pull" example - shows how to pull remote data into a local branch.
3
 *
4
 * Written by the libgit2 contributors
5
 *
6
 * To the extent possible under law, the author(s) have dedicated all copyright
7
 * and related and neighboring rights to this software to the public domain
8
 * worldwide. This software is distributed without any warranty.
9
 *
10
 * You should have received a copy of the CC0 Public Domain Dedication along
11
 * with this software. If not, see
12
 * <http://creativecommons.org/publicdomain/zero/1.0/>.
13
 */
14

15
use chrono::{DateTime, Utc};
16
use git2::{
17
    AutotagOption, Commit, Diff, DiffStatsFormat, FetchOptions, Object, ObjectType,
18
    RemoteCallbacks, RemoteUpdateFlags, Repository,
19
};
20
use std::env;
21
use std::io::Write;
22
use std::str;
23

24
pub fn is_diff<'a>(
2✔
25
    repo: &'a git2::Repository,
2✔
26
    remote_name: &str,
2✔
27
    branch_name: &str,
2✔
28
    verbosity: u8,
2✔
29
) -> Result<git2::AnnotatedCommit<'a>, git2::Error> {
2✔
30
    let mut cb = RemoteCallbacks::new();
2✔
31
    let mut remote = repo
2✔
32
        .find_remote(remote_name)
2✔
33
        .or_else(|_| repo.remote_anonymous(remote_name))
2✔
34
        .unwrap();
2✔
35
    cb.sideband_progress(|data| {
2✔
NEW
36
        if verbosity >= 2 {
×
37
            let dt = Utc::now();
×
38
            print!(
×
39
                "goa [{}]: remote: {}",
×
40
                dt,
×
41
                std::str::from_utf8(data).unwrap()
×
42
            );
×
43
        }
×
44
        std::io::stdout().flush().unwrap();
×
45
        true
×
46
    });
×
47

48
    let mut fo = FetchOptions::new();
2✔
49
    fo.remote_callbacks(cb);
2✔
50
    remote.download(&[] as &[&str], Some(&mut fo)).unwrap();
2✔
51

52
    // Disconnect the underlying connection to prevent from idling.
53
    remote.disconnect().unwrap();
2✔
54

55
    // Update the references in the remote's namespace to point to the right
56
    // commits. This may be needed even if there was no packfile to download,
57
    // which can happen e.g. when the branches have been changed but all the
58
    // needed objects are available locally.
59
    remote
2✔
60
        .update_tips(None, RemoteUpdateFlags::UPDATE_FETCHHEAD, AutotagOption::Unspecified, None)
2✔
61
        .unwrap();
2✔
62

63
    let l = String::from(branch_name);
2✔
64
    let r = format!("{}/{}", remote_name, branch_name);
2✔
65
    let tl = tree_to_treeish(repo, Some(&l)).unwrap();
2✔
66
    let tr = tree_to_treeish(repo, Some(&r)).unwrap();
2✔
67

68
    let head = repo.head().unwrap();
2✔
69
    let oid = head.target().unwrap();
2✔
70
    let commit = repo.find_commit(oid).unwrap();
2✔
71

72
    let _branch = repo.branch(branch_name, &commit, false);
2✔
73

74
    let obj = repo
2✔
75
        .revparse_single(&("refs/heads/".to_owned() + branch_name))
2✔
76
        .unwrap();
2✔
77

78
    repo.checkout_tree(&obj, None)?;
2✔
79

80
    repo.set_head(&("refs/heads/".to_owned() + branch_name))?;
2✔
81

82
    let diff = match (tl, tr) {
2✔
83
        (Some(local), Some(origin)) => repo
2✔
84
            .diff_tree_to_tree(local.as_tree(), origin.as_tree(), None)
2✔
85
            .unwrap(),
2✔
86
        (_, _) => unreachable!(),
×
87
    };
88

89
    if diff.deltas().len() > 0 {
2✔
90
        // TODO: make this a verbose thing
NEW
91
        if verbosity >= 2 {
×
92
            display_stats(&diff).expect("ERROR: unable to print diff stats");
×
93
        }
×
94
        let fetch_head = repo.find_reference("FETCH_HEAD")?;
×
95
        repo.reference_to_annotated_commit(&fetch_head)
×
96
    } else {
97
        let msg = "no diffs, back to sleep.";
2✔
98
        Err(git2::Error::from_str(msg))
2✔
99
    }
100
}
2✔
101

102
pub fn set_last_commit(repo: &git2::Repository, branch_name: &str, verbosity: u8) {
6✔
103
    let commit = find_last_commit_on_branch(repo, branch_name);
6✔
104
    commit_to_envs(&commit.unwrap(), verbosity);
6✔
105
}
6✔
106

107
pub fn tree_to_treeish<'a>(
4✔
108
    repo: &'a Repository,
4✔
109
    arg: Option<&String>,
4✔
110
) -> Result<Option<Object<'a>>, git2::Error> {
4✔
111
    let arg = match arg {
4✔
112
        Some(s) => s,
4✔
113
        None => return Ok(None),
×
114
    };
115
    let obj = match repo.revparse_single(arg) {
4✔
116
        Ok(obj) => obj,
4✔
117
        Err(_) => {
118
            eprintln!("Error: branch not found");
×
119
            std::process::exit(1);
×
120
        }
121
    };
122
    let tree = obj.peel(ObjectType::Tree).unwrap();
4✔
123
    Ok(Some(tree))
4✔
124
}
4✔
125

126
fn display_stats(diff: &Diff) -> Result<(), git2::Error> {
×
127
    let stats = diff.stats().unwrap();
×
128
    let format = DiffStatsFormat::FULL;
×
129
    let buf = stats.to_buf(format, 80).unwrap();
×
130
    let dt = Utc::now();
×
NEW
131
    print!("goa [{}]: {}", dt, std::str::from_utf8(&buf).unwrap());
×
132
    Ok(())
×
133
}
×
134

135
fn find_last_commit_on_branch<'a>(
6✔
136
    repo: &'a Repository,
6✔
137
    branch_name: &str,
6✔
138
) -> Result<Commit<'a>, git2::Error> {
6✔
139
    let (object, reference) = repo.revparse_ext(branch_name).expect("Object not found");
6✔
140

141
    repo.checkout_tree(&object, None)
6✔
142
        .expect("Failed to checkout");
6✔
143

144
    match reference {
6✔
145
        // gref is an actual reference like branches or tags
146
        Some(gref) => repo.set_head(gref.name().unwrap()),
4✔
147
        // this is a commit, not a reference
148
        None => repo.set_head_detached(object.id()),
2✔
149
    }
150
    .expect("Failed to set HEAD");
6✔
151

152
    let obj = repo.head()?.resolve()?.peel(ObjectType::Commit)?;
6✔
153
    obj.into_commit()
6✔
154
        .map_err(|_| git2::Error::from_str("Couldn't find commit"))
6✔
155
}
6✔
156

NEW
157
fn find_last_commit(repo: &Repository) -> Result<Commit<'_>, git2::Error> {
×
158
    let obj = repo.head()?.resolve()?.peel(ObjectType::Commit)?;
×
159
    obj.into_commit()
×
160
        .map_err(|_| git2::Error::from_str("Couldn't find commit"))
×
161
}
×
162

163
fn commit_to_envs(commit: &Commit, verbosity: u8) {
4✔
164
    let timestamp = commit.time().seconds();
4✔
165
    let tm = DateTime::from_timestamp(timestamp, 0).unwrap_or_else(Utc::now);
4✔
166
    if verbosity > 0 {
4✔
167
        let dt = Utc::now();
4✔
168
        println!(
4✔
169
            "goa [{}]: commit {}\nAuthor: {}\nDate:   {}\n\n    {}",
4✔
170
            dt,
4✔
171
            commit.id(),
4✔
172
            commit.author(),
4✔
173
            tm,
4✔
174
            commit.message().unwrap_or("no commit message")
4✔
175
        );
4✔
176
    }
4✔
177
    env::set_var("GOA_LAST_COMMIT_ID", commit.id().to_string());
4✔
178
    env::set_var("GOA_LAST_COMMIT_AUTHOR", commit.author().to_string());
4✔
179
    env::set_var(
4✔
180
        "GOA_LAST_COMMIT_MESSAGE",
181
        commit.message().unwrap_or(""),
4✔
182
    );
183
    env::set_var("GOA_LAST_COMMIT_TIME", tm.to_string());
4✔
184
}
4✔
185

186
fn fast_forward(
×
187
    repo: &Repository,
×
188
    lb: &mut git2::Reference,
×
189
    rc: &git2::AnnotatedCommit,
×
190
) -> Result<(), git2::Error> {
×
191
    let name = match lb.name() {
×
192
        Some(s) => s.to_string(),
×
193
        None => String::from_utf8_lossy(lb.name_bytes()).to_string(),
×
194
    };
195
    let msg = format!("Fast-Forward: Setting {} to id: {}", name, rc.id());
×
196
    lb.set_target(rc.id(), &msg)?;
×
197
    repo.set_head(&name)?;
×
198
    repo.checkout_head(Some(
×
199
        git2::build::CheckoutBuilder::default()
×
200
            // For some reason the force is required to make the working directory actually get updated
×
201
            // I suspect we should be adding some logic to handle dirty working directory states
×
202
            // but this is just an example so maybe not.
×
203
            .force(),
×
204
    ))?;
×
205
    Ok(())
×
206
}
×
207

208
fn normal_merge(
×
209
    repo: &Repository,
×
210
    local: &git2::AnnotatedCommit,
×
211
    remote: &git2::AnnotatedCommit,
×
212
) -> Result<(), git2::Error> {
×
213
    let local_tree = repo.find_commit(local.id())?.tree()?;
×
214
    let remote_tree = repo.find_commit(remote.id())?.tree()?;
×
215
    let ancestor = repo
×
216
        .find_commit(repo.merge_base(local.id(), remote.id())?)?
×
217
        .tree()?;
×
218
    let mut idx = repo.merge_trees(&ancestor, &local_tree, &remote_tree, None)?;
×
219

220
    if idx.has_conflicts() {
×
221
        eprintln!("Error: Merge conficts detected...");
×
222
        repo.checkout_index(Some(&mut idx), None)?;
×
223
        return Ok(());
×
224
    }
×
225
    let result_tree = repo.find_tree(idx.write_tree_to(repo)?)?;
×
226
    // now create the merge commit
227
    let msg = format!("Merge: {} into {}", remote.id(), local.id());
×
228
    let sig = repo.signature()?;
×
229
    let local_commit = repo.find_commit(local.id())?;
×
230
    let remote_commit = repo.find_commit(remote.id())?;
×
231
    // Do our merge commit and set current branch head to that commit.
232
    let _merge_commit = repo.commit(
×
233
        Some("HEAD"),
×
234
        &sig,
×
235
        &sig,
×
236
        &msg,
×
237
        &result_tree,
×
238
        &[&local_commit, &remote_commit],
×
239
    )?;
×
240
    // Set working tree to match head.
241
    repo.checkout_head(None)?;
×
242
    Ok(())
×
243
}
×
244

245
pub fn do_merge<'a>(
×
246
    repo: &'a Repository,
×
247
    remote_branch: &str,
×
248
    fetch_commit: git2::AnnotatedCommit<'a>,
×
249
    verbosity: u8,
×
250
) -> Result<(), git2::Error> {
×
251
    // 1. do a merge analysis
252
    let analysis = repo.merge_analysis(&[&fetch_commit])?;
×
253

254
    // 2. Do the appopriate merge
255
    if analysis.0.is_fast_forward() {
×
256
        // do a fast forward
257
        let refname = format!("refs/heads/{}", remote_branch);
×
258
        match repo.find_reference(&refname) {
×
259
            Ok(mut r) => {
×
260
                fast_forward(repo, &mut r, &fetch_commit)?;
×
261
            }
262
            Err(_) => {
263
                // The branch doesn't exist so just set the reference to the
264
                // commit directly. Usually this is because you are pulling
265
                // into an empty repository.
266
                repo.reference(
×
267
                    &refname,
×
268
                    fetch_commit.id(),
×
269
                    true,
270
                    &format!("Setting {} to {}", remote_branch, fetch_commit.id()),
×
271
                )?;
×
272
                repo.set_head(&refname)?;
×
273
                repo.checkout_head(Some(
×
274
                    git2::build::CheckoutBuilder::default()
×
275
                        .allow_conflicts(true)
×
276
                        .conflict_style_merge(true)
×
277
                        .force(),
×
278
                ))?;
×
279
            }
280
        };
281
        let commit = find_last_commit(repo).expect("Couldn't find last commit");
×
282
        commit_to_envs(&commit, verbosity);
×
283
    } else if analysis.0.is_normal() {
×
284
        // do a normal merge
285
        let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?;
×
286
        normal_merge(repo, &head_commit, &fetch_commit)?;
×
287
        let commit = find_last_commit(repo).expect("Couldn't find last commit");
×
288
        commit_to_envs(&commit, verbosity);
×
289
    } else {
×
290
        eprintln!("Error: Nothing to do?");
×
291
    }
×
292
    Ok(())
×
293
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc