github.com/decred/politeia@v1.4.0/politeiad/cmd/legacypoliteia/git_log.go (about) 1 // Copyright (c) 2022 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "encoding/json" 9 "fmt" 10 "os/exec" 11 "path/filepath" 12 "regexp" 13 "strings" 14 "time" 15 16 "github.com/decred/politeia/politeiad/cmd/legacypoliteia/gitbe" 17 ) 18 19 // git_log.go contains the code that runs the git log command and parses 20 // its output. 21 22 // parseVoteTimestamps parses the git commit log and returns the vote 23 // timestamps for each of the cast votes in a proposal's ballot journal. The 24 // timestamps are not actually the exact timestamp of when the vote was cast, 25 // but rather the timestamp of the git commit that added the vote to the git 26 // repo. 27 // 28 // Note, it's possible for a commit to contain the ballot journal updates from 29 // multiple proposal votes when the votes occur at the same time. This is fine. 30 // It just means that the returned map may contain additional vote timestamps. 31 // The caller should not assume that only the vote timestamps being returned 32 // are for the specified proposal. 33 func parseVoteTimestamps(proposalDir string) (map[string]int64, error) { 34 fmt.Printf(" Parsing the vote timestamps from the git logs...\n") 35 36 // The following command is run from the decred plugin directory 37 // for the proposal. 38 // 39 // $ /usr/bin/git log --reverse -p ballot.journal 40 // 41 // Decred plugin dir: /[token]/[version]/plugins/decred 42 // 43 // This command logs the commits that touched the ballot.journal. 44 // The commit details and full diff are logged. We parse these logs 45 // to find when each vote was committed to the ballot journal and 46 // associate the vote with the timestamp of the commit that added 47 // it. 48 // 49 // See sample_commit.txt for an example of what a commit will look 50 // like. The output will contain all commits that touched the 51 // ballots journal file. 52 var ( 53 decredPluginDir = filepath.Join(proposalDir, gitbe.DecredPluginPath) 54 55 args = []string{"log", "--reverse", "-p", gitbe.BallotJournalFilename} 56 cmd = exec.Command("git", args...) 57 ) 58 cmd.Dir = decredPluginDir 59 60 out, err := cmd.Output() 61 if err != nil { 62 return nil, err 63 } 64 65 // Split the output into individual commits. A commit 66 // will start with "commit [commitHash]". 67 // 68 // Ex: "commit b09912047e9ffc82c944f9f82d2384bc23b4b3b9" 69 rawCommits := strings.Split(string(out), "commit") 70 71 // Parse the commit timestamp and ticket hashes from the 72 // raw commit text and associate each ticket hash with a 73 // commit timestamp. 74 voteTimestamps := make(map[string]int64, 40960) // [ticket]unixTime 75 for i, rawCommit := range rawCommits { 76 s := fmt.Sprintf(" Parsing ballot journal commit %v/%v", 77 i+1, len(rawCommits)) 78 printInPlace(s) 79 80 // Skip empty entries 81 rawCommit = strings.TrimSpace(rawCommit) 82 if len(rawCommit) == 0 { 83 continue 84 } 85 86 // Parse the commit date 87 t, err := parseCommitDate(rawCommit) 88 if err != nil { 89 return nil, err 90 } 91 92 // Parse the votes 93 castVotes, err := parseCastVotes(rawCommit) 94 if err != nil { 95 return nil, err 96 } 97 98 // Associate each vote in the commit with the 99 // commit timestamp. 100 for _, cv := range castVotes { 101 voteTimestamps[cv.Ticket] = t.Unix() 102 } 103 } 104 105 fmt.Printf("\n") 106 fmt.Printf(" %v vote timestamps found\n", len(voteTimestamps)) 107 108 return voteTimestamps, nil 109 } 110 111 var ( 112 // dateLineRegExp matches the date line from a git commit log message. 113 // 114 // Ex: "Date: Sun Apr 11 18:58:01 2021 +0000" 115 dateLineRegExp = regexp.MustCompile(`Date[:\s]*(.*)`) 116 117 // commitDateLayout is the date layout that is used in a git commit log 118 // message. 119 commitDateLayout = "Mon Jan 2 15:04:05 2006 -0700" 120 ) 121 122 // parseCommitDate parses the date line from a git commit log message and 123 // returns a Time representation of it. 124 // 125 // Ex: "Date: Sun Apr 11 18:58:01 2021 +0000" is parsed from the git commit 126 // log message and converted to a Time type. 127 func parseCommitDate(commitLog string) (*time.Time, error) { 128 // Parse the date line from the commit log message 129 // 130 // Ex: "Date: Sun Apr 11 18:58:01 2021 +0000" 131 dateStrs := dateLineRegExp.FindAllString(commitLog, -1) 132 if len(dateStrs) != 1 { 133 return nil, fmt.Errorf("found %v date strings, want 1", len(dateStrs)) 134 } 135 dateStr := dateStrs[0] 136 137 // Trim the prefix and whitespace 138 dateStr = strings.TrimPrefix(dateStr, "Date:") 139 dateStr = strings.TrimSpace(dateStr) 140 141 // Convert the date string to a Time type 142 t, err := time.Parse(commitDateLayout, dateStr) 143 if err != nil { 144 return nil, err 145 } 146 147 return &t, nil 148 } 149 150 var ( 151 // castVoteRegExp matches the gitbe CastVote JSON structure. 152 // 153 // Ex: {"token":"95a14094485c92ed3f578b650bd76c5f8c3fd6392650c16bd4ae37e6167c040d","ticket":"12a94af3ac7efe530abdb62c20d522f270b250f1a9e050ee63b796936abd4bed","votebit":"2","signature":"208b378e391e22802408dc26e65048cebc1245f2ff153cc4de85c73b07a5ae7f3679a4f2b55a3d28df60fa80b618b8aaebafbfe0d12ef18c4d63d954687c983637"} 154 castVoteRE = `{"token":"[0-9a-f]{64}","ticket":"[0-9a-f]{64}",` + 155 `"votebit":"[0-9]","signature":"[0-9a-f]{130}"}` 156 castVoteRegExp = regexp.MustCompile(castVoteRE) 157 ) 158 159 // parseCastVotes parses the JSON encoded gitbe CastVote structures from the 160 // provided string and returns the decoded JSON. 161 func parseCastVotes(s string) ([]gitbe.CastVote, error) { 162 var ( 163 castVotesJSON = castVoteRegExp.FindAll([]byte(s), -1) 164 castVotes = make([]gitbe.CastVote, 0, len(castVotesJSON)) 165 ) 166 for _, b := range castVotesJSON { 167 var cv gitbe.CastVote 168 err := json.Unmarshal(b, &cv) 169 if err != nil { 170 return nil, err 171 } 172 castVotes = append(castVotes, cv) 173 } 174 return castVotes, nil 175 }