radicle_source/
commit.rs

1// This file is part of radicle-surf
2// <https://github.com/radicle-dev/radicle-surf>
3//
4// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
5//
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License version 3 or
8// later as published by the Free Software Foundation.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with this program. If not, see <https://www.gnu.org/licenses/>.
17
18use std::convert::TryFrom as _;
19
20use serde::{
21    ser::{SerializeStruct as _, Serializer},
22    Serialize,
23};
24
25use radicle_surf::{
26    diff,
27    vcs::git::{self, Browser, Rev},
28};
29
30use crate::{branch::Branch, error::Error, person::Person, revision::Revision};
31
32/// Commit statistics.
33#[derive(Clone, Serialize)]
34pub struct Stats {
35    /// Additions.
36    pub additions: u64,
37    /// Deletions.
38    pub deletions: u64,
39}
40
41/// Representation of a changeset between two revs.
42#[derive(Clone, Serialize)]
43pub struct Commit {
44    /// The commit header.
45    pub header: Header,
46    /// The change statistics for this commit.
47    pub stats: Stats,
48    /// The changeset introduced by this commit.
49    pub diff: diff::Diff,
50    /// The list of branches this commit belongs to.
51    pub branches: Vec<Branch>,
52}
53
54/// Representation of a code commit.
55#[derive(Clone)]
56pub struct Header {
57    /// Identifier of the commit in the form of a sha1 hash. Often referred to
58    /// as oid or object id.
59    pub sha1: git2::Oid,
60    /// The author of the commit.
61    pub author: Person,
62    /// The summary of the commit message body.
63    pub summary: String,
64    /// The entire commit message body.
65    pub message: String,
66    /// The committer of the commit.
67    pub committer: Person,
68    /// The recorded time of the committer signature. This is a convenience
69    /// alias until we expose the actual author and commiter signatures.
70    pub committer_time: git2::Time,
71}
72
73impl Header {
74    /// Returns the commit description text. This is the text after the one-line
75    /// summary.
76    #[must_use]
77    pub fn description(&self) -> &str {
78        self.message
79            .strip_prefix(&self.summary)
80            .unwrap_or(&self.message)
81            .trim()
82    }
83}
84
85impl From<&git::Commit> for Header {
86    fn from(commit: &git::Commit) -> Self {
87        Self {
88            sha1: commit.id,
89            author: Person {
90                name: commit.author.name.clone(),
91                email: commit.author.email.clone(),
92            },
93            summary: commit.summary.clone(),
94            message: commit.message.clone(),
95            committer: Person {
96                name: commit.committer.name.clone(),
97                email: commit.committer.email.clone(),
98            },
99            committer_time: commit.committer.time,
100        }
101    }
102}
103
104impl Serialize for Header {
105    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
106    where
107        S: Serializer,
108    {
109        let mut state = serializer.serialize_struct("Header", 6)?;
110        state.serialize_field("sha1", &self.sha1.to_string())?;
111        state.serialize_field("author", &self.author)?;
112        state.serialize_field("summary", &self.summary)?;
113        state.serialize_field("description", &self.description())?;
114        state.serialize_field("committer", &self.committer)?;
115        state.serialize_field("committerTime", &self.committer_time.seconds())?;
116        state.end()
117    }
118}
119
120/// A selection of commit headers and their statistics.
121#[derive(Serialize)]
122pub struct Commits {
123    /// The commit headers
124    pub headers: Vec<Header>,
125    /// The statistics for the commit headers
126    pub stats: radicle_surf::vcs::git::Stats,
127}
128
129/// Retrieves a [`Commit`].
130///
131/// # Errors
132///
133/// Will return [`Error`] if the project doesn't exist or the surf interaction
134/// fails.
135pub fn commit(browser: &mut Browser<'_>, sha1: git2::Oid) -> Result<Commit, Error> {
136    browser.commit(sha1)?;
137
138    let history = browser.get();
139    let commit = history.first();
140
141    let diff = if let Some(parent) = commit.parents.first() {
142        browser.diff(*parent, sha1)?
143    } else {
144        browser.initial_diff(sha1)?
145    };
146
147    let mut deletions = 0;
148    let mut additions = 0;
149
150    for file in &diff.modified {
151        if let diff::FileDiff::Plain { ref hunks } = file.diff {
152            for hunk in hunks.iter() {
153                for line in &hunk.lines {
154                    match line {
155                        diff::LineDiff::Addition { .. } => additions += 1,
156                        diff::LineDiff::Deletion { .. } => deletions += 1,
157                        _ => {},
158                    }
159                }
160            }
161        }
162    }
163
164    for file in &diff.created {
165        if let diff::FileDiff::Plain { ref hunks } = file.diff {
166            for hunk in hunks.iter() {
167                for line in &hunk.lines {
168                    if let diff::LineDiff::Addition { .. } = line {
169                        additions += 1
170                    }
171                }
172            }
173        }
174    }
175
176    for file in &diff.deleted {
177        if let diff::FileDiff::Plain { ref hunks } = file.diff {
178            for hunk in hunks.iter() {
179                for line in &hunk.lines {
180                    if let diff::LineDiff::Deletion { .. } = line {
181                        deletions += 1
182                    }
183                }
184            }
185        }
186    }
187
188    let branches = browser
189        .revision_branches(sha1)?
190        .into_iter()
191        .map(Branch::from)
192        .collect();
193
194    Ok(Commit {
195        header: Header::from(commit),
196        stats: Stats {
197            additions,
198            deletions,
199        },
200        diff,
201        branches,
202    })
203}
204
205/// Retrieves the [`Header`] for the given `sha1`.
206///
207/// # Errors
208///
209/// Will return [`Error`] if the project doesn't exist or the surf interaction
210/// fails.
211pub fn header(browser: &mut Browser<'_>, sha1: git2::Oid) -> Result<Header, Error> {
212    browser.commit(sha1)?;
213
214    let history = browser.get();
215    let commit = history.first();
216
217    Ok(Header::from(commit))
218}
219
220/// Retrieves the [`Commit`] history for the given `revision`.
221///
222/// # Errors
223///
224/// Will return [`Error`] if the project doesn't exist or the surf interaction
225/// fails.
226pub fn commits<P>(
227    browser: &mut Browser<'_>,
228    maybe_revision: Option<Revision<P>>,
229) -> Result<Commits, Error>
230where
231    P: ToString,
232{
233    let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
234
235    if let Some(revision) = maybe_revision {
236        browser.rev(revision)?;
237    }
238
239    let headers = browser.get().iter().map(Header::from).collect();
240    let stats = browser.get_stats()?;
241
242    Ok(Commits { headers, stats })
243}