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

kitplummer / goa / 20872343545

10 Jan 2026 03:49AM UTC coverage: 67.029%. First build
20872343545

Pull #121

github

web-flow
Merge a238a7b4a into bc15a6a4f
Pull Request #121: fix: replace unwrap/process::exit with proper error handling

100 of 132 new or added lines in 3 files covered. (75.76%)

370 of 552 relevant lines covered (67.03%)

3.18 hits per line

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

42.92
/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

35
    cb.sideband_progress(|data| {
2✔
36
        if verbosity >= 2 {
×
37
            let dt = Utc::now();
×
NEW
38
            if let Ok(msg) = std::str::from_utf8(data) {
×
NEW
39
                print!("goa [{}]: remote: {}", dt, msg);
×
NEW
40
            }
×
41
        }
×
NEW
42
        let _ = std::io::stdout().flush();
×
43
        true
×
44
    });
×
45

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

50
    // Disconnect the underlying connection to prevent from idling.
51
    remote.disconnect()?;
2✔
52

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

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

69
    let head = repo.head()?;
2✔
70
    let oid = head
2✔
71
        .target()
2✔
72
        .ok_or_else(|| git2::Error::from_str("HEAD has no target"))?;
2✔
73
    let commit = repo.find_commit(oid)?;
2✔
74

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

77
    let obj = repo.revparse_single(&("refs/heads/".to_owned() + branch_name))?;
2✔
78

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

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

83
    let diff = match (tl, tr) {
2✔
84
        (Some(local), Some(origin)) => {
2✔
85
            repo.diff_tree_to_tree(local.as_tree(), origin.as_tree(), None)?
2✔
86
        }
NEW
87
        (_, _) => return Err(git2::Error::from_str("Could not resolve local or remote tree")),
×
88
    };
89

90
    if diff.deltas().len() > 0 {
2✔
91
        if verbosity >= 2 {
×
NEW
92
            if let Err(e) = display_stats(&diff) {
×
NEW
93
                eprintln!("Warning: unable to print diff stats: {}", e);
×
NEW
94
            }
×
95
        }
×
96
        let fetch_head = repo.find_reference("FETCH_HEAD")?;
×
97
        repo.reference_to_annotated_commit(&fetch_head)
×
98
    } else {
99
        let msg = "no diffs, back to sleep.";
2✔
100
        Err(git2::Error::from_str(msg))
2✔
101
    }
102
}
2✔
103

104
pub fn set_last_commit(
6✔
105
    repo: &git2::Repository,
6✔
106
    branch_name: &str,
6✔
107
    verbosity: u8,
6✔
108
) -> Result<(), git2::Error> {
6✔
109
    let commit = find_last_commit_on_branch(repo, branch_name)?;
6✔
110
    commit_to_envs(&commit, verbosity);
4✔
111
    Ok(())
4✔
112
}
6✔
113

114
pub fn tree_to_treeish<'a>(
4✔
115
    repo: &'a Repository,
4✔
116
    arg: Option<&String>,
4✔
117
) -> Result<Option<Object<'a>>, git2::Error> {
4✔
118
    let arg = match arg {
4✔
119
        Some(s) => s,
4✔
120
        None => return Ok(None),
×
121
    };
122
    let obj = repo.revparse_single(arg).map_err(|e| {
4✔
NEW
123
        git2::Error::from_str(&format!("branch '{}' not found: {}", arg, e))
×
NEW
124
    })?;
×
125
    let tree = obj.peel(ObjectType::Tree)?;
4✔
126
    Ok(Some(tree))
4✔
127
}
4✔
128

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

140
fn find_last_commit_on_branch<'a>(
6✔
141
    repo: &'a Repository,
6✔
142
    branch_name: &str,
6✔
143
) -> Result<Commit<'a>, git2::Error> {
6✔
144
    let (object, reference) = repo.revparse_ext(branch_name)?;
6✔
145

