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  }