diff --git a/api.go b/api.go
index 489be3b506401ec4e119158d29197f779eec3506..95d156831b206a4179f30e9206ba72ca1212ef5c 100644
--- a/api.go
+++ b/api.go
@@ -429,7 +429,7 @@ func (p *Project) GetAPIURL() string {
 }
 
 // GetMRs retrieves all the MRs inside the project, with given optional filters
-func (p *Project) GetMRs(filters map[string]string) ([]MergeRequest, error) {
+func (p *Project) GetMRs(filters map[string]string, showProgress bool) ([]MergeRequest, error) {
 	var mrs, partial []MergeRequest
 	url := fmt.Sprintf(`%s/merge_requests`, p.GetAPIURL())
 	params := defaultPagination()
@@ -447,6 +447,11 @@ func (p *Project) GetMRs(filters map[string]string) ([]MergeRequest, error) {
 		mrs = append(mrs, partial...)
 		url = next
 		params = map[string]string{}
+		if showProgress {
+			fmt.Print("\033[G\033[K") // move the cursor left and clear the line
+			log.Print("Counting MergeRequests: ", len(mrs))
+			fmt.Print("\033[A") // move the cursor up
+		}
 	}
 	for index := range mrs {
 		mrs[index].Project = p
diff --git a/issue.go b/issue.go
index 1adf453afca5a06a48f669455d7ffdafdb59d0f7..0f51d261ba28f7a43739bed48b30426ff3573543 100644
--- a/issue.go
+++ b/issue.go
@@ -66,11 +66,7 @@ func (i IssueDelete) Action(wg *sync.WaitGroup, issue Issue, filters map[string]
 }
 func (i IssueDelete) Result([]interface{}, map[string]string, map[string]string) {}
 
-type UserCount struct {
-	User  User
-	Count int
-}
-type StatsReport struct {
+type IssueStatsReport struct {
 	NoteUsers []UserCount
 	User      User
 }
@@ -78,7 +74,7 @@ type StatsReport struct {
 type IssueStats struct{}
 
 func (i IssueStats) Action(wg *sync.WaitGroup, issue Issue, filters map[string]string, options map[string]string, results chan interface{}) {
-	report := StatsReport{}
+	report := IssueStatsReport{}
 	defer func() {
 		results <- report
 		wg.Done()
@@ -119,7 +115,7 @@ func (i IssueStats) Result(results []interface{}, filters map[string]string, opt
 	wordsUserMap := map[int64]int64{}
 	issueUserMap := map[int64]int64{}
 	for _, r := range results {
-		report := r.(StatsReport)
+		report := r.(IssueStatsReport)
 		for _, uw := range report.NoteUsers {
 			userMap[uw.User.ID] = uw.User
 			if _, ok := noteUserMap[uw.User.ID]; ok {
diff --git a/main.go b/main.go
index bb0dd0764b910f28cd37209619f0e66d18aecc51..aeb10faa88f953a600a3d6aced0fbb26ebdfcff1 100644
--- a/main.go
+++ b/main.go
@@ -32,19 +32,6 @@ var (
 func bold(s string) string {
 	return fmt.Sprintf("\033[1m%s\033[0m", s)
 }
-func logMR(mrID int64, v ...interface{}) {
-	header := fmt.Sprintf("[MR %d]", mrID)
-	if colorful {
-		color := mrID % 256
-		header = fmt.Sprintf("\033[38;5;%dm[MR %d]\033[39;49m", color, mrID)
-	}
-	logStderrln(header, fmt.Sprint(v...))
-}
-func logMRVerbose(mrID int64, v ...interface{}) {
-	if verbose || debug {
-		logMR(mrID, v...)
-	}
-}
 func logStderrln(v ...interface{}) {
 	fmt.Fprintln(os.Stderr, v...)
 }
@@ -75,6 +62,11 @@ func checkDate(m map[string]string, key string) {
 	}
 }
 
+type UserCount struct {
+	User  User
+	Count int
+}
+
 type Item struct {
 	Key, Value int64
 }
@@ -163,7 +155,7 @@ func main() {
 	case "issues":
 		ActOnIssues(&project, arrayOptionToMap(filters), maxParallel, action, arrayOptionToMap(actionOptions))
 	case "mrs":
-		// pass
+		ActOnMergeRequests(&project, arrayOptionToMap(filters), maxParallel, action, arrayOptionToMap(actionOptions))
 	default:
 		log.Println("Unknown category: ", on)
 		os.Exit(-1)
diff --git a/mr.go b/mr.go
new file mode 100644
index 0000000000000000000000000000000000000000..e575201c1a1e2d84f6a27e1b9e708618cf7ed9dc
--- /dev/null
+++ b/mr.go
@@ -0,0 +1,333 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"os"
+	"regexp"
+	"sync"
+	"time"
+)
+
+func logMR(mrID int64, v ...interface{}) {
+	header := fmt.Sprintf("[MR %d]", mrID)
+	if colorful {
+		color := mrID % 256
+		header = fmt.Sprintf("\033[38;5;%dm[MR %d]\033[39;49m", color, mrID)
+	}
+	logStderrln(header, fmt.Sprint(v...))
+}
+func logMRVerbose(mrID int64, v ...interface{}) {
+	if verbose || debug {
+		logMR(mrID, v...)
+	}
+}
+
+type MergeRequestAction interface {
+	Action(*sync.WaitGroup, MergeRequest, map[string]string, map[string]string, chan interface{})
+	Result([]interface{}, map[string]string, map[string]string)
+}
+
+type MergeRequestShow struct{}
+
+func (m MergeRequestShow) Action(wg *sync.WaitGroup, mr MergeRequest, filters map[string]string, options map[string]string, results chan interface{}) {
+	defer func() {
+		results <- nil
+		wg.Done()
+	}()
+	logMR(mr.IID, "From: ", mr.Author.Username, " (", mr.Author.Name, ") ", "Subject: ", mr.Title)
+}
+func (i MergeRequestShow) Result([]interface{}, map[string]string, map[string]string) {}
+
+type MergeRequestDummy struct{}
+
+func (i MergeRequestDummy) Action(wg *sync.WaitGroup, mr MergeRequest, filters map[string]string, options map[string]string, results chan interface{}) {
+	defer func() {
+		results <- nil
+		wg.Done()
+	}()
+}
+func (i MergeRequestDummy) Result([]interface{}, map[string]string, map[string]string) {}
+
+type MRNbVersions struct {
+	MRID int64
+	Nb   int
+}
+
+type MRReport struct {
+	NoteUsers []UserCount
+	UpUsers   []User
+	DownUsers []User
+	User      User
+	Versions  MRNbVersions
+}
+
+type MergeRequestStats struct{}
+
+func (i MergeRequestStats) Action(wg *sync.WaitGroup, mr MergeRequest, filters map[string]string, options map[string]string, results chan interface{}) {
+	report := MRReport{}
+	defer func() {
+		results <- report
+		wg.Done()
+	}()
+	logMR(mr.IID, "MR:", mr.IID, " from ", mr.Author.Name)
+	var mrNoteUsers []UserCount
+	var upVotes []User
+	var downVotes []User
+	report.User = mr.Author
+	versions, err := mr.GetVersions()
+	if err != nil {
+		logMR(mr.IID, "WARNING: error during versions processing: "+err.Error())
+		panic(err.Error())
+	}
+	report.Versions = MRNbVersions{mr.IID, len(*versions)}
+	// NOTES
+	notes, err := mr.GetAllNotes(false)
+	if err != nil {
+		logMR(mr.IID, "WARNING: error during notes processing: "+err.Error())
+		panic(err.Error())
+	}
+	re := regexp.MustCompile(`\S+`)
+	timeBefore := time.Time{}
+	if before, ok := filters[UpdatedBefore]; ok {
+		timeBefore, _ = time.Parse(time.RFC3339, before)
+	}
+	timeAfter := time.Time{}
+	if after, ok := filters[UpdatedAfter]; ok {
+		timeAfter, _ = time.Parse(time.RFC3339, after)
+	}
+
+	for _, note := range *notes {
+		if (!timeBefore.IsZero() && note.UpdatedAt.After(timeBefore)) || (!timeAfter.IsZero() && note.UpdatedAt.Before(timeAfter)) {
+			logMR(mr.IID, "Skipping Note ", note.ID, " updated at ", note.UpdatedAt)
+			continue
+		}
+		nbWords := len(re.FindAllString(note.Body, -1))
+		logMR(mr.IID, "One note from ", note.Author.Name, ", nb words: ", nbWords)
+		mrNoteUsers = append(mrNoteUsers, UserCount{note.Author, nbWords})
+	}
+	report.NoteUsers = mrNoteUsers
+	// VOTES
+	awards, err := mr.GetAwardEmojis()
+	if err != nil {
+		logMR(mr.IID, "WARNING: error during emojis processing: "+err.Error())
+		return
+	}
+	for _, award := range *awards {
+		switch emoji := award.Name; emoji {
+		case VoteUp:
+			logMR(mr.IID, "FOUND +1 from ", award.User.Name)
+			upVotes = append(upVotes, award.User)
+		case VoteDown:
+			logMR(mr.IID, "FOUND -1 from ", award.User.Name)
+			downVotes = append(downVotes, award.User)
+		}
+	}
+	report.UpUsers = upVotes
+	report.DownUsers = downVotes
+}
+
+func (i MergeRequestStats) Result(results []interface{}, filters map[string]string, options map[string]string) {
+
+	userMap := map[int64]User{}
+	noteUserMap := map[int64]int64{}
+	wordsUserMap := map[int64]int64{}
+	upMap := map[int64]int64{}
+	downMap := map[int64]int64{}
+	mrUserMap := map[int64]int64{}
+	maxNbVersion := MRNbVersions{0, 0}
+
+	numberOfMRs := len(results)
+
+	for _, r := range results {
+		report := r.(MRReport)
+		for _, uw := range report.NoteUsers {
+			userMap[uw.User.ID] = uw.User
+			if _, ok := noteUserMap[uw.User.ID]; ok {
+				noteUserMap[uw.User.ID] += 1
+				wordsUserMap[uw.User.ID] += int64(uw.Count)
+			} else {
+				noteUserMap[uw.User.ID] = 1
+				wordsUserMap[uw.User.ID] = int64(uw.Count)
+			}
+		}
+		for _, u := range report.UpUsers {
+			userMap[u.ID] = u
+			if i, ok := upMap[u.ID]; ok {
+				upMap[u.ID] = i + 1
+			} else {
+				upMap[u.ID] = 1
+			}
+		}
+		for _, u := range report.DownUsers {
+			userMap[u.ID] = u
+			if i, ok := downMap[u.ID]; ok {
+				downMap[u.ID] = i + 1
+			} else {
+				downMap[u.ID] = 1
+			}
+		}
+		if _, ok := mrUserMap[report.User.ID]; ok {
+			mrUserMap[report.User.ID] += 1
+		} else {
+			mrUserMap[report.User.ID] = 1
+		}
+		if report.Versions.Nb > maxNbVersion.Nb {
+			maxNbVersion = report.Versions
+		}
+	}
+	output := "md"
+	if o, ok := options["output"]; ok {
+		output = o
+	}
+	switch output {
+	case "csv":
+		printMergeRequestCSV(filters, options, numberOfMRs, userMap, noteUserMap, wordsUserMap, upMap, downMap, mrUserMap, maxNbVersion)
+	default:
+		printMRMarkdown(filters, options, numberOfMRs, userMap, noteUserMap, wordsUserMap, upMap, downMap, mrUserMap, maxNbVersion)
+	}
+}
+
+func printMRMarkdown(filters map[string]string, options map[string]string, numberOfMRs int, userMap map[int64]User, noteUserMap map[int64]int64, wordsUserMap map[int64]int64, upMap map[int64]int64, downMap map[int64]int64, mrUserMap map[int64]int64, maxNbVersion MRNbVersions) {
+	logStdoutln("## Merge Requests")
+	logStdoutln("")
+	before := ""
+	if b, ok := filters[UpdatedBefore]; ok {
+		before = b
+	}
+	after := ""
+	if a, ok := filters[UpdatedAfter]; ok {
+		after = a
+	}
+	if after != "" {
+		logStdoutln("After ", after)
+	}
+	if before != "" {
+		logStdoutln("Before ", before)
+	}
+	logStdoutln("")
+	logStdoutln("Number of MRs found: ", numberOfMRs)
+	logStdoutln("")
+	for _, item := range sortMap(mrUserMap) {
+		uid, nbMrs := item.Key, item.Value
+		logStdoutln(userMap[uid].Name, " (", userMap[uid].Username, ") : ", nbMrs)
+	}
+	logStdoutln("")
+	logStdoutln("### Comments")
+	logStdoutln("")
+	for _, item := range sortMap(noteUserMap) {
+		uid, nbNote := item.Key, item.Value
+		logStdoutln(userMap[uid].Name, " (", userMap[uid].Username, ") : ", nbNote)
+	}
+	logStdoutln("")
+	logStdoutln("### Words")
+	logStdoutln("")
+	for _, item := range sortMap(wordsUserMap) {
+		uid, nbWords := item.Key, item.Value
+		logStdoutln(userMap[uid].Name, " (", userMap[uid].Username, ") : ", nbWords)
+	}
+	logStdoutln("")
+	logStdoutln("### Votes")
+	logStdoutln("")
+	logStdoutln("#### Upvotes")
+	logStdoutln("")
+	for _, item := range sortMap(upMap) {
+		uid, nb := item.Key, item.Value
+		logStdoutln(userMap[uid].Name, " (", userMap[uid].Username, ") : ", nb)
+	}
+	logStdoutln("")
+	logStdoutln("#### Downvotes")
+	logStdoutln("")
+	for _, item := range sortMap(downMap) {
+		uid, nb := item.Key, item.Value
+		logStdoutln(userMap[uid].Name, " (", userMap[uid].Username, ") : ", nb)
+	}
+	logStdoutln("")
+	logStdoutln("MR With most nb of versions: MR ID:", maxNbVersion.MRID, " with ", maxNbVersion.Nb, " versions")
+	logStdoutln("")
+
+}
+
+func printMergeRequestCSV(filters map[string]string, options map[string]string, numberOfMRs int, userMap map[int64]User, noteUserMap map[int64]int64, wordsUserMap map[int64]int64, upMap map[int64]int64, downMap map[int64]int64, mrUserMap map[int64]int64, maxNbVersion MRNbVersions) {
+	logStdoutln(" After , Before , Username , Type , Value , NumberOf ")
+	before := ""
+	if b, ok := filters[UpdatedBefore]; ok {
+		before = b
+	}
+	after := ""
+	if a, ok := filters[UpdatedAfter]; ok {
+		after = a
+	}
+	for _, item := range sortMap(mrUserMap) {
+		uid, nbMrs := item.Key, item.Value
+		logStdoutln("\""+after+"\"", " , ", "\""+before+"\"", " , ", "\""+userMap[uid].Username+"\"", " , ", "MRNumber", " , ", nbMrs, " , ", numberOfMRs)
+	}
+	for _, item := range sortMap(noteUserMap) {
+		uid, nbNote := item.Key, item.Value
+		logStdoutln("\""+after+"\"", " , ", "\""+before+"\"", " , ", "\""+userMap[uid].Username+"\"", " , ", "MRComments", " , ", nbNote, " , ", "")
+	}
+	for _, item := range sortMap(wordsUserMap) {
+		uid, nbWords := item.Key, item.Value
+		logStdoutln("\""+after+"\"", " , ", "\""+before+"\"", " , ", "\""+userMap[uid].Username+"\"", " , ", "MRWords", " , ", nbWords, " , ", "")
+	}
+	for _, item := range sortMap(upMap) {
+		uid, nb := item.Key, item.Value
+		logStdoutln("\""+after+"\"", " , ", "\""+before+"\"", " , ", "\""+userMap[uid].Username+"\"", " , ", "MRUpVotes", " , ", nb, " , ", "")
+	}
+	for _, item := range sortMap(downMap) {
+		uid, nb := item.Key, item.Value
+		logStdoutln("\""+after+"\"", " , ", "\""+before+"\"", " , ", "\""+userMap[uid].Username+"\"", " , ", "MRDownVotes", " , ", nb, " , ", "")
+	}
+}
+
+func ActOnMergeRequests(project *Project, filters map[string]string, maxParallel int, actionStr string, options map[string]string) {
+	checkDate(filters, UpdatedBefore)
+	checkDate(filters, UpdatedAfter)
+	mrs, err := project.GetMRs(filters, true)
+	if err != nil {
+		log.Println("Error while retrieving Project's MergeRequests: ", err.Error())
+		os.Exit(-1)
+	}
+	log.Println("Found", len(mrs), "MergeRequests in Project Number ", project.ID, "with the following criteria: ")
+	log.Println(filters)
+
+	var action MergeRequestAction
+	switch actionStr {
+	case ActionDummy:
+		action = MergeRequestDummy{}
+	case ActionShow:
+		action = MergeRequestShow{}
+	case ActionStats:
+		action = MergeRequestStats{}
+	default:
+		log.Println("Action", actionStr, "Unknown")
+		os.Exit(-1)
+	}
+	log.Println("")
+	log.Println("You asked for Action", bold(actionStr))
+	log.Println("")
+	if !askConfirm("Do you want to procede?") {
+		log.Println("Aborting...")
+		os.Exit(-1)
+	}
+	results := make(chan (interface{}), len(mrs))
+	for i := 0; i < len(mrs); i += maxParallel {
+		var sub []MergeRequest
+		if i+maxParallel > len(mrs) {
+			sub = mrs[i:]
+		} else {
+			sub = mrs[i : i+maxParallel]
+		}
+		var wg sync.WaitGroup
+		for _, mr := range sub {
+			wg.Add(1)
+			go action.Action(&wg, mr, filters, options, results)
+		}
+		wg.Wait()
+	}
+	var resultArray []interface{}
+	for i := 0; i < len(mrs); i++ {
+		resultArray = append(resultArray, <-results)
+	}
+	action.Result(resultArray, filters, options)
+}