The presentation will begin on the next slide.

CLC presentation

Created by Cyrus Yip.

My student id is 1225258.

May 27, 2024

Overview

Here are the goals I intended to achieve with this project.

  • Learn the Rust programming language
  • Develop a functional program in this language to show proficiency

Significance

I wanted to learn Rust because its a popular language with focus on performance, safety, and memory management.

Learning a new lower-level like Rust will also improve my understanding of computer science.

Plan

  1. Learn the basics of Rust
  2. Think of a program idea
  3. Determine the required dependencies
  4. Understand their documentation
  5. Create sample programs to test working with the dependencies
  6. Develop a prototype for the program
  7. Add additional features
  8. Clean up and refactor the code
  9. Perform minor bug fixes and improvements

Plan (continued)

  1. Publish code repository

Resources

These are what I used to learn:

Outline

I wanted to create a program that parsed a previous project's Git history to display a formatted changelog.

My previous project, songs-backup, periodically records the current state of a YouTube playlist with a commit.

Dependencies

  • clap for parsing command-line arguments
  • git2 for interacting with the Git repository

Challenges

I had trouble with the complexity of the git2 library.

Overcoming Challenges

I overcame this by experimentation and looking at examples.

Iterations

At the start, I just outputed dates to see if I parsed the commits correctly.


              Fri Feb  2 23:12:42 2024 +0000
              Fri Feb  2 22:15:27 2024 +0000
              Fri Feb  2 21:12:29 2024 +0000
              Fri Feb  2 20:14:50 2024 +0000
              Fri Feb  2 19:15:53 2024 +0000
              Fri Feb  2 18:16:57 2024 +0000
              Fri Feb  2 17:43:11 2024 +0000
              Fri Feb  2 16:52:16 2024 +0000
              Fri Feb  2 15:14:01 2024 +0000
              Fri Feb  2 14:12:27 2024 +0000
              Fri Feb  2 13:12:14 2024 +0000
              Fri Feb  2 12:13:26 2024 +0000
              Fri Feb  2 11:14:32 2024 +0000
              Fri Feb  2 10:18:17 2024 +0000
              Fri Feb  2 09:14:14 2024 +0000
              Fri Feb  2 08:15:02 2024 +0000
              Fri Feb  2 07:14:14 2024 +0000
              Fri Feb  2 06:13:05 2024 +0000
              Fri Feb  2 05:14:00 2024 +0000
              Fri Feb  2 04:22:57 2024 +0000
              Fri Feb  2 03:12:16 2024 +0000
              Fri Feb  2 02:14:40 2024 +0000
              Fri Feb  2 01:15:01 2024 +0000
              Fri Feb  2 00:16:26 2024 +0000
              Thu Feb  1 23:12:34 2024 +0000
              Thu Feb  1 22:17:17 2024 +0000
              Thu Feb  1 21:14:15 2024 +0000
              Thu Feb  1 20:16:29 2024 +0000
              Thu Feb  1 19:16:22 2024 +0000
              Thu Feb  1 18:19:24 2024 +0000
              Thu Feb  1 17:46:01 2024 +0000
              Thu Feb  1 16:52:06 2024 +0000
              Thu Feb  1 15:13:22 2024 +0000
              Thu Feb  1 14:13:34 2024 +0000
              Thu Feb  1 13:12:33 2024 +0000
              Thu Feb  1 12:13:51 2024 +0000
              Thu Feb  1 11:12:37 2024 +0000
              Thu Feb  1 10:15:57 2024 +0000
              Thu Feb  1 09:14:45 2024 +0000
              Thu Feb  1 08:18:40 2024 +0000
            

After some work, I started to filter them and list the video ids.


              ## Thu Feb  2 17:02:57 2023 +0000
              Added rW843YCHrh0
              ## Thu Feb  2 18:45:16 2023 +0000
              Added ZxY13m83UUw
              ## Mon Feb  6 21:11:32 2023 +0000
              Added 2KlHN15BCNg
              Added G8twM11msXU
              ## Wed Feb  8 18:24:02 2023 +0000
              Removed EFjtIV45Vw4
              ## Thu Feb  9 00:15:49 2023 +0000
              Added bB2GSoDYhLw
              Added gCc_dqj_W1s
              Added hiFH2I7xaeU
              ## Thu Feb  9 08:16:47 2023 +0000
              Added WgVWLa6klKo
              ## Thu Feb  9 18:49:44 2023 +0000
              Added Isd5ViaHLbg
              Added xSg5wI3hcy4
              ## Fri Feb 10 00:15:48 2023 +0000
              Added pEEEe9xnv_Y
              ## Sat Feb 11 12:11:56 2023 +0000
              Added edid66UmfiQ
              ## Sun Feb 12 10:14:00 2023 +0000
              Added Ycc7ZW27VEY
              ## Sun Feb 12 11:09:38 2023 +0000
              Added AROi9sNCVKs
              Added SID2OofwYyM
              Added iiw9Z1I1AcE
              Added qLTCh1WpDBU
              ## Sun Feb 12 18:44:34 2023 +0000
              Added fW1Cgv63naI
              Added iPcX6EpJI3o
              Added w4nihuYVTW0
              ## Tue Feb 14 00:16:26 2023 +0000
              Added J3KA6WDAYPM
              ## Wed Feb 15 00:16:22 2023 +0000
              Added CSo0rfnWUQM
              Added WgTms03-syU
              ## Wed Feb 15 08:17:34 2023 +0000
              Added cHgfFyyMXf4
              ## Wed Feb 15 09:12:03 2023 +0000
              Added mgmfkIAXHjg
              ## Wed Feb 15 17:02:09 2023 +0000
              Added 1NU9Pk607uk
              Removed m0TRYx894RE
            

