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-- Global state 11local verbose = 0 12 13-- Execute a command and return its output. A final newline is stripped, 14-- similar to sh. 15local function exec_command(command) 16 local handle = assert(io.popen(command)) 17 local output = handle:read("a") 18 handle:close() 19 if output:sub(-1) == "\n" then 20 return output:sub(1, -2) 21 end 22 return output 23end 24 25-- Return a table of cherry-pick (MFC) candidates. 26local function read_from(from_branch, to_branch, author, dirspec) 27 local command = "git rev-list --first-parent --reverse " 28 command = command .. to_branch .. ".." .. from_branch 29 if #author > 0 then 30 command = command .. " --committer \\<" .. author .. "@" 31 end 32 if dirspec then 33 command = command .. " " .. dirspec 34 end 35 if verbose > 1 then 36 print("Obtaining MFC-from commits using command:") 37 print(command) 38 end 39 local handle = assert(io.popen(command)) 40 local content = {} 41 for line in handle:lines() do 42 table.insert(content, line) 43 end 44 handle:close() 45 return content 46end 47 48-- Return a table of original hashes of changes that have already been 49-- cherry-picked (MFC'd). 50local function read_to(from_branch, to_branch, dirspec) 51 local command = "git log " .. from_branch .. ".." .. to_branch 52 command = command .. " --grep 'cherry picked from'" 53 if dirspec then 54 command = command .. " " .. dirspec 55 end 56 if verbose > 1 then 57 print("Obtaining MFC-to commits using command:") 58 print(command) 59 end 60 local handle = assert(io.popen(command)) 61 local content = {} 62 for line in handle:lines() do 63 local hash = line:match("%(cherry picked from commit ([0-9a-f]+)%)") 64 if hash then 65 table.insert(content, hash) 66 end 67 end 68 handle:close() 69 return content 70end 71 72-- Read a commit exclude file and return its content as a table. Comments 73-- starting with # and text after a hash is ignored. 74local function read_exclude(filename) 75 local file = assert(io.open(filename, "r")) 76 local content = {} 77 for line in file:lines() do 78 local hash = line:match("^%x+") 79 if hash then 80 -- Hashes are 40 chars; if less, expand short hash. 81 if #hash < 40 then 82 hash = exec_command( 83 "git rev-parse " .. hash) 84 end 85 table.insert(content, hash) 86 end 87 end 88 file:close() 89 return content 90end 91 92--- Remove hashes from 'set1' list that are present in 'set2' list 93local function set_difference(set1, set2) 94 local set2_values = {} 95 for _, value in ipairs(set2) do 96 set2_values[value] = true 97 end 98 99 local result = {} 100 for _, value in ipairs(set1) do 101 if not set2_values[value] then 102 table.insert(result, value) 103 end 104 end 105 return result 106end 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 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 local 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