<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>bot &amp;mdash; musicmatzes blog</title>
    <link>https://beyermatthias.de/tag:bot</link>
    <description></description>
    <pubDate>Tue, 28 Jul 2026 03:02:14 +0200</pubDate>
    <item>
      <title>Writing a mastodon bot</title>
      <link>https://beyermatthias.de/writing-a-mastodon-bot</link>
      <description>&lt;![CDATA[Today, I wrote a mastodon bot.&#xA;&#xA;  Shut up and show me the code!&#xA;&#xA;Here you go.&#xA;&#xA;The idea&#xA;&#xA;My idea was, to write a bot that fetches the lastest master from a git&#xA;repository and counts some commits and then posts a message to mastodon about&#xA;what it counted.&#xA;&#xA;Because I always complain about people pushing to the master branch of a big&#xA;community git repository directly, I decided that this would be a perfect fit.&#xA;&#xA;(Whether pushing to master directly is okay and when it is not okay to do this&#xA;is another topic and I won&#39;t discuss this here)&#xA;&#xA;The dependencies&#xA;&#xA;Well, because I didn&#39;t want to implement everything myself, I started pulling&#xA;in some dependencies:&#xA;&#xA;log and envlogger for logging&#xA;structopt, toml, serde and config for argument parsing and config reading&#xA;anyhow because I don&#39;t care too much about error handling. It just has to&#xA;  work&#xA;getset for a bit cleaner code (not strictly necessary, tbh)&#xA;handlebars for templating the status message that will be posted&#xA;elefren as mastodon API crate&#xA;git2 for working with the git repository which the bot posts about&#xA;&#xA;The Plan&#xA;&#xA;How the bot should work was rather clear from the outset. First of all, it&#xA;shouldn&#39;t be a always-running-process. I wanted it to be as simple as possible,&#xA;thus, triggering it via a systemd-timer should suffice.&#xA;Next, it should only fetch the latest commits, so it should be able to work on&#xA;a working clone of a repository. This way, we don&#39;t need another clone of a&#xA;potentially huge repository on our disk.&#xA;The path of the repository should of course not be hardcoded, as shouldn&#39;t the&#xA;&#34;upstream&#34; remote name or the &#34;master&#34; branch name (because you might want to track&#xA;a &#34;release-xyz&#34; branch or because &#34;master&#34; was renamed to something else).&#xA;&#xA;Also, it should be configurable how many hours of commits should be checked.&#xA;Maybe the user wants to run this bot once a day, maybe once a week. Both is possible,&#xA;of course.&#xA;But if the user runs it once a day, they want to check only the commits of the&#xA;last 24 hours.&#xA;If they run it once a week, the last 168 hours would be more appropriate.&#xA;&#xA;The message that gets posted should also not be hardcoded, but a template where&#xA;the variables the bot counted are available.&#xA;&#xA;All the above goes into the configuration file the bot ready (and which can be&#xA;set via the --config option on the bots CLI).&#xA;&#xA;The configuration struct&#xA;for the setup described above&#xA;is rather trivial, as is&#xA;the CLI setup.&#xA;&#xA;The setup&#xA;&#xA;The first things the bot has to do is reading the commandline and the&#xA;configuration after initializing the logger, which is a no-brainer, too:&#xA;&#xA;fn main() -  Result() {&#xA;    envlogger::init();&#xA;    log::debug!(&#34;Logger initialized&#34;);&#xA;&#xA;    let opts = Opts::fromargssafe()?;&#xA;    let config: Conf = {&#xA;        let mut config = ::config::Config::default();&#xA;&#xA;        config&#xA;            .merge(::config::File::from(opts.config().topathbuf()))?&#xA;            .merge(::config::Environment::withprefix(&#34;COMBOT&#34;))?;&#xA;        config.tryinto()?&#xA;    };&#xA;    let mastodondata: elefren::Data = toml::de::fromstr(&amp;std::fs::readtostring(config.mastodondata())?)?;&#xA;&#xA;The mastodon data is read from a configuration file that is different from the&#xA;main configuration file, because it may contain sensitive data and if a user&#xA;wants to put their configuration of the bot into a (public?) git repository,&#xA;they might not want to include this data.&#xA;That&#39;s why I opted for another file here, its format is described in the&#xA;configuration example file (next to the setting where the file actually is).&#xA;&#xA;Next, the mastodon client has to be setup and the repository has to be opened:&#xA;&#xA;    let client = elefren::Mastodon::from(mastodondata);&#xA;    let statuslanguage = elefren::Language::from6391(config.statuslanguage())&#xA;        .okorelse(|| anyhow!(&#34;Could not parse status language code: {}&#34;, config.statuslanguage()))?;&#xA;    log::debug!(&#34;config parsed&#34;);&#xA;&#xA;    let repo = git2::Repository::open(config.repositorypath())?;&#xA;    log::debug!(&#34;Repo opened successfully&#34;);&#xA;&#xA;which is rather trivial, too.&#xA;&#xA;The Calculations&#xA;&#xA;Then, we fetch the appropriate remote branch and count the commits:&#xA;&#xA;    let  = fetchmainremote(&amp;repo, &amp;config)?;&#xA;    log::debug!(&#34;Main branch fetched successfully&#34;);&#xA;&#xA;    let (commits, merges, nonmerges) = countcommitsonmainbranch(&amp;repo, &amp;config)?;&#xA;    log::debug!(&#34;Counted commits successfully&#34;);&#xA;&#xA;    log::info!(&#34;Commits    = {}&#34;, commits);&#xA;    log::info!(&#34;Merges     = {}&#34;, merges);&#xA;    log::info!(&#34;Non-Merges = {}&#34;, nonmerges);&#xA;&#xA;The functions called in this snippet will be described later on.&#xA;Just consider them working for now, and let&#39;s move on to the status posting part&#xA;of the bot now.&#xA;&#xA;First of all, we use the variables to compute the status message using the&#xA;template from the configuration file.&#xA;&#xA;    {&#xA;        let statustext = {&#xA;            let mut hb = handlebars::Handlebars::new();&#xA;            hb.registertemplatestring(&#34;status&#34;, config.statustemplate())?;&#xA;            let mut data = std::collections::BTreeMap::new();&#xA;            data.insert(&#34;commits&#34;, commits);&#xA;            data.insert(&#34;merges&#34;, merges);&#xA;            data.insert(&#34;nonmerges&#34;, nonmerges);&#xA;            hb.render(&#34;status&#34;, &amp;data)?&#xA;        };&#xA;&#xA;Handlebars is a perfect fit for that job, as it is rather trivial to use, albeit&#xA;a very powerful templating language is used.&#xA;The user could, for example, even add some conditions to their template, like if&#xA;there are no commits at all, the status message could just say&#xA;&#34;I&#39;m a lonely bot, because nobody commits to master these days...&#34; or something&#xA;like that.&#xA;&#xA;Next, we build the status object we pass to mastodon, and post it.&#xA;&#xA;        let status = elefren::StatusBuilder::new()&#xA;            .status(statustext)&#xA;            .language(statuslanguage)&#xA;            .build()&#xA;            .expect(&#34;Failed to build status&#34;);&#xA;&#xA;        let status = client.newstatus(status)&#xA;            .expect(&#34;Failed to post status&#34;);&#xA;        if let Some(url) = status.url.asref() {&#xA;            log::info!(&#34;Status posted: {}&#34;, url);&#xA;        } else {&#xA;            log::info!(&#34;Status posted, no url&#34;);&#xA;        }&#xA;        log::debug!(&#34;New status = {:?}&#34;, status);&#xA;    }&#xA;&#xA;    Ok(())&#xA;} // main()&#xA;&#xA;Some logging is added as well, of course.&#xA;&#xA;And that&#39;s the whole main function!&#xA;&#xA;Fetching the repository.&#xA;&#xA;But we are not done yet.&#xA;First of all, we need the function that fetches the remote repository.&#xA;&#xA;Because of the infamous git2 library, this part is rather trivial to implement&#xA;as well:&#xA;&#xA;fn fetchmainremote(repo: &amp;git2::Repository, config: &amp;Conf) -  Result() {&#xA;    log::debug!(&#34;Fetch: {} / {}&#34;, config.originremotename(), config.masterbranchname());&#xA;    repo.findremote(config.originremotename())?&#xA;        .fetch(&amp;[config.masterbranchname()], None, None)&#xA;        .maperr(Error::from)&#xA;}&#xA;&#xA;Here we have a function that takes a reference to the repository as well as a&#xA;reference to our Conf object.&#xA;We then, after some logging, find the appropriate remote in our repository and&#xA;simply call fetch for it.&#xA;In case of Err(), we map that to our anyhow::Error type and return it,&#xA;because the callee should handle that.&#xA;&#xA;Counting the commits&#xA;&#xA;Counting the commits is the last part we need to implement.&#xA;&#xA;fn countcommitsonmainbranch(repo: &amp;git2::Repository, config: &amp;Conf) -  Result(usize, usize, usize) {&#xA;&#xA;The function, like the fetchmainremote function, takes a reference to the&#xA;repository as well as a reference to the Conf object of our program.&#xA;It returns, in case of success, a tuple with three elements.&#xA;I did not add strong typing here, because the codebase is rather small (less&#xA;than 160 lines overall), so there&#39;s not need to be very explicit about the&#xA;types here.&#xA;&#xA;Just keep in mind that the first of the three values is the number of all&#xA;commits, the second is the number of merges and the last is the number of&#xA;non-merges.&#xA;&#xA;That also means:&#xA;&#xA;tuple.0 = tuple.1 + tuple.2&#xA;&#xA;Next, let&#39;s have a variable that holds the branch name&#xA;with the remote, like we&#39;re used from git itself&#xA;(this is later required for git2).&#xA;Also, we need to calculate the timestamp that is the lowest timestamp we&#xA;consider.&#xA;Because our configuration file specifies this in hours rather than seconds, we&#xA;simply  60  60 here.&#xA;&#xA;    let branchname = format!(&#34;{}/{}&#34;, config.originremotename(), config.masterbranchname());&#xA;    let minimumtimeepoch = chrono::offset::Local::now().timestamp() - (config.hourstocheck()  60  60);&#xA;&#xA;    log::debug!(&#34;Branch to count     : {}&#34;, branchname);&#xA;    log::debug!(&#34;Earliest commit time: {:?}&#34;, minimumtimeepoch);&#xA;&#xA;Next, we need to instruct git2 to create a Revwalk object for us:&#xA;&#xA;    let revwalkstart = repo&#xA;        .findbranch(&amp;branchname, git2::BranchType::Remote)?&#xA;        .get()&#xA;        .peeltocommit()?&#xA;        .id();&#xA;&#xA;    log::debug!(&#34;Starting at: {}&#34;, revwalkstart);&#xA;&#xA;    let mut rw = repo.revwalk()?;&#xA;    rw.simplifyfirstparent()?;&#xA;    rw.push(revwalkstart)?;&#xA;&#xA;That can be used to iterate over the history of a branch, starting at a certain&#xA;commit.&#xA;But before we can do that, we need to actually find that commit, which is the&#xA;first part of the above snippet.&#xA;Then, we create a Revwalk object, configure it to consider only the first&#xA;parent (because that&#39;s what we care about) and push the rev to start walking&#xA;from it.&#xA;&#xA;The last bit of the function implements the actual counting.&#xA;&#xA;    let mut commits = 0;&#xA;    let mut merges = 0;&#xA;    let mut nonmerges = 0;&#xA;&#xA;    for rev in rw {&#xA;        let rev = rev?;&#xA;        let commit = repo.findcommit(rev)?;&#xA;        log::trace!(&#34;Found commit: {:?}&#34;, commit);&#xA;&#xA;        if commit.time().seconds() &lt; minimumtimeepoch {&#xA;            log::trace!(&#34;Commit too old, stopping iteration&#34;);&#xA;            break;&#xA;        }&#xA;        commits += 1;&#xA;&#xA;        let ismerge = commit.parentids().count()   1;&#xA;        log::trace!(&#34;Merge: {:?}&#34;, ismerge);&#xA;&#xA;        if ismerge {&#xA;            merges += 1;&#xA;        } else {&#xA;            nonmerges += 1;&#xA;        }&#xA;    }&#xA;&#xA;    log::trace!(&#34;Ready iterating&#34;);&#xA;    Ok((commits, merges, nonmerges))&#xA;}&#xA;&#xA;This is done the simple way, without making use of the excelent iterator API.&#xA;First, we create our variables for counting and then, we use the Revwalk&#xA;object and iterate over it.&#xA;For each rev, we unwrap it using the ? operator and then ask the repo to&#xA;give us the corresponding commit.&#xA;We then check whether the time of the commit is before_ our minimum time and if&#xA;it is, we abort the iteration.&#xA;If it is not, we continnue and count the commit.&#xA;We then check whether the commit has more than one parent, because that is what&#xA;makes a commit a merge-commit, and increase the appropriate variable.&#xA;&#xA;Last but not least, we return our findings to the caller.&#xA;&#xA;Conclusion&#xA;&#xA;And this is it! It was a rather nice journey to implement this bot.&#xA;There isn&#39;t too much that can fail here, some calculations might wrap and result&#xA;in false calculations. Possibly a clippy run would find some things that could&#xA;be improved, of course (feel free to submit patches).&#xA;&#xA;If you want to run this bot on your own instance and for your own repositories,&#xA;make sure to check&#xA;the README file&#xA;first.&#xA;Also, feel free to ask questions about this bot and of course, you&#39;re welcome to&#xA;send patches (make sure to --signoff your commits).&#xA;&#xA;And now, enjoy&#xA;the first post&#xA;of&#xA;the bot.&#xA;&#xA;tags: #mastodon #bot #rust&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>Today, I wrote a mastodon bot.</p>

<blockquote><p>Shut up and show me the code!</p></blockquote>

<p><a href="https://git.beyermatthi.as/complainingaboutmastercommits-bot">Here you go</a>.</p>

<h1 id="the-idea" id="the-idea">The idea</h1>

<p>My idea was, to write a bot that fetches the lastest <code>master</code> from a git
repository and counts some commits and then posts a message to mastodon about
what it counted.</p>

<p>Because I always complain about people pushing to the <code>master</code> branch of a big
community git repository directly, I decided that this would be a perfect fit.</p>

<p>(Whether pushing to master directly is okay and when it is not okay to do this
is another topic and I won&#39;t discuss this here)</p>

<h1 id="the-dependencies" id="the-dependencies">The dependencies</h1>

<p>Well, because I didn&#39;t want to implement <em>everything</em> myself, I started pulling
in some dependencies:</p>
<ul><li><code>log</code> and <code>env_logger</code> for logging</li>
<li><code>structopt</code>, <code>toml</code>, <code>serde</code> and <code>config</code> for argument parsing and config reading</li>
<li><code>anyhow</code> because I don&#39;t care too much about error handling. It just has to
work</li>
<li><code>getset</code> for a bit cleaner code (not strictly necessary, tbh)</li>
<li><code>handlebars</code> for templating the status message that will be posted</li>
<li><code>elefren</code> as mastodon API crate</li>
<li><code>git2</code> for working with the git repository which the bot posts about</li></ul>

<h1 id="the-plan" id="the-plan">The Plan</h1>

<p>How the bot should work was rather clear from the outset. First of all, it
shouldn&#39;t be a always-running-process. I wanted it to be as simple as possible,
thus, triggering it via a systemd-timer should suffice.
Next, it should only <em>fetch</em> the latest commits, so it should be able to work on
a working clone of a repository. This way, we don&#39;t need another clone of a
potentially huge repository on our disk.
The path of the repository should of course not be hardcoded, as shouldn&#39;t the
“upstream” remote name or the “master” branch name (because you might want to track
a “release-xyz” branch or because “master” was renamed to something else).</p>

<p>Also, it should be configurable how many hours of commits should be checked.
Maybe the user wants to run this bot once a day, maybe once a week. Both is possible,
of course.
But if the user runs it once a day, they want to check only the commits of the
last 24 hours.
If they run it once a week, the last 168 hours would be more appropriate.</p>

<p>The message that gets posted should also not be hardcoded, but a template where
the variables the bot counted are available.</p>

<p>All the above goes into the configuration file the bot ready (and which can be
set via the <code>--config</code> option on the bots CLI).</p>

<p>The configuration struct
<a href="https://git.beyermatthi.as/complainingaboutmastercommits-bot/tree/src/main.rs?id=63a6fa0c43d885b5f367372c06c578548d0b6384#n9">for the setup described above</a>
is rather trivial, as is
<a href="https://git.beyermatthi.as/complainingaboutmastercommits-bot/tree/src/main.rs?id=63a6fa0c43d885b5f367372c06c578548d0b6384#n33">the CLI setup</a>.</p>

<h1 id="the-setup" id="the-setup">The setup</h1>

<p>The first things the bot has to do is reading the commandline and the
configuration after initializing the logger, which is a no-brainer, too:</p>

<pre><code class="language-rust">fn main() -&gt; Result&lt;()&gt; {
    env_logger::init();
    log::debug!(&#34;Logger initialized&#34;);

    let opts = Opts::from_args_safe()?;
    let config: Conf = {
        let mut config = ::config::Config::default();

        config
            .merge(::config::File::from(opts.config().to_path_buf()))?
            .merge(::config::Environment::with_prefix(&#34;COMBOT&#34;))?;
        config.try_into()?
    };
    let mastodon_data: elefren::Data = toml::de::from_str(&amp;std::fs::read_to_string(config.mastodon_data())?)?;
</code></pre>

<p>The mastodon data is read from a configuration file that is different from the
main configuration file, because it may contain sensitive data and if a user
wants to put their configuration of the bot into a (public?) git repository,
they might not want to include this data.
That&#39;s why I opted for another file here, its format is described in the
configuration example file (next to the setting where the file actually is).</p>

<p>Next, the mastodon client has to be setup and the repository has to be opened:</p>

<pre><code class="language-rust">    let client = elefren::Mastodon::from(mastodon_data);
    let status_language = elefren::Language::from_639_1(config.status_language())
        .ok_or_else(|| anyhow!(&#34;Could not parse status language code: {}&#34;, config.status_language()))?;
    log::debug!(&#34;config parsed&#34;);

    let repo = git2::Repository::open(config.repository_path())?;
    log::debug!(&#34;Repo opened successfully&#34;);
</code></pre>

<p>which is rather trivial, too.</p>

<h1 id="the-calculations" id="the-calculations">The Calculations</h1>

<p>Then, we fetch the appropriate remote branch and count the commits:</p>

<pre><code class="language-rust">    let _ = fetch_main_remote(&amp;repo, &amp;config)?;
    log::debug!(&#34;Main branch fetched successfully&#34;);

    let (commits, merges, nonmerges) = count_commits_on_main_branch(&amp;repo, &amp;config)?;
    log::debug!(&#34;Counted commits successfully&#34;);

    log::info!(&#34;Commits    = {}&#34;, commits);
    log::info!(&#34;Merges     = {}&#34;, merges);
    log::info!(&#34;Non-Merges = {}&#34;, nonmerges);
</code></pre>

<p>The functions called in this snippet will be described later on.
Just consider them working for now, and let&#39;s move on to the status posting part
of the bot now.</p>

<p>First of all, we use the variables to compute the status message using the
template from the configuration file.</p>

<pre><code class="language-rust">    {
        let status_text = {
            let mut hb = handlebars::Handlebars::new();
            hb.register_template_string(&#34;status&#34;, config.status_template())?;
            let mut data = std::collections::BTreeMap::new();
            data.insert(&#34;commits&#34;, commits);
            data.insert(&#34;merges&#34;, merges);
            data.insert(&#34;nonmerges&#34;, nonmerges);
            hb.render(&#34;status&#34;, &amp;data)?
        };
</code></pre>

<p>Handlebars is a perfect fit for that job, as it is rather trivial to use, albeit
a very powerful templating language is used.
The user could, for example, even add some conditions to their template, like if
there are no commits at all, the status message could just say
“I&#39;m a lonely bot, because nobody commits to master these days...” or something
like that.</p>

<p>Next, we build the status object we pass to mastodon, and post it.</p>

<pre><code class="language-rust">        let status = elefren::StatusBuilder::new()
            .status(status_text)
            .language(status_language)
            .build()
            .expect(&#34;Failed to build status&#34;);

        let status = client.new_status(status)
            .expect(&#34;Failed to post status&#34;);
        if let Some(url) = status.url.as_ref() {
            log::info!(&#34;Status posted: {}&#34;, url);
        } else {
            log::info!(&#34;Status posted, no url&#34;);
        }
        log::debug!(&#34;New status = {:?}&#34;, status);
    }

    Ok(())
} // main()
</code></pre>

<p>Some logging is added as well, of course.</p>

<p>And that&#39;s the whole main function!</p>

<h1 id="fetching-the-repository" id="fetching-the-repository">Fetching the repository.</h1>

<p>But we are not done yet.
First of all, we need the function that fetches the remote repository.</p>

<p>Because of the infamous <code>git2</code> library, this part is rather trivial to implement
as well:</p>

<pre><code class="language-rust">fn fetch_main_remote(repo: &amp;git2::Repository, config: &amp;Conf) -&gt; Result&lt;()&gt; {
    log::debug!(&#34;Fetch: {} / {}&#34;, config.origin_remote_name(), config.master_branch_name());
    repo.find_remote(config.origin_remote_name())?
        .fetch(&amp;[config.master_branch_name()], None, None)
        .map_err(Error::from)
}
</code></pre>

<p>Here we have a function that takes a reference to the repository as well as a
reference to our <code>Conf</code> object.
We then, after some logging, find the appropriate remote in our repository and
simply call <code>fetch</code> for it.
In case of <code>Err(_)</code>, we map that to our <code>anyhow::Error</code> type and return it,
because the callee should handle that.</p>

<h1 id="counting-the-commits" id="counting-the-commits">Counting the commits</h1>

<p>Counting the commits is the last part we need to implement.</p>

<pre><code class="language-rust">fn count_commits_on_main_branch(repo: &amp;git2::Repository, config: &amp;Conf) -&gt; Result&lt;(usize, usize, usize)&gt; {
</code></pre>

<p>The function, like the <code>fetch_main_remote</code> function, takes a reference to the
repository as well as a reference to the <code>Conf</code> object of our program.
It returns, in case of success, a tuple with three elements.
I did not add strong typing here, because the codebase is rather small (less
than 160 lines overall), so there&#39;s not need to be <em>very</em> explicit about the
types here.</p>

<p>Just keep in mind that the first of the three values is the number of all
commits, the second is the number of merges and the last is the number of
non-merges.</p>

<p>That also means:</p>

<pre><code>tuple.0 = tuple.1 + tuple.2
</code></pre>

<p>Next, let&#39;s have a variable that holds the branch name
with the remote, like we&#39;re used from git itself
(this is later required for <code>git2</code>).
Also, we need to calculate the timestamp that is the lowest timestamp we
consider.
Because our configuration file specifies this in <em>hours</em> rather than seconds, we
simply <code>* 60 * 60</code> here.</p>

<pre><code class="language-rust">    let branchname = format!(&#34;{}/{}&#34;, config.origin_remote_name(), config.master_branch_name());
    let minimum_time_epoch = chrono::offset::Local::now().timestamp() - (config.hours_to_check() * 60 * 60);

    log::debug!(&#34;Branch to count     : {}&#34;, branchname);
    log::debug!(&#34;Earliest commit time: {:?}&#34;, minimum_time_epoch);
</code></pre>

<p>Next, we need to instruct <code>git2</code> to create a <code>Revwalk</code> object for us:</p>

<pre><code class="language-rust">    let revwalk_start = repo
        .find_branch(&amp;branchname, git2::BranchType::Remote)?
        .get()
        .peel_to_commit()?
        .id();

    log::debug!(&#34;Starting at: {}&#34;, revwalk_start);

    let mut rw = repo.revwalk()?;
    rw.simplify_first_parent()?;
    rw.push(revwalk_start)?;
</code></pre>

<p>That can be used to iterate over the history of a branch, starting at a certain
commit.
But before we can do that, we need to actually <em>find</em> that commit, which is the
first part of the above snippet.
Then, we create a <code>Revwalk</code> object, configure it to consider only the first
parent (because that&#39;s what we care about) and push the rev to start walking
from it.</p>

<p>The last bit of the function implements the actual counting.</p>

<pre><code class="language-rust">    let mut commits = 0;
    let mut merges = 0;
    let mut nonmerges = 0;

    for rev in rw {
        let rev = rev?;
        let commit = repo.find_commit(rev)?;
        log::trace!(&#34;Found commit: {:?}&#34;, commit);

        if commit.time().seconds() &lt; minimum_time_epoch {
            log::trace!(&#34;Commit too old, stopping iteration&#34;);
            break;
        }
        commits += 1;

        let is_merge = commit.parent_ids().count() &gt; 1;
        log::trace!(&#34;Merge: {:?}&#34;, is_merge);

        if is_merge {
            merges += 1;
        } else {
            nonmerges += 1;
        }
    }

    log::trace!(&#34;Ready iterating&#34;);
    Ok((commits, merges, nonmerges))
}
</code></pre>

<p>This is done the simple way, without making use of the excelent iterator API.
First, we create our variables for counting and then, we use the <code>Revwalk</code>
object and iterate over it.
For each <code>rev</code>, we unwrap it using the <code>?</code> operator and then ask the repo to
give us the corresponding commit.
We then check whether the time of the commit is <em>before</em> our minimum time and if
it is, we abort the iteration.
If it is not, we continnue and count the commit.
We then check whether the commit has more than one parent, because that is what
makes a commit a merge-commit, and increase the appropriate variable.</p>

<p>Last but not least, we return our findings to the caller.</p>

<h1 id="conclusion" id="conclusion">Conclusion</h1>

<p>And this is it! It was a rather nice journey to implement this bot.
There isn&#39;t too much that can fail here, some calculations might wrap and result
in false calculations. Possibly a <code>clippy</code> run would find some things that could
be improved, of course (feel free to submit patches).</p>

<p>If you want to run this bot on your own instance and for your own repositories,
make sure to check
<a href="https://git.beyermatthi.as/complainingaboutmastercommits-bot/tree/README.md">the README file</a>
first.
Also, feel free to ask questions about this bot and of course, you&#39;re welcome to
send patches (make sure to <code>--signoff</code> your commits).</p>

<p>And now, enjoy
<a href="https://botsin.space/@complainingaboutmastercommits/105572322090509761">the first post</a>
of
<a href="https://botsin.space/@complainingaboutmastercommits">the bot</a>.</p>

<p>tags: <a href="https://beyermatthias.de/tag:mastodon" class="hashtag"><span>#</span><span class="p-category">mastodon</span></a> <a href="https://beyermatthias.de/tag:bot" class="hashtag"><span>#</span><span class="p-category">bot</span></a> <a href="https://beyermatthias.de/tag:rust" class="hashtag"><span>#</span><span class="p-category">rust</span></a></p>
]]></content:encoded>
      <guid>https://beyermatthias.de/writing-a-mastodon-bot</guid>
      <pubDate>Sun, 17 Jan 2021 16:40:21 +0100</pubDate>
    </item>
  </channel>
</rss>