Làm Thế Nào Để Đếm Được Line of Code của 10 000 Pull Requests ?

Chào các bạn 😄,

Sau chuỗi series về lập trình Ruby on Rails dài dòng của mình không biết có bạn nào đã follow được hết từ phần một đến phần phần 8 không nhỉ, không biết trình của các chú về lập trình Ruby on Rails bây giờ đến đâu rồi, đã có thể hiểu và tự build được các dự án của mình chưa :v.

Nếu có ai đó đã follow theo series của mình và đã đạt được thành quả nhất định, thì comment bên dưới cho mình biết phát nhé =]].

Chém gió như thế đủ rồi nhỉ =]], hôm nay mình sẽ đổi không khí chút không chơi với Ruby on Rails khô khan nữa, mà sẽ chuyển qua dùng Ruby để giải quyết vấn đề nhỏ phát sinh trong dự án của mình, đúng như tiêu đề : "Làm Thế Nào Để Đếm Được Line of Code của 10 000 Pull Requests ?"

1. Vấn đề đặt ra

  • Nếu project của bạn là một project public thì việc đếm số line of code sẽ trở nên rất đơn giản, khi thằng Github đã cung cấp sắn cho chúng ta API để có thể thực hiện này một cách dễ dàng. Các bạn có thể tìm hiểu thêm ở đây : https://developer.github.com/v3/repos/statistics/

  • Tuy nhiên nếu project của bạn là một project private thì nó lại khác, lần trước mình có tìm hiểu để xem có thể dùng API statistics của Github không, thì tìm một hồi không thấy ra kết quả đâu (sad4), và lúc đấy dự án đang cần gấp nên nghĩ sang phương án khác, vì tịt với API statistics của Github với project private rồi 😐

Thế là nghĩ sang phương án cứ ở mỗi cái Pull Request gửi lên Github lúc view detail, các bạn để ý nó sẽ show ra số Line of Addition và Line of Deletion như ở bên dưới:

Ví dụ đối với PR của gem devise này : https://github.com/plataformatec/devise/pull/4737/files

Ta có:

  • Line of Addition: +3
  • Line of Deletion: -1

OK như thế là Github đã giúp ta thống kê được số line of code của từng PR rồi, giờ việc của chúng ta là đếm số line of code của 10 000 Pull Request Thật ra con số 10 000 PR chỉ là ví dụ thôi mình ghi cho nó hấp dẫn, chứ thực tế các bạn sẽ phải đếm số PR theo một điều kiện nào đó, ví dụ như: trong một tháng, trong một năm, trong khoảng thời gian X -> Y,... chắc cũng không tới nổi 10k đâu =]]

Vậy chúng ta sẽ phải ngồi mở từng PR ra và đếm từng dòng, note lại số liệu sau đó ngồi cộng chúng lại à ? -> Câu trả lời là không. Đối với công việc có tính lặp đi lặp lại và nhàm chán thế này, thì chắc các bạn sẽ nghĩ tới cái tool tự động nào đấy sẽ giúp chúng ta làm việc này ? Chính xác rồi đấy, chẳng có ai rỗi hơi ngồi đếm cái đống trên cả.

Tool tự động để đếm cái này không biết là có không mình chưa search thử nhưng chắc là không có vì yêu cầu của việc này nó cụ thể qúa, nên thôi chắc tự viết lấy một cái 😄

2. Công cụ support mình nghĩ tới là : Selenium

Một thư viện hỗ trợ rất phổ biến trong việc test tự động các Web Application. Nó hỗ trợ hầu hết các ngôn ngữ phổ biến nhất hiện nay: Java, C#, Ruby, Python, Javascript(Nodejs)

Hay quá có hỗ trợ cho Ruby luôn, vậy dùng luôn Ruby để viết. Quá tiện nhỉ (lol)

OK, vậy chung ta đã có yêu cầu và thư viện hỗ trợ, bắt tay vào làm nhỉ.

3. Tiến hành

3.1 Cài đặt Selenium

3.1.1 Cài đặt gem selenium-webdriver

Đơn giản như cài đặt các gem khác, các bạn chỉ cần chạy lệnh bên dưới

gem install selenium-webdriver

Để thằng selenium-webdriver của chúng ta có thể giao tiếp được với các browser các bạn cần cài đặt Third Party Browser Drivers, tùy vài Browser mà các bạn dự định sẽ dùng mà chúng ta sẽ download các Browser Drivers cho phù hợp.

Để download các bạn vào trang chủ của nó:

http://www.seleniumhq.org/download/

Phần Third Party Drivers, Bindings, and Plugins chọn phần Browser Drivers muốn download.

