1// Copyright 2014 The Go Authors.  All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package main
6
7import (
8	"bytes"
9	"fmt"
10	"io"
11	"net/http"
12	"sort"
13	"strings"
14	"time"
15)
16
17var (
18	pendingLocal       bool // -l flag, use only local operations (no network)
19	pendingCurrentOnly bool // -c flag, show only current branch
20	pendingShort       bool // -s flag, short display
21)
22
23// A pendingBranch collects information about a single pending branch.
24// We overlap the reading of this information for each branch.
25type pendingBranch struct {
26	*Branch            // standard Branch functionality
27	current   bool     // is this the current branch?
28	staged    []string // files in staging area, only if current==true
29	unstaged  []string // files unstaged in local directory, only if current==true
30	untracked []string // files untracked in local directory, only if current==true
31}
32
33// load populates b with information about the branch.
34func (b *pendingBranch) load() {
35	b.loadPending()
36	if !b.current && b.commitsAhead == 0 {
37		// Won't be displayed, don't bother looking any closer.
38		return
39	}
40	b.OriginBranch() // cache result
41	if b.current {
42		b.staged, b.unstaged, b.untracked = LocalChanges()
43	}
44	var changeIDs []string
45	var commits []*Commit
46	for _, c := range b.Pending() {
47		c.committed = ListFiles(c)
48		if c.ChangeID == "" {
49			c.gerr = fmt.Errorf("missing Change-Id in commit message")
50		} else {
51			changeIDs = append(changeIDs, fullChangeID(b.Branch, c))
52			commits = append(commits, c)
53		}
54	}
55	if !pendingLocal {
56		gs, err := b.GerritChanges(changeIDs, "DETAILED_LABELS", "CURRENT_REVISION", "MESSAGES", "DETAILED_ACCOUNTS")
57		if len(gs) != len(commits) && err == nil {
58			err = fmt.Errorf("invalid response from Gerrit server - %d queries but %d results", len(changeIDs), len(gs))
59		}
60		if err != nil {
61			for _, c := range commits {
62				if c.gerr != nil {
63					c.gerr = err
64				}
65			}
66		} else {
67			for i, c := range commits {
68				if len(gs[i]) == 1 {
69					c.g = gs[i][0]
70				}
71			}
72		}
73	}
74	for _, c := range b.Pending() {
75		if c.g == nil {
76			c.g = new(GerritChange) // easier for formatting code
77		}
78	}
79}
80
81func cmdPending(args []string) {
82	// NOTE: New flags should be added to the usage message below as well as doc.go.
83	flags.BoolVar(&pendingCurrentOnly, "c", false, "show only current branch")
84	flags.BoolVar(&pendingLocal, "l", false, "use only local information - no network operations")
85	flags.BoolVar(&pendingShort, "s", false, "show short listing")
86	flags.Parse(args)
87	if len(flags.Args()) > 0 {
88		fmt.Fprintf(stderr(), "Usage: %s pending %s [-c] [-l] [-s]\n", progName, globalFlags)
89		exit(2)
90	}
91
92	// Fetch info about remote changes, so that we can say which branches need sync.
93	doneFetch := make(chan bool, 1)
94	if pendingLocal {
95		doneFetch <- true
96	} else {
97		http.DefaultClient.Timeout = 60 * time.Second
98		go func() {
99			run("git", "fetch", "-q")
100			doneFetch <- true
101		}()
102	}
103
104	// Build list of pendingBranch structs to be filled in.
105	// The current branch is always first.
106	var branches []*pendingBranch
107	branches = []*pendingBranch{{Branch: CurrentBranch(), current: true}}
108	if !pendingCurrentOnly {
109		current := CurrentBranch().Name
110		for _, b := range LocalBranches() {
111			if b.Name != current {
112				branches = append(branches, &pendingBranch{Branch: b})
113			}
114		}
115	}
116
117	// The various data gathering is a little slow,
118	// especially run in serial with a lot of branches.
119	// Overlap inspection of multiple branches.
120	// Each branch is only accessed by a single worker.
121
122	// Build work queue.
123	work := make(chan *pendingBranch, len(branches))
124	done := make(chan bool, len(branches))
125	for _, b := range branches {
126		work <- b
127	}
128	close(work)
129
130	// Kick off goroutines to do work.
131	n := len(branches)
132	if n > 10 {
133		n = 10
134	}
135	for i := 0; i < n; i++ {
136		go func() {
137			for b := range work {
138				// This b.load may be using a stale origin/master ref, which is OK.
139				b.load()
140				done <- true
141			}
142		}()
143	}
144
145	// Wait for goroutines to finish.
146	// Note: Counting work items, not goroutines (there may be fewer goroutines).
147	for range branches {
148		<-done
149	}
150	<-doneFetch
151
152	// Print output.
153	// If there are multiple changes in the current branch, the output splits them out into separate sections,
154	// in reverse commit order, to match git log output.
155	//
156	//	wbshadow 7a524a1..a496c1e (current branch, all mailed, 23 behind, tracking master)
157	//	+ uncommitted changes
158	//		Files unstaged:
159	//			src/runtime/proc1.go
160	//
161	//	+ a496c1e https://go-review.googlesource.com/2064 (mailed)
162	//		runtime: add missing write barriers in append's copy of slice data
163	//
164	//		Found with GODEBUG=wbshadow=1 mode.
165	//		Eventually that will run automatically, but right now
166	//		it still detects other missing write barriers.
167	//
168	//		Change-Id: Ic8624401d7c8225a935f719f96f2675c6f5c0d7c
169	//
170	//		Code-Review:
171	//			+0 Austin Clements, Rick Hudson
172	//		Files in this change:
173	//			src/runtime/slice.go
174	//
175	//	+ 95390c7 https://go-review.googlesource.com/2061 (mailed)
176	//		runtime: add GODEBUG wbshadow for finding missing write barriers
177	//
178	//		This is the detection code. It works well enough that I know of
179	//		a handful of missing write barriers. However, those are subtle
180	//		enough that I'll address them in separate followup CLs.
181	//
182	//		Change-Id: If863837308e7c50d96b5bdc7d65af4969bf53a6e
183	//
184	//		Code-Review:
185	//			+0 Austin Clements, Rick Hudson
186	//		Files in this change:
187	//			src/runtime/extern.go
188	//			src/runtime/malloc1.go
189	//			src/runtime/malloc2.go
190	//			src/runtime/mgc.go
191	//			src/runtime/mgc0.go
192	//			src/runtime/proc1.go
193	//			src/runtime/runtime1.go
194	//			src/runtime/runtime2.go
195	//			src/runtime/stack1.go
196	//
197	// The first line only gives information that applies to the entire branch:
198	// the name, the commit range, whether this is the current branch, whether
199	// all the commits are mailed/submitted, how far behind, what remote branch
200	// it is tracking.
201	// The individual change sections have per-change information: the hash of that
202	// commit, the URL on the Gerrit server, whether it is mailed/submitted, the list of
203	// files in that commit. The uncommitted file modifications are shown as a separate
204	// section, at the beginning, to fit better into the reverse commit order.
205	//
206	// The short view compresses the listing down to two lines per commit:
207	//	wbshadow 7a524a1..a496c1e (current branch, all mailed, 23 behind, tracking master)
208	//	+ uncommitted changes
209	//		Files unstaged:
210	//			src/runtime/proc1.go
211	//	+ a496c1e runtime: add missing write barriers in append's copy of slice data (CL 2064, mailed)
212	//	+ 95390c7 runtime: add GODEBUG wbshadow for finding missing write barriers (CL 2061, mailed)
213
214	var buf bytes.Buffer
215	printFileList := func(name string, list []string) {
216		if len(list) == 0 {
217			return
218		}
219		fmt.Fprintf(&buf, "\tFiles %s:\n", name)
220		for _, file := range list {
221			fmt.Fprintf(&buf, "\t\t%s\n", file)
222		}
223	}
224
225	for _, b := range branches {
226		if !b.current && b.commitsAhead == 0 {
227			// Hide branches with no work on them.
228			continue
229		}
230
231		fmt.Fprintf(&buf, "%s", b.Name)
232		work := b.Pending()
233		if len(work) > 0 {
234			fmt.Fprintf(&buf, " %.7s..%s", b.branchpoint, work[0].ShortHash)
235		}
236		var tags []string
237		if b.DetachedHead() {
238			tags = append(tags, "detached")
239		} else if b.current {
240			tags = append(tags, "current branch")
241		}
242		if allMailed(work) && len(work) > 0 {
243			tags = append(tags, "all mailed")
244		}
245		if allSubmitted(work) && len(work) > 0 {
246			tags = append(tags, "all submitted")
247		}
248		if n := b.CommitsBehind(); n > 0 {
249			tags = append(tags, fmt.Sprintf("%d behind", n))
250		}
251		if br := b.OriginBranch(); br == "" {
252			tags = append(tags, "remote branch unknown")
253		} else if br != "origin/master" && br != "origin/main" {
254			tags = append(tags, "tracking "+strings.TrimPrefix(b.OriginBranch(), "origin/"))
255		}
256		if len(tags) > 0 {
257			fmt.Fprintf(&buf, " (%s)", strings.Join(tags, ", "))
258		}
259		fmt.Fprintf(&buf, "\n")
260		printed := false
261
262		if b.current && len(b.staged)+len(b.unstaged)+len(b.untracked) > 0 {
263			printed = true
264			fmt.Fprintf(&buf, "+ uncommitted changes\n")
265			printFileList("untracked", b.untracked)
266			printFileList("unstaged", b.unstaged)
267			printFileList("staged", b.staged)
268			if !pendingShort {
269				fmt.Fprintf(&buf, "\n")
270			}
271		}
272
273		for _, c := range work {
274			printed = true
275			fmt.Fprintf(&buf, "+ ")
276			formatCommit(&buf, c, pendingShort)
277			if !pendingShort {
278				printFileList("in this change", c.committed)
279				fmt.Fprintf(&buf, "\n")
280			}
281		}
282		if pendingShort || !printed {
283			fmt.Fprintf(&buf, "\n")
284		}
285	}
286
287	stdout().Write(buf.Bytes())
288}
289
290// formatCommit writes detailed information about c to w. c.g must
291// have the "CURRENT_REVISION" (or "ALL_REVISIONS") and
292// "DETAILED_LABELS" options set.
293//
294// If short is true, this writes a single line overview.
295//
296// If short is false, this writes detailed information about the
297// commit and its Gerrit state.
298func formatCommit(w io.Writer, c *Commit, short bool) {
299	g := c.g
300	if g == nil {
301		g = new(GerritChange)
302	}
303	msg := strings.TrimRight(c.Message, "\r\n")
304	fmt.Fprintf(w, "%s", c.ShortHash)
305	var tags []string
306	if short {
307		if i := strings.Index(msg, "\n"); i >= 0 {
308			msg = msg[:i]
309		}
310		fmt.Fprintf(w, " %s", msg)
311		if g.Number != 0 {
312			tags = append(tags, fmt.Sprintf("CL %d%s", g.Number, codeReviewScores(g)))
313		}
314	} else {
315		if g.Number != 0 {
316			fmt.Fprintf(w, " %s/%d", auth.url, g.Number)
317		}
318	}
319	if g.CurrentRevision == c.Hash {
320		tags = append(tags, "mailed")
321	}
322	switch g.Status {
323	case "MERGED":
324		tags = append(tags, "submitted")
325	case "ABANDONED":
326		tags = append(tags, "abandoned")
327	}
328	if len(c.Parents) > 1 {
329		var h []string
330		for _, p := range c.Parents[1:] {
331			h = append(h, p[:7])
332		}
333		tags = append(tags, "merge="+strings.Join(h, ","))
334	}
335	if len(tags) > 0 {
336		fmt.Fprintf(w, " (%s)", strings.Join(tags, ", "))
337	}
338	fmt.Fprintf(w, "\n")
339	if short {
340		return
341	}
342
343	fmt.Fprintf(w, "\t%s\n", strings.Replace(msg, "\n", "\n\t", -1))
344	fmt.Fprintf(w, "\n")
345
346	for _, name := range g.LabelNames() {
347		label := g.Labels[name]
348		minValue := 10000
349		maxValue := -10000
350		byScore := map[int][]string{}
351		for _, x := range label.All {
352			// Hide CL owner unless owner score is nonzero.
353			if g.Owner != nil && x.ID == g.Owner.ID && x.Value == 0 {
354				continue
355			}
356			byScore[x.Value] = append(byScore[x.Value], x.Name)
357			if minValue > x.Value {
358				minValue = x.Value
359			}
360			if maxValue < x.Value {
361				maxValue = x.Value
362			}
363		}
364		// Unless there are scores to report, do not show labels other than Code-Review.
365		// This hides Run-TryBot and TryBot-Result.
366		if minValue >= 0 && maxValue <= 0 && name != "Code-Review" {
367			continue
368		}
369		fmt.Fprintf(w, "\t%s:\n", name)
370		for score := maxValue; score >= minValue; score-- {
371			who := byScore[score]
372			if len(who) == 0 || score == 0 && name != "Code-Review" {
373				continue
374			}
375			sort.Strings(who)
376			fmt.Fprintf(w, "\t\t%+d %s\n", score, strings.Join(who, ", "))
377		}
378	}
379}
380
381// codeReviewScores reports the code review scores as tags for the short output.
382//
383// g must have the "DETAILED_LABELS" option set.
384func codeReviewScores(g *GerritChange) string {
385	label := g.Labels["Code-Review"]
386	if label == nil {
387		return ""
388	}
389	minValue := 10000
390	maxValue := -10000
391	for _, x := range label.All {
392		if minValue > x.Value {
393			minValue = x.Value
394		}
395		if maxValue < x.Value {
396			maxValue = x.Value
397		}
398	}
399	var scores string
400	if minValue < 0 {
401		scores += fmt.Sprintf(" %d", minValue)
402	}
403	if maxValue > 0 {
404		scores += fmt.Sprintf(" %+d", maxValue)
405	}
406	return scores
407}
408
409// allMailed reports whether all commits in work have been posted to Gerrit.
410func allMailed(work []*Commit) bool {
411	for _, c := range work {
412		if c.Hash != c.g.CurrentRevision {
413			return false
414		}
415	}
416	return true
417}
418
419// allSubmitted reports whether all commits in work have been submitted to the origin branch.
420func allSubmitted(work []*Commit) bool {
421	for _, c := range work {
422		if c.g.Status != "MERGED" {
423			return false
424		}
425	}
426	return true
427}
428
429// suffix returns an empty string if n == 1, s otherwise.
430func suffix(n int, s string) string {
431	if n == 1 {
432		return ""
433	}
434	return s
435}
436