summaryrefslogtreecommitdiff
path: root/src/main/lua/git/GitflowChangelogGen.lua
blob: 3b44ac3b4e8fb428c0c72416fae5d5cc9fb2ba10 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195

local log = io.stderr
local main


function printHelp()
    io.stdout:write("  \n"
        .."  Helper to extract essential data from a gitflow log which potentially\n"
        .."  is useful to write a CHANGELOG from.\n"
        .."  \n"
        .."  Options:\n"
        .."  \n"
        .."    --since <date>\n"
        .."        Ignore commits with this ISO date and older.\n"
        .."  \n"
        .."    --remote <str>\n"
        .."      Name of the git remote to use. Defaults to 'upstream'.\n"
        .."  \n"
        .."    --no-fetch\n"
        .."      Do NOT update refs from remote. Just use what we have local.\n"
        .."  \n"
        )
end


function parseArgs( app )
    local iA = 0
    while true do iA = iA + 1
        local arg = _ENV.arg[iA]
        if not arg then
            break
        elseif arg == "--since" then
            iA = iA + 1; arg = _ENV.arg[iA]
            if not arg then log:write("EINVAL: --since needs value\n")return end
            app.since = arg
        elseif arg == "--remote" then
            iA = iA + 1; arg = _ENV.arg[iA]
            if not arg then log:write("EINVAL: --remote needs value\n")return end
            app.remoteName = arg
        elseif arg == "--no-fetch" then
            app.isFetch = false
        elseif arg == "--help" then
            app.isHelp = true; return 0
        else
            log:write("EINVAL: ".. arg .."\n")return
        end
    end
    if not app.since then log:write("EINVAL: --since missing\n")return end
    if not app.remoteName then app.remoteName = "upstream" end
    return 0
end


function readCommitHdr( app )
    --log:write("[DEBUG] parse hdr from '".. app.fullHistory:sub(app.fullHistoryRdBeg, app.fullHistoryRdBeg+256) .."...'\n")
    local f, t = app.fullHistory:find("^"
        .."commit ........................................[^\n]*\n"
        .."Merge: [0-9a-z]+ [0-9a-z]+\n"
        .."Author: [^\n]+\n"
        .."Date:   [^\n]+\n"
        .."\n"
        , app.fullHistoryRdBeg)
    if not f then f, t = app.fullHistory:find("^"
        .."commit ........................................[^\n]*\n"
        .."Author: [^\n]+\n"
        .."Date:   [^\n]+\n"
        .."\n"
        , app.fullHistoryRdBeg) end
    if not f then
        assert(app.fullHistory:len() == app.fullHistoryRdBeg-1, app.fullHistory:len()..", "..app.fullHistoryRdBeg)
        app.parseFn = false
        return
    end
    app.commitHdr = assert(app.fullHistory:sub(f, t-1))
    --log:write("hdrBeginsWith '"..(app.commitHdr:sub(1, 32)).."...'\n")
    app.fullHistoryRdBeg = t + 1
    --log:write("hdr parsed. rdCursr now points to '".. app.fullHistory:sub(app.fullHistoryRdBeg, app.fullHistoryRdBeg+16) .."...'\n")
    app.parseFn = assert(readCommitMsg)
end


function readCommitMsg( app )
    local idxOfC = app.fullHistoryRdBeg
    local chrPrev = false
    while true do idxOfC = idxOfC + 1
        local chr = app.fullHistory:byte(idxOfC)
        --log:write("CHR '"..tostring(app.fullHistory:sub(idxOfC, idxOfC)).."'\n")
        if (chr == 0x63) and chrPrev == 0x0A then
            idxOfC = idxOfC - 1
            break -- LF followed by 'c' (aka 'commit') found
        elseif not chr then
            idxOfC = idxOfC - 1
            break
        else
            chrPrev = assert(chr)
        end
    end
    local mtch = app.fullHistory:sub(app.fullHistoryRdBeg, idxOfC - 1)
    assert(mtch)
    while mtch:byte(mtch:len()) == 0x0A do mtch = mtch:sub(1, -2) end
    mtch = mtch:gsub("\n    ", "\n"):gsub("^    ", "")
    app.commitMsg = mtch
    app.fullHistoryRdBeg = idxOfC + 1
    app.parseFn = readCommitHdr
    --log:write("msg parsed. rdCursr now points to '".. app.fullHistory:sub(app.fullHistoryRdBeg, app.fullHistoryRdBeg+16) .."...'\n")
    table.insert(app.commits, {
        hdr = assert(app.commitHdr),
        msg = assert(app.commitMsg),
    })
end


function run( app )
    local snk = io.stdout
    if app.isFetch then
        -- Make sure refs are up-to-date
        local gitFetch = "git fetch \"".. app.remoteName .."\""
        log:write("[DEBUG] ".. gitFetch .."\n")
        local gitFetch = io.popen(gitFetch)
        while true do
            local buf = gitFetch:read(1<<16)
            if not buf then break end
            log:write(buf)
        end
    end
    -- Collect input
    local git = "git log --date-order --first-parent --decorate --since \"".. app.since.."\""
        .." \"".. app.remoteName .."/master\""
        .." \"".. app.remoteName .."/develop\""
    log:write("[DEBUG] ".. git .."\n")
    local git = io.popen(git)
    while true do
        local buf = git:read(1<<16)
        if not buf then break end
        --io.stdout:write(buf)
        table.insert(app.fullHistory, buf)
    end
    -- Parse raw commits
    app.fullHistory = table.concat(app.fullHistory)
    app.parseFn = assert(readCommitHdr)
    while app.parseFn do app.parseFn(app) end
    -- Prepare output
    local prevDate = "0000-00-00"
    local version, prevVersion = "v_._._", false
    local dateEntry = false
    local entries = {}
    for k, v in ipairs(app.commits) do
        local date = assert(v.hdr:match("\nDate: +([0-9-]+) "))
        local author = assert(v.hdr:match("\nAuthor: +([^\n]+)\n"))
        local prNr, short = v.msg:match("Pull request #(%d+): ([^\n]+)\n")
        prevVersion = version
        _, version = v.hdr:match("^([^\n]+)\n"):match("tag: ([a-z]+)-([^,]+)[,)]")
        if not version then version = prevVersion end

        if version ~= prevVersion or not dateEntry then
            if dateEntry then table.insert(entries, dateEntry) end
            dateEntry = {
                txt = date .." - ".. version .."\n\nResolved issues:\n\n"
            }
            prevDate = date
        end
        if prNr then
            dateEntry.txt = dateEntry.txt .. short .." (PR ".. prNr ..")\n"
        else
            dateEntry.txt = dateEntry.txt .. v.msg .."\n"
        end
    end
    if dateEntry then table.insert(entries, dateEntry) end
    -- output
    for k, v in ipairs(entries) do
        snk:write("\n\n")
        snk:write(v.txt)
        snk:write("\n")
    end
end


function main()
    local app = {
        since = false,
        remoteName = false,
        isFetch = true,
        fullHistory = {},
        fullHistoryRdBeg = 1,
        commits = {},
        parseFn = false,
    }
    if parseArgs(app) ~= 0 then os.exit(1) end
    if app.isHelp then printHelp() return end
    run(app)
end


main()