146
    repo.checkout_tree(&object, None)?;
4✔
147

148
    match reference {
4✔
149
        // gref is an actual reference like branches or tags
150
        Some(gref) => {
4✔
151
            let name = gref
4✔
152
                .name()
4✔
153
                .ok_or_else(|| git2::Error::from_str("Reference has no name"))?;
4✔
154
            repo.set_head(name)?;
4✔
155
        }
156
        // this is a commit, not a reference
NEW
157
        None => repo.set_head_detached(object.id())?,
×
158
    }
159

160
    let obj = repo.head()?.resolve()?.peel(ObjectType::Commit)?;
4✔
161
    obj.into_commit()
4✔
162
        .map_err(|_| git2::Error::from_str("Couldn't find commit"))
4✔
163
}
6✔
164

165
fn find_last_commit(repo: &Repository) -> Result<Commit<'_>, git2::Error> {
×
166
    let obj = repo.head()?.resolve()?.peel(ObjectType::Commit)?;
×
167
    obj.into_commit()
×
168
        .map_err(|_| git2::Error::from_str("Couldn't find commit"))
×
169
}
×
170

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

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

216
fn normal_merge(
×
217
    repo: &Repository,
×
218
    local: &git2::AnnotatedCommit,
×
219
    remote: &git2::AnnotatedCommit,
×
220
) -> Result<(), git2::Error> {
×
221
    let local_tree = repo.find_commit(local.id())?.tree()?;
×
222
    let remote_tree = repo.find_commit(remote.id())?.tree()?;
×
223
    let ancestor = repo
×
224
        .find_commit(repo.merge_base(local.id(), remote.id())?)?
×
225
        .tree()?;
×
226
    let mut idx = repo.merge_trees(&ancestor, &local_tree, &remote_tree, None)?;
×
227

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

253
pub fn do_merge<'a>(
×
254
    repo: &'a Repository,
×
255
    remote_branch: &str,
×
256
    fetch_commit: git2::AnnotatedCommit<'a>,
×
257
    verbosity: u8,
×
258
) -> Result<(), git2::Error> {
×
259
    // 1. do a merge analysis
260
    let analysis = repo.merge_analysis(&[&fetch_commit])?;
×
261

262
    // 2. Do the appopriate merge
263
    if analysis.0.is_fast_forward() {
×
264
        // do a fast forward
265
        let refname = format!("refs/heads/{}", remote_branch);
×
266
        match repo.find_reference(&refname) {
×
267
            Ok(mut r) => {
×
268
                fast_forward(repo, &mut r, &fetch_commit)?;
×
269
            }
270
            Err(_) => {
271
                // The branch doesn't exist so just set the reference to the
272
                // commit directly. Usually this is because you are pulling
273
                // into an empty repository.
274
                repo.reference(
×
275
                    &refname,
×
276
                    fetch_commit.id(),
×
277
                    true,
278
                    &format!("Setting {} to {}", remote_branch, fetch_commit.id()),
×
279
                )?;
×
280
                repo.set_head(&refname)?;
×
281
                repo.checkout_head(Some(
×
282
                    git2::build::CheckoutBuilder::default()
×
283
                        .allow_conflicts(true)
×
284
                        .conflict_style_merge(true)
×
285
                        .force(),
×
286
                ))?;
×
287
            }
288
        };
NEW
289
        let commit = find_last_commit(repo)?;
×
290
        commit_to_envs(&commit, verbosity);
×
291
    } else if analysis.0.is_normal() {
×
292
        // do a normal merge
293
        let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?;
×
294
        normal_merge(repo, &head_commit, &fetch_commit)?;
×
NEW
295
        let commit = find_last_commit(repo)?;
×
296
        commit_to_envs(&commit, verbosity);
×
297
    } else {
×
298
        eprintln!("Error: Nothing to do?");
×
299
    }
×
300
    Ok(())
×
301
}
×
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