1#!/usr/libexec/flua 2 3-- SPDX-License-Identifier: BSD-2-Clause 4-- Copyright 2024 The FreeBSD Foundation 5 6-- MFC candidate search utility. Identify hashes that exist only in the 7-- "MFC from" branch and do not have a corresponding "cherry picked from" 8-- commit in the "MFC to" branch. 9 10-- Execute a command and return its output. A final newline is stripped, 11-- similar to sh. 12local function exec_command(command) 13 local handle = assert(io.popen(command)) 14 local output = handle:read("a") 15 handle:close() 16 if output:sub(-1) == "\n" then 17 return output:sub(1, -2) 18 end 19 return output 20end 21 22-- Return a table of cherry-pick (MFC) candidates. 23local function read_from(from_branch, to_branch, author, dirspec) 24 local command = "git rev-list --first-parent --reverse " 25 command = command .. to_branch .. ".." .. from_branch 26 if #author > 0 then 27 command = command .. " --committer \\<" .. author .. "@" 28 end 29 if dirspec then 30 command = command .. " " .. dirspec 31 end 32 if verbose > 1 then 33 print("Obtaining MFC-from commits using command:") 34 print(command) 35 end 36 local handle = assert(io.popen(command)) 37 local content = {} 38 for line in handle:lines() do 39 table.insert(content, line) 40 end 41 handle:close() 42 return content 43end 44 45-- Return a table of original hashes of changes that have already been 46-- cherry-picked (MFC'd). 47local function read_to(from_branch, to_branch, dirspec) 48 local command = "git log " .. from_branch .. ".." .. to_branch 49 command = command .. " --grep 'cherry picked from'" 50 if dirspec then 51 command = command .. " " .. dirspec 52 end 53 if verbose > 1 then 54 print("Obtaining MFC-to commits using command:") 55 print(command) 56 end 57 local handle = assert(io.popen(command)) 58 local content = {} 59 for line in handle:lines() do 60 local hash = line:match("%(cherry picked from commit ([0-9a-f]+)%)") 61 if hash then 62 table.insert(content, hash) 63 end 64 end 65 handle:close() 66 return content 67end 68 69-- Read a commit exclude file and return its content as a table. Comments 70-- starting with # and text after a hash is ignored. 71local function read_exclude(filename) 72 local file = assert(io.open(filename, "r")) 73 local content = {} 74 for line in file:lines() do 75 local hash = line:match("^%x+") 76 if hash then 77 -- Hashes are 40 chars; if less, expand short hash. 78 if #hash < 40 then 79 hash = exec_command( 80 "git rev-parse " .. hash) 81 end 82 table.insert(content, hash) 83 end 84 end 85 file:close() 86 return content 87end 88 89--- Remove hashes from 'set1' list that are present in 'set2' list 90local function set_difference(set1, set2) 91 local set2_values = {} 92 for _, value in ipairs(set2) do 93 set2_values[value] = true 94 end 95 96 local result = {} 97 for _, value in ipairs(set1) do 98 if not set2_values[value] then 99 table.insert(result, value) 100 end 101 end 102 return result 103end 104 105-- Global state 106verbose = 0 107 108local function params(from_branch, to_branch, author) 109 print("from: " .. from_branch) 110 print("to: " .. to_branch) 111 if #author > 0 then 112 print("author/committer: " .. author) 113 else 114 print("author/committer: <all>") 115 end 116end 117 118local function usage(from_branch, to_branch, author) 119 local script_name = arg[0]:match("([^/]+)$") 120 print(script_name .. " [-ah] [-f from_branch] [-t to_branch] [-u user] [-X exclude_file] [path ...]") 121 print() 122 params(from_branch, to_branch, author) 123end 124 125-- Main function 126local function main() 127 local from_branch = "freebsd/main" 128 local to_branch = "" 129 local author = os.getenv("USER") or "" 130 local dirspec = nil 131 132 local url = exec_command("git remote get-url freebsd") 133 local freebsd_repo = string.match(url, "[^/]+$") 134 freebsd_repo = string.gsub(freebsd_repo, ".git$", "") 135 if freebsd_repo == "ports" or freebsd_repo == "freebsd-ports" then 136 local year = os.date("%Y") 137 local month = os.date("%m") 138 local qtr = math.ceil(month / 3) 139 to_branch = "freebsd/" .. year .. "Q" .. qtr 140 elseif freebsd_repo == "src" or freebsd_repo == "freebsd-src" then 141 to_branch = "freebsd/stable/14" 142 -- If pwd is a stable or release branch tree, default to it. 143 local cur_branch = exec_command("git symbolic-ref --short HEAD") 144 if string.match(cur_branch, "^stable/") then 145 to_branch = cur_branch 146 elseif string.match(cur_branch, "^releng/") then 147 to_branch = cur_branch 148 local major = string.match(cur_branch, "%d+") 149 from_branch = "freebsd/stable/" .. major 150 end 151 else 152 print("pwd is not under a ports or src repository.") 153 return 154 end 155 156 local do_help = false 157 local exclude_file = nil 158 local i = 1 159 while i <= #arg and arg[i] do 160 local opt = arg[i] 161 if opt == "-a" then 162 author = "" 163 elseif opt == "-f" then 164 from_branch = arg[i + 1] 165 i = i + 1 166 elseif opt == "-h" then 167 do_help = true 168 i = i + 1 169 elseif opt == "-t" then 170 to_branch = arg[i + 1] 171 i = i + 1 172 elseif opt == "-u" then 173 author = arg[i + 1] 174 i = i + 1 175 elseif opt == "-v" then 176 verbose = verbose + 1 177 elseif opt == "-X" then 178 exclude_file = arg[i + 1] 179 i = i + 1 180 else 181 break 182 end 183 i = i + 1 184 end 185 186 if do_help then 187 usage(from_branch, to_branch, author) 188 return 189 end 190 191 if arg[i] then 192 dirspec = arg[i] 193 --print("dirspec = " .. dirspec) 194 -- XXX handle multiple dirspecs? 195 end 196 197 if verbose > 0 then 198 params(from_branch, to_branch, author) 199 end 200 201 local from_hashes = read_from(from_branch, to_branch, author, dirspec) 202 local to_hashes = read_to(from_branch, to_branch, dirspec) 203 204 local result_hashes = set_difference(from_hashes, to_hashes) 205 206 if exclude_file then 207 exclude_hashes = read_exclude(exclude_file) 208 result_hashes = set_difference(result_hashes, exclude_hashes) 209 end 210 211 -- Print the result 212 for _, hash in ipairs(result_hashes) do 213 print(exec_command("git show --pretty='%h %s' --no-patch " .. hash)) 214 end 215end 216 217main() 218