Finally, I added a link to the video.


              # songs-history
              ## Mon Nov  7 09:27:34 2022 -0800
              Added [--99FF6xFNs](https://youtu.be/--99FF6xFNs)  
              Added [-1c6y3qsR2g](https://youtu.be/-1c6y3qsR2g)  
              Added [-3xrSr9a5vg](https://youtu.be/-3xrSr9a5vg)  
              Added [-40fLtf9Hio](https://youtu.be/-40fLtf9Hio)  
              Added [-GoZOCNSIYw](https://youtu.be/-GoZOCNSIYw)  
              Added [-IudP7eYVLc](https://youtu.be/-IudP7eYVLc)  
              Added [-RGYC87IZ-E](https://youtu.be/-RGYC87IZ-E)  
              Added [-RfkZve6cIg](https://youtu.be/-RfkZve6cIg)  
              Added [-TOtm_9qBf8](https://youtu.be/-TOtm_9qBf8)  
              Added [-TcBZiit5a8](https://youtu.be/-TcBZiit5a8)  
              Added [-ZxMDgCpGlg](https://youtu.be/-ZxMDgCpGlg)  
              Added [-bWqcKzbQBY](https://youtu.be/-bWqcKzbQBY)  
              Added [-biOGdYiF-I](https://youtu.be/-biOGdYiF-I)  
              Added [-gkwJEkS4_M](https://youtu.be/-gkwJEkS4_M)  
              Added [-jPgsg3xs0I](https://youtu.be/-jPgsg3xs0I)  
              Added [-lcVA_rn-7s](https://youtu.be/-lcVA_rn-7s)  
              Added [-nfnbrICydI](https://youtu.be/-nfnbrICydI)  
              Added [-tKVN2mAKRI](https://youtu.be/-tKVN2mAKRI)  
              Added [-u3NXWgQpjY](https://youtu.be/-u3NXWgQpjY)  
              Added [-wpTY3LM5bc](https://youtu.be/-wpTY3LM5bc)  
              Added [-y6J28I5z0c](https://youtu.be/-y6J28I5z0c)  
              Added [0-q1KafFCLU](https://youtu.be/0-q1KafFCLU)  
              Added [03OvXCvtV0E](https://youtu.be/03OvXCvtV0E)  
              Added [04tYkKUPPv4](https://youtu.be/04tYkKUPPv4)  
              Added [0AFBaZp1wPU](https://youtu.be/0AFBaZp1wPU)  
              Added [0BZLxBp4wgU](https://youtu.be/0BZLxBp4wgU)  
              Added [0GeCPanRHU0](https://youtu.be/0GeCPanRHU0)  
              Added [0KPJE-NiwWs](https://youtu.be/0KPJE-NiwWs)  
              Added [0Q5Ra4suNPE](https://youtu.be/0Q5Ra4suNPE)  
              Added [0SqwzA0FVTU](https://youtu.be/0SqwzA0FVTU)  
              Added [0T9guE-LyHg](https://youtu.be/0T9guE-LyHg)  
              Added [0_1MxVlid7s](https://youtu.be/0_1MxVlid7s)  
              Added [0cOAUSVBGX8](https://youtu.be/0cOAUSVBGX8)  
              Added [0eEWxzW2wJU](https://youtu.be/0eEWxzW2wJU)  
              Added [0imk0ByfgoM](https://youtu.be/0imk0ByfgoM)  
              Added [0ivQwwgW4OY](https://youtu.be/0ivQwwgW4OY)  
              Added [0kA9HQmHm9c](https://youtu.be/0kA9HQmHm9c)  
              Added [0l-qw9yRFOA](https://youtu.be/0l-qw9yRFOA)  
              Added [0t2tjNqGyJI](https://youtu.be/0t2tjNqGyJI)  
              Added [0wAEZouvlj8](https://youtu.be/0wAEZouvlj8)  
              Added [1-xGerv5FOk](https://youtu.be/1-xGerv5FOk)  
            

Final Product

Please enjoy this demo of the program in action.

Here's the full source code.


            use std::{
                collections::HashSet,
                fs::OpenOptions,
                io::{BufWriter, Read, Write},
                path::{Path, PathBuf},
            };
            
            use clap::Parser;
            use git2::{
                Delta::{Added, Deleted},
                Repository, Time,
            };
            use serde_json::Deserializer;
            
            #[derive(Parser, Debug)]
            #[command(version, about, long_about = None)]
            struct Args {
                /// Path to the songs-backup git repository
                directory: PathBuf,
            
                /// Overwrite the output file
                #[arg(short, long)]
                force: bool,
            }
            
            fn main() -> std::io::Result<()> {
                let args = Args::parse();
            
                let repo = match Repository::open(args.directory) {
                    Ok(repo) => repo,
                    Err(e) => panic!("failed to open: {}", e),
                };
            
                let mut revwalk = repo.revwalk().unwrap();
                revwalk.push_head().unwrap();
            
                let file = match OpenOptions::new()
                    .write(true)
                    .truncate(true)
                    .create_new(!args.force)
                    .create(args.force)
                    .open("output.txt")
                {
                    Ok(f) => f,
                    Err(e) => match e.kind() {
                        std::io::ErrorKind::AlreadyExists => {
                            panic!("Output file output.txt already exists. Use -f, --force to force overwriting the destination");
                        }
                        _ => panic!("failed to open file: {}", e),
                    },
                };
                let mut writer = BufWriter::new(file);
                writeln!(writer, "# songs-history")?;
            
                let current_ids = get_current_ids(&repo).unwrap();
            
                let mut already_added: HashSet<String> = HashSet::new();
            
                for commit in revwalk.collect::<Vec<_>>().iter().rev() {
                    let commit = repo.find_commit(*commit.as_ref().unwrap()).unwrap();
                    let parent = match commit.parent(0) {
                        Ok(parent) => parent,
                        Err(_) => continue,
                    };
            
                    let diff = repo
                        .diff_tree_to_tree(
                            Some(&parent.tree().unwrap()),
                            Some(&commit.tree().unwrap()),
                            None,
                        )
                        .unwrap();
            
                    let mut added: Vec<String> = Vec::new();
                    let mut deleted: Vec<String> = Vec::new();
            
                    for delta in diff.deltas() {
                        if !matches!(delta.status(), Added | Deleted) {
                            continue;
                        }
                        let new_file = delta.new_file();
                        let path = new_file.path().unwrap();
                        if !path.starts_with("output/songs") {
                            continue;
                        }
                        let video = path.file_stem().unwrap().to_string_lossy();
                        match delta.status() {
                            Added => {
                                if already_added.contains(&video.to_string()) {
                                    continue;
                                }
                                already_added.insert(video.to_string());
                                added.push(video.to_string());
                            }
                            Deleted => {
                                if current_ids.contains(&video.to_string()) {
                                    continue;
                                }
                                deleted.push(video.to_string());
                            }
                            _ => {}
                        }
                    }
            
                    if added.len() + deleted.len() == 0 {
                        continue;
                    }
            
                    writeln!(writer, "## {}", format_time(&commit.time()))?;
                    for video in added {
                        writeln!(writer, "Added {}  ", format_video(&video))?;
                    }
                    for video in deleted {
                        writeln!(writer, "Removed {}  ", format_video(&video))?;
                    }
                }
            
                println!("Wrote to output.txt");
                Ok(())
            }
            
            fn format_time(time: &Time) -> String {
                let (offset, sign) = match time.offset_minutes() {
                    n if n < 0 => (-n, '-'),
                    n => (n, '+'),
                };
                let (hours, minutes) = (offset / 60, offset % 60);
                let ts = time::Timespec::new(time.seconds() + (time.offset_minutes() as i64) * 60, 0);
                let time = time::at(ts);
            
                format!(
                    "{} {}{:02}{:02}",
                    time.strftime("%a %b %e %T %Y").unwrap(),
                    sign,
                    hours,
                    minutes
                )
            }
            
            fn get_current_ids(repo: &Repository) -> Result<HashSet<String>, git2::Error> {
                let obj = repo
                    .head()?
                    .peel_to_tree()?
                    .get_path(Path::new("output/summary.json"))?
                    .to_object(&repo)?
                    .peel_to_blob()?;
                let mut file_content = String::new();
                obj.content().read_to_string(&mut file_content).unwrap();
            
                let mut ids: HashSet<String> = HashSet::new();
            
                let stream = Deserializer::from_str(&file_content).into_iter::<serde_json::Value>();
            
                for value in stream {
                    if let Ok(obj) = value {
                        if let Some(items) = obj.get("items") {
                            if let Some(items_array) = items.as_array() {
                                for item in items_array {
                                    let id = item["id"].as_str().unwrap();
                                    ids.insert(id.to_string());
                                }
                            }
                        }
                    }
                }
            
                Ok(ids)
            }
            
            fn format_video(video: &str) -> String {
                format!("[{}](https://youtu.be/{})", video, video)
            }            
            

Publishing

I posted it to a GitHub repository, realcyguy/songs-history.

Reflection

I'm happy with the result because I created something that I was missing and I learned Rust. Specifically, my understanding has improved the most in these areas:

  • Rust syntax
  • Ownership and borrowing
  • Pattern matching
  • Macros
  • Git commits

Acknowledgment

Thank you!