Trong bài viết này mình sẽ sử dụng FireFox, do đó mình sẽ download thằng Mozilla GeckoDriver Click chọn version: 0.19.0 để download cho phiên bản Ubuntu, nếu bạn nào dùng Windows hoặc OS khác thì cần download phiên bản tương ứng.

Ở đây mình sẽ chọn down phiên bản: geckodriver-v0.19.1-linux64.tar.gz cho Ubuntu

Sau khi download xong các bạn cần giải nén nó ra và sẽ được file geckodriver

Để thằng geckodriver có thể chạy được các bạn cần move nó vào thư mục /usr/local/bin

sudo mv geckodriver /usr/local/bin

3.1.2 Thư viện Selenium cho Ruby

Tất tần tật về Example và API doc của selenium-webdriver ở đây: https://github.com/SeleniumHQ/selenium/wiki/Ruby-Bindings

Các bạn cần method hay example gì thì vào đây để search nhé, bên dưới là mình sử dụng lại các method mà nó đã cung cấp ở trên thôi.

Ngoài ra mình còn tìm thêm được 1 trang các example về các method rất rõ ràng ở đây: https://gist.github.com/huangzhichong/3284966#file-selenium-webdriver-cheatsheet-md

Các bạn có thể tham khảo

3.2 Login với Github

Đầu tiên ta cần tạo một class để chạy code. Ở đây mình sẽ đặt tên là SeleniumTest Ta sẽ có file khởi tạo như sau:

Khởi tạo để kết nối với selenium-webdriver:

attr_reader :driver

def initialize
  @driver = Selenium::WebDriver.for :firefox
  @driver.navigate.to "https://github.com/login"
end

Như này đã, giờ chạy thử xem nó làm được gì nào:

ruby selenium_test.rb

nó sẽ tự động mở FireFox cho chúng ta và truy cập vào trang https://github.com/login Bước đầu có vẻ ngon lành nhỉ 😄

Login function

Vì là đối với các Project Private chúng ta cần phải đăng nhập vào Github rồi mới truy cập vào được. Nê tiếp theo chúng ta sẽ viết lệnh login vào Github tự động nhé :v

Chúng ta đã đến được trang login của Github https://github.com/login Giờ để login thì cần thằng SeleniumTest của chúng ta thực hiện các bước bên dưới

B1: điền email addresspassword vào hai ô tương ứng B2: nhấn button Sign in

Để tương tác được với các item trên Web browser đầu tiên chúng ta cần lấy được điều khiển của chúng hay nôm na là chúng ta cần điều khiển được item đó bằng lệnh.

Selenium-webdriver cung cấp cho chúng ta method find_element để làm việc này. Chi tiết các bạn có thể xem thêm ở 3.1.2 Thư viện Selenium cho Ruby ở trên http://seleniumhq.github.io/selenium/docs/api/rb/Selenium/WebDriver/SearchContext.html

Ở đây vì là form submit nên mình sẽ dùng find_element name:

Cách để tìm thuộc tính name của item rất đơn giản. Với FireFox chỉ cần đưa chuột lên đối tượng cần xem, click phải chọn Inspect Element(Q) nó sẽ hiện ra thông tin thẻ HTML của đối tượng đấy, và các bạn chỉ cần quan tâm đến thuộc tính name="xxx" là đủ đấy chính là giá trị của attribute name là chúng ta cần tìm.

Đối với element email address của trang https://github.com/login thì nó có giá trị name="login"

Tương tự với các thằng text field Password và button Sign in

private
def login_github
  email = driver.find_element name: "login"
  email.send_keys "<your email>"

  password = driver.find_element name: "password"
  password.send_keys "<your password>"

  submit = driver.find_element name: "commit"
  submit.click
end

Ta sẽ có function login_github như này

cho vào hàm execute rồi chạy thử lại phát nào, nhớ là điền thông tin để đăng nhập vào nhé

def execute
  login_github
end   

Sau khi chạy nó sẽ login luôn vào cho chúng ta và được như này

3.3 List các Pull Request

Bây giờ chúng ta sẽ list tất cả các Pull Request mà cần đếm line of code ra. Trong bài hướng dẫn này mình sẽ lấy tạm Repository của thằng Devise để list Pull Request nhé, ta cứ xem nó như là project private đi =]]

link repo gem devise: https://github.com/plataformatec/devise

Điều kiện để list của chúng ta là: sẽ phải list tất cả PR trên devise

  • được tạo từ 01/01/2017 -> 31/12/2017
  • đã được merge
  • không chứa từ "Merge" hoặc "Release" trong title

Ta sẽ có query trên github như này: is:pr is:merged NOT Merge in:title OR NOT Release in:title created:2017-01-01..2017-12-31

Để tìm hiểu về cách viết query nâng cao trên Github: các bạn có thể tham khảo thêm ở đây https://help.github.com/articles/searching-issues-and-pull-requests/

