diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml
index 43d347a1de779e63e71571463602e9f76a01baeb..2b6fd7d4d3d82476aefb23e857e7f3cc9f517cfe 100644
--- a/xtask/Cargo.toml
+++ b/xtask/Cargo.toml
@@ -32,3 +32,5 @@ version-compare = "0.0.11"
 tera = { version = "1", default-features = false }
 weight-analyzer = {path = "../resources/weight_analyzer"}
 scale-value = "0.13.0"
+serde_yaml = { version = "0.9.27", default-features = false }
+chrono = "0.4.31"
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 5efd1857c8a827bd3717f5ed230ae4ec2d0eb677..c65408416ac7a7ff755eec65280fdad7d6e01b05 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -57,7 +57,12 @@ enum DuniterXTaskCommand {
     /// Update raw specs locally with the files published on a Release
     UpdateRawSpecs { milestone: String },
     /// Project management: show issues for a Milestone
-    ShowMilestone { milestone: String },
+    ShowMilestone {
+        milestone: String,
+        #[clap(long)]
+        /// Previous dump to compare with
+        compare_with: Option<String>,
+    },
     /// Create asset in a release
     CreateAssetLink {
         tag: String,
@@ -94,9 +99,10 @@ async fn main() -> Result<()> {
         DuniterXTaskCommand::ReleaseRuntime { milestone, branch } => {
             release_runtime::release_runtime(milestone, branch).await
         }
-        DuniterXTaskCommand::ShowMilestone { milestone } => {
-            show_milestone::show_milestone(milestone).await
-        }
+        DuniterXTaskCommand::ShowMilestone {
+            milestone,
+            compare_with,
+        } => show_milestone::show_milestone(milestone, compare_with).await,
         DuniterXTaskCommand::UpdateRawSpecs { milestone } => {
             release_runtime::update_raw_specs(milestone).await
         }
diff --git a/xtask/src/release_runtime.rs b/xtask/src/release_runtime.rs
index ce3f467116f1d789a9f8c897d207e9954ab63c15..d1d1cfaf8c7a63a062158f8446b033359ab5279e 100644
--- a/xtask/src/release_runtime.rs
+++ b/xtask/src/release_runtime.rs
@@ -100,13 +100,18 @@ pub(super) async fn release_runtime(milestone: String, branch: String) -> Result
 
     // Get changes (list of MRs) from gitlab API
     let changes = get_changes::get_changes(milestone.clone()).await?;
+    let changes_str = changes
+        .iter()
+        .map(|change| change.to_string())
+        .collect::<Vec<String>>()
+        .join("\n");
 
     release_notes.push_str(
         format!(
             "
 # Changes
 
-{changes}
+{changes_str}
 "
         )
         .as_str(),
diff --git a/xtask/src/release_runtime/get_changes.rs b/xtask/src/release_runtime/get_changes.rs
index a4147cf3fcb667c5d9bd8cdb9aec64c0d36c0a3a..f396898658f93d9ff8df7a95d8a77dbe2d6acc34 100644
--- a/xtask/src/release_runtime/get_changes.rs
+++ b/xtask/src/release_runtime/get_changes.rs
@@ -25,7 +25,23 @@ use graphql_client::{GraphQLQuery, Response};
 )]
 pub struct GetChangesQuery;
 
-pub(crate) async fn get_changes(milestone: String) -> Result<String> {
+#[derive(Debug, serde::Serialize, serde::Deserialize)]
+pub struct GitlabMRChange {
+    pub title: String,
+    pub mr_number: u32,
+}
+
+impl ToString for GitlabMRChange {
+    fn to_string(&self) -> String {
+        format!(
+            "* {mr_title} (!{mr_number})",
+            mr_title = self.title,
+            mr_number = self.mr_number
+        )
+    }
+}
+
+pub(crate) async fn get_changes(milestone: String) -> Result<Vec<GitlabMRChange>> {
     // this is the important line
     let request_body = GetChangesQuery::build_query(get_changes_query::Variables { milestone });
 
@@ -41,13 +57,12 @@ pub(crate) async fn get_changes(milestone: String) -> Result<String> {
         if let Some(project) = data.project {
             if let Some(merge_requests) = project.merge_requests {
                 if let Some(nodes) = merge_requests.nodes {
-                    let mut changes = String::new();
+                    let mut changes = Vec::<GitlabMRChange>::new();
                     for merge_request in nodes.into_iter().flatten() {
-                        changes.push_str(&format!(
-                            "* {mr_title} (!{mr_number})\n",
-                            mr_title = merge_request.title,
-                            mr_number = merge_request.iid
-                        ));
+                        changes.push(GitlabMRChange {
+                            title: merge_request.title,
+                            mr_number: merge_request.iid.parse()?,
+                        });
                     }
                     Ok(changes)
                 } else {
diff --git a/xtask/src/release_runtime/get_issues.rs b/xtask/src/release_runtime/get_issues.rs
index d7bc8f0392706905b6bd82d809187a5d22f46e66..a1080a7fc6bcb7493bee51a417ee17d1a1adac98 100644
--- a/xtask/src/release_runtime/get_issues.rs
+++ b/xtask/src/release_runtime/get_issues.rs
@@ -16,6 +16,7 @@
 
 use anyhow::{anyhow, Result};
 use graphql_client::{GraphQLQuery, Response};
+use std::fmt::Display;
 
 #[derive(GraphQLQuery)]
 #[graphql(
@@ -25,14 +26,26 @@ use graphql_client::{GraphQLQuery, Response};
 )]
 pub struct GetIssuesQuery;
 
-pub struct Issue {
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
+pub struct GitlabIssue {
     pub title: String,
-    pub number: String,
+    pub number: u32,
     pub status: String,
     pub assignees: Vec<String>,
 }
 
-pub(crate) async fn get_issues(milestone: String) -> Result<Vec<Issue>> {
+impl Display for GitlabIssue {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let str = format!(
+            "#{issue_number} — {issue_title}",
+            issue_title = self.title,
+            issue_number = self.number
+        );
+        write!(f, "{}", str)
+    }
+}
+
+pub(crate) async fn get_issues(milestone: String) -> Result<Vec<GitlabIssue>> {
     // this is the important line
     let request_body = GetIssuesQuery::build_query(get_issues_query::Variables { milestone });
 
@@ -50,9 +63,9 @@ pub(crate) async fn get_issues(milestone: String) -> Result<Vec<Issue>> {
                 if let Some(nodes) = issues.nodes {
                     let mut changes = Vec::new();
                     for issue in nodes.into_iter().flatten() {
-                        changes.push(Issue {
+                        changes.push(GitlabIssue {
                             title: issue.title,
-                            number: issue.iid.expect("iid must exist"),
+                            number: issue.iid.expect("iid must exist").parse()?,
                             status: format!("{:?}", issue.state),
                             assignees: issue
                                 .assignees
diff --git a/xtask/src/show_milestone.rs b/xtask/src/show_milestone.rs
index 5293e217a3ed8b9386caf7acb0d5a290ca42c363..7614e06769b1d7747cf4b225b455a5573f420d5b 100644
--- a/xtask/src/show_milestone.rs
+++ b/xtask/src/show_milestone.rs
@@ -14,57 +14,236 @@
 // You should have received a copy of the GNU Affero General Public License
 // along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
 
+use crate::release_runtime::get_changes::GitlabMRChange;
+use crate::release_runtime::get_issues::GitlabIssue;
 use crate::release_runtime::{get_changes, get_issues};
 use anyhow::Result;
+use std::fs;
 
-pub(super) async fn show_milestone(milestone: String) -> Result<()> {
-    // Get changes (list of MRs) from gitlab API
-    let changes = get_changes::get_changes(milestone.clone()).await?;
+#[derive(Debug, serde::Serialize, serde::Deserialize)]
+struct MilestoneDump {
+    milestone: String,
+    date: String,
+    changes: Vec<GitlabMRChange>,
+    issues: Vec<GitlabIssue>,
+    new_issues: IssueCounter,
+    unstarted_issues: IssueCounter,
+    taken_issues: IssueCounter,
+    unchanged_issues: IssueCounter,
+    open_issues: IssueCounter,
+    closed_issues: IssueCounter,
+}
 
-    let mut release_notes = format!("");
-    release_notes.push_str(
-        format!(
-            "
-# Changes
+#[derive(Debug, serde::Serialize, serde::Deserialize)]
+struct IssueCounter {
+    count: u32,
+    issues: Vec<u32>,
+}
 
-{changes}
-"
-        )
-        .as_str(),
-    );
+impl IssueCounter {
+    fn new() -> Self {
+        IssueCounter {
+            count: 0,
+            issues: Vec::new(),
+        }
+    }
+}
+
+impl From<Vec<GitlabIssue>> for IssueCounter {
+    fn from(issues: Vec<GitlabIssue>) -> Self {
+        IssueCounter {
+            count: issues.len() as u32,
+            issues: issues.into_iter().map(|issue| issue.number).collect(),
+        }
+    }
+}
+
+impl MilestoneDump {
+    fn new() -> Self {
+        MilestoneDump {
+            milestone: "".to_string(),
+            date: "".to_string(),
+            changes: Vec::new(),
+            issues: Vec::new(),
+            new_issues: IssueCounter::new(),
+            taken_issues: IssueCounter::new(),
+            unchanged_issues: IssueCounter::new(),
+            unstarted_issues: IssueCounter::new(),
+            open_issues: IssueCounter::new(),
+            closed_issues: IssueCounter::new(),
+        }
+    }
+}
 
+pub(super) async fn show_milestone(milestone: String, compare_with: Option<String>) -> Result<()> {
     // Get changes (list of MRs) from gitlab API
+    let mut changes = get_changes::get_changes(milestone.clone()).await?;
     let mut issues = get_issues::get_issues(milestone.clone()).await?;
-    let mut issues_str = String::new();
-    issues.sort_by(|a, b| b.assignees.len().cmp(&a.assignees.len()));
-    issues.iter().for_each(|issue| {
-        issues_str.push_str(&format!(
-            "* [#{issue_number}](https://git.duniter.org/nodes/rust/duniter-v2s/-/issues/{issue_number}) ({issue_status}",
-            issue_number = issue.number,
-            issue_status = issue.status
-        ));
-        if !issue.assignees.is_empty() {
-            issues_str.push_str(&format!(
-                " — assigned to {assignees}) ",
-                assignees = issue.assignees.join(", ")
-            ));
-        } else {
-            issues_str.push_str(") ");
-        }
-        issues_str.push_str(&format!(" {issue_title}\n", issue_title = issue.title));
-    });
+    let previous_dump: MilestoneDump = compare_with
+        .map(|file| serde_yaml::from_str(&fs::read_to_string(file).unwrap()).unwrap())
+        .unwrap_or(MilestoneDump::new());
 
-    release_notes.push_str(
-        format!(
-            "
-# Issues
+    changes.sort_by(|a, b| b.mr_number.cmp(&a.mr_number));
+    let dump = MilestoneDump {
+        milestone: milestone.clone(),
+        date: chrono::Local::now().format("%Y-%m-%d").to_string(),
+        changes,
+        issues: issues.clone(),
+        new_issues: issues
+            .clone()
+            .into_iter()
+            .filter(|issue| {
+                !previous_dump
+                    .issues
+                    .iter()
+                    .find(|p| p.number == issue.number)
+                    .is_some()
+            })
+            .collect::<Vec<GitlabIssue>>()
+            .into(),
+        taken_issues: issues
+            .clone()
+            .into_iter()
+            .filter(|issue| {
+                previous_dump
+                    .issues
+                    .iter()
+                    .find(|p| {
+                        p.number == issue.number
+                            && p.status == "opened"
+                            && p.assignees.is_empty()
+                            && !issue.assignees.is_empty()
+                    })
+                    .is_some()
+            })
+            .collect::<Vec<GitlabIssue>>()
+            .into(),
+        unchanged_issues: issues
+            .clone()
+            .into_iter()
+            .filter(|issue| previous_dump.issues.iter().find(|p| *p == issue).is_some())
+            .collect::<Vec<GitlabIssue>>()
+            .into(),
+        unstarted_issues: issues
+            .clone()
+            .into_iter()
+            .filter(|issue| issue.status == "opened" && issue.assignees.is_empty())
+            .collect::<Vec<GitlabIssue>>()
+            .into(),
+        open_issues: issues
+            .clone()
+            .into_iter()
+            .filter(|issue| issue.status == "opened")
+            .collect::<Vec<GitlabIssue>>()
+            .into(),
+        closed_issues: issues
+            .clone()
+            .into_iter()
+            .filter(|issue| issue.status == "closed")
+            .collect::<Vec<GitlabIssue>>()
+            .into(),
+    };
+
+    let mut release_notes = format!("");
+    release_notes.push_str(serde_yaml::to_string(&dump)?.as_str());
 
-{issues_str}
-"
+    // Dump issues ordered by ID (for raw changelog — allows to compare with previous changelog to see new issues)
+
+    let mut issues_str = "## Issues\n\n".to_string();
+    issues_str.push_str(
+        print_issues_section(
+            "\n### Ouvertes",
+            &mut dump
+                .open_issues
+                .issues
+                .iter()
+                .map(|i| get_issue(&issues, *i))
+                .collect(),
+        )
+        .as_str(),
+    );
+    // issues_str.push_str(print_issues_section("\n#### Dont non démarrées",
+    //                                          &mut dump.unstarted_issues.issues.iter().map(|i| get_issue(&issues, *i)).collect()).as_str());
+    issues_str.push_str(
+        print_issues_section(
+            "\n#### Dont assignées depuis le dernier point",
+            &mut dump
+                .taken_issues
+                .issues
+                .iter()
+                .map(|i| get_issue(&issues, *i))
+                .filter(|i| !i.assignees.is_empty())
+                .collect(),
+        )
+        .as_str(),
+    );
+    issues_str.push_str(
+        print_issues_section(
+            "\n#### Dont stagnantes depuis le dernier point",
+            &mut dump
+                .unchanged_issues
+                .issues
+                .iter()
+                .map(|i| get_issue(&issues, *i))
+                .filter(|i| i.status == "opened")
+                .collect(),
+        )
+        .as_str(),
+    );
+    // issues_str.push_str(print_issues_section("\n### Fermées",
+    //                                          &mut dump.closed_issues.issues.iter().map(|i| get_issue(&issues, *i)).collect()).as_str());
+    issues_str.push_str(
+        print_issues_section(
+            "\n#### Dont fermées depuis le dernier point",
+            &mut dump
+                .taken_issues
+                .issues
+                .iter()
+                .map(|i| get_issue(&issues, *i))
+                .filter(|i| i.status == "closed")
+                .chain(
+                    dump.new_issues
+                        .issues
+                        .iter()
+                        .map(|i| get_issue(&issues, *i))
+                        .filter(|i| i.status == "closed"),
+                )
+                .collect(),
         )
         .as_str(),
     );
+    release_notes.push_str(issues_str.as_str());
     println!("{}", release_notes);
-
     Ok(())
 }
+
+fn print_issues_section(title: &str, issues: &mut Vec<GitlabIssue>) -> String {
+    let mut issues_str = String::new();
+    issues_str.push_str(format!("{}\n\n", title).as_str());
+    issues_str.push_str(format!("Total :  {}\n\n", issues.len()).as_str());
+    issues_str.push_str("| ID | Status | Assignees | Title |\n");
+    issues_str.push_str("| -- | ------ | --------- | ----- |\n");
+    issues.sort_by(|a, b| b.number.cmp(&a.number));
+    issues.iter().for_each(|issue_number| {
+        issues_str.push_str(print_issue(issue_number).as_str());
+    });
+    issues_str
+}
+
+fn print_issue(issue: &GitlabIssue) -> String {
+    format!(
+        "| [#{issue_number}](https://git.duniter.org/nodes/rust/duniter-v2s/-/issues/{issue_number}) | {issue_status} | {assignees} | {issue_title} |\n",
+        issue_number = issue.number,
+        issue_status = issue.status,
+        assignees = issue.assignees.join(", "),
+        issue_title = issue.title
+    )
+}
+
+fn get_issue(issues: &Vec<GitlabIssue>, number: u32) -> GitlabIssue {
+    issues
+        .iter()
+        .find(|issue| issue.number == number)
+        .unwrap()
+        .clone()
+}