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 git-show-fmt] [-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 2>/dev/null") 133 local freebsd_repo 134 if url and url ~= "" then 135 freebsd_repo = string.match(url, "[^/]+$") 136 freebsd_repo = string.gsub(freebsd_repo, "%.git$", "") 137 end 138 if freebsd_repo == "ports" or freebsd_repo == "freebsd-ports" then 139 local year = os.date("%Y") 140 local month = os.date("%m") 141 local qtr = math.ceil(month / 3) 142 to_branch = "freebsd/" .. year .. "Q" .. qtr 143 elseif freebsd_repo == "src" or freebsd_repo == "freebsd-src" then 144 -- If pwd is a stable or release branch tree, default to it. 145 local cur_branch = exec_command("git symbolic-ref --short HEAD") 146 if string.match(cur_branch, "^stable/") then 147 to_branch = cur_branch 148 elseif string.match(cur_branch, "^releng/") then 149 to_branch = cur_branch 150 local major = string.match(cur_branch, "%d+") 151 from_branch = "freebsd/stable/" .. major 152 else 153 -- Use latest stable branch. 154 to_branch = exec_command("git for-each-ref --sort=-v:refname " .. 155 "--format='%(refname:lstrip=2)' " .. 156 "refs/remotes/freebsd/stable/* --count=1") 157 end 158 else 159 print("pwd is not under a ports or src repository.") 160 return 161 end 162 163 local do_help = false 164 local exclude_file = nil 165 local gitshowfmt = '%h %s' 166 local i = 1 167 while i <= #arg and arg[i] do 168 local opt = arg[i] 169 if opt == "-a" then 170 author = "" 171 elseif opt == "-f" then 172 from_branch = arg[i + 1] 173 i = i + 1 174 elseif opt == "-h" then 175 do_help = true 176 i = i + 1 177 elseif opt == "-t" then 178 to_branch = arg[i + 1] 179 i = i + 1 180 elseif opt == "-u" then 181 author = arg[i + 1] 182 i = i + 1 183 elseif opt == "-v" then 184 verbose = verbose + 1 185 elseif opt == "-F" then 186 gitshowfmt = arg[i + 1] 187 i = i + 1 188 elseif opt == "-X" then 189 exclude_file = arg[i + 1] 190 i = i + 1 191 else 192 break 193 end 194 i = i + 1 195 end 196 197 if do_help then 198 usage(from_branch, to_branch, author) 199 return 200 end 201 202 if arg[i] then 203 dirspec = arg[i] 204 --print("dirspec = " .. dirspec) 205 -- XXX handle multiple dirspecs? 206 end 207 208 if verbose > 0 then 209 params(from_branch, to_branch, author) 210 end 211 212 local from_hashes = read_from(from_branch, to_branch, author, dirspec) 213 local to_hashes = read_to(from_branch, to_branch, dirspec) 214 215 local result_hashes = set_difference(from_hashes, to_hashes) 216 217 if exclude_file then 218 exclude_hashes = read_exclude(exclude_file) 219 result_hashes = set_difference(result_hashes, exclude_hashes) 220 end 221 222 -- Print the result 223 for _, hash in ipairs(result_hashes) do 224 print(exec_command("git show --pretty='" .. gitshowfmt .. "' --no-patch " .. hash)) 225 end 226end 227 228main() 229