Mình nghĩ đây cũng là một kỹ năng nên có, khi muốn query một vài cái phức tạp =]]

OK bỏ query vào test thử thì ta thấy thằng devise có tổng cộng 34 cái PR, ít hơn con số 10k nhiều lần nhỉ =]], mà thôi vì đã chạy Automation nên con số không quan trọng lắm đâu (yaoming)

Giờ chúng ta sẽ code để con SeleniumTest của chúng ta filter được ra như trên:

attr_reader :driver, :repository_path, :pull_requests

def initialize
  @driver = Selenium::WebDriver.for :firefox
  @driver.navigate.to "https://github.com/login"
  @repository_path = "https://github.com/plataformatec/devise/pulls"
  @pull_requests = []
end

Hàm filters_pull_request_by_query : filter pull request theo query của chúng ta ở trên Hàm get_list_pull_requests : get list pull request từ query ra được

def get_list_pull_requests repository_path
  driver.get repository_path
  filters_by_months = [
    "is:pr is:merged NOT Merge in:title OR NOT Release in:title created:2017-01-01..2017-12-31"]
  filters_by_months.each do |month|
    filters_pull_request_by_query month
    sleep 5
    button_next = driver.find_element class: "next_page"
    while button_next
      link_pull_requests = driver.find_elements :xpath, "//a[@class='link-gray-dark no-underline h4 js-navigation-open']"

      puts link_pull_requests.size

      link_pull_requests.each do |link_pull_request|
        pull_requests << link_pull_request.attribute("href")
        puts link_pull_request.attribute("href")
      end
      break if button_next.tag_name == "span"
      button_next.click
      sleep 2
      button_next = driver.find_element class: "next_page"
    end
  end
  puts "\n"
  puts pull_requests
  puts pull_requests.size
end

def filters_pull_request_by_query query
  filter_element = driver.find_element id: "js-issues-search"
  filter_element.clear
  filter_element.send_keys query
  filter_element.submit
end
def execute
  login_github
  get_list_pull_requests repository_path
end

Kết quả chúng ta sẽ được như này

3.4 Đếm line of code Pull Request

Như vậy là chúng ta đã có được list Pull Request cần đếm line of code, giờ chỉ việc code để con SeleniumTest của chúng ta count được là ok

attr_reader :driver, :repository_path, :pull_requests, :wait, :data_export_to_csv

def initialize
  ...
  @wait = Selenium::WebDriver::Wait.new timeout: 10
  @data_export_to_csv = []
end
def count_total_additional
  total_additional = 0
  total_deletion = 0
  pull_requests.each do |pull_request|
    driver.get "#{pull_request}/files"
    wait.until {driver.find_element :xpath, '//*[@id="files_bucket"]/div[3]/div/span/span[1]'}
    additional_element = driver.find_element :xpath, '//*[@id="files_bucket"]/div[3]/div/span/span[1]'
    deletion_element = driver.find_element :xpath, '//*[@id="files_bucket"]/div[3]/div/span/span[2]'
    additional = additional_element.text.gsub(",", "").to_i
    deletion = deletion_element.text.gsub("−", "-").to_i
    data_export_to_csv << [pull_request, additional, deletion]
    total_additional += additional
    total_deletion += deletion
    puts "#{pull_request}  Line code +: #{additional}  Line code -: #{deletion}"
  end
  puts "total_additional : #{total_additional}"
  puts "total_deletion : #{total_deletion}"
end
def execute
  login_github
  get_list_pull_requests repository_path
  count_total_additional
end

Ta sẽ được kết quả như này Có vẻ là ok rồi nhỉ, tuy nhiên dữ liệu mà nằm trên terminal thì nhìn nó chuối quá, tính toán lại mất công, ta cho nó ra file csv cho nó chuyên nghiệp :v

3.5 Xuất file CSV

def execute
  login_github
  get_list_pull_requests repository_path
  count_total_additional
  export_data_to_csv "/home/framgia/Documents/Framgia/tai_lieu/Viblo/result/result.csv"
end
def export_data_to_csv file_name
  CSV.open(file_name, "wb") do |csv|
    csv << csv_header
    data_export_to_csv.each do |data|
      csv << data
    end
  end

  puts "export csv success !"
end

chạy lại phát và xem kết quả nào :v

Sau khi nó chạy xong thì ta có file như này, có vẻ đáp ứng được yêu cầu đưa ra rồi, giờ muốn nó chạy bao nhiêu PR thì chỉ việc nhấn lệnh và ngồi chơi xơi nước thôi :v

Full source code: https://github.com/duc11t3bk/tool_get_pull_request

Hôm nay đến đây thôi, chào các bạn và hẹn gặp vào kì tới :v

Nguồn:


All Rights Reserved