Commit 02e4d29c3d
Verified · cmc
Layout: unified · split
internal/control/label.go +21 −1
| @@ -38,6 +38,20 @@ func init() { | |||
| 38 | // it is the one that types cleanly there. | 38 | // it is the one that types cleanly there. |
| 39 | var labelColorPat = regexp.MustCompile(`^#?[0-9a-fA-F]{6}$`) | 39 | var labelColorPat = regexp.MustCompile(`^#?[0-9a-fA-F]{6}$`) |
| 40 | 40 | ||
| 41 | // labelsScreen is a label list at a terminal: each label's name, colour | ||
| 42 | // and how much carries it. | ||
| 43 | func labelsScreen(labels []store.Label, actions ...action) screen { | ||
| 44 | rows := make([]row, len(labels)) | ||
| 45 | for i, l := range labels { | ||
| 46 | org := "" | ||
| 47 | if l.Org { | ||
| 48 | org = "org" | ||
| 49 | } | ||
| 50 | rows[i] = rowOf(cRef(l.Name), cSwatch(l.Color), cMeta(fmt.Sprintf("%d issues", l.Issues), fmt.Sprintf("%d MRs", l.MRs), org)) | ||
| 51 | } | ||
| 52 | return listScreen("Labels", rows, actions...) | ||
| 53 | } | ||
| 54 | |||
| 41 | func runLabelList(c *Ctx, args []string) int { | 55 | func runLabelList(c *Ctx, args []string) int { |
| 42 | if len(args) != 1 { | 56 | if len(args) != 1 { |
| 43 | return c.usage() | 57 | return c.usage() |
| @@ -54,7 +68,7 @@ func runLabelList(c *Ctx, args []string) int { | |||
| 54 | if err != nil { | 68 | if err != nil { |
| 55 | return c.fail(protocol.ExitFailure, "%v", err) | 69 | return c.fail(protocol.ExitFailure, "%v", err) |
| 56 | } | 70 | } |
| 57 | return c.emit(labels, func(w io.Writer) { | 71 | return c.emitView(labels, func(w io.Writer) { |
| 58 | tb := c.table(w, "NAME", "COLOR", "ISSUES", "MRS") | 72 | tb := c.table(w, "NAME", "COLOR", "ISSUES", "MRS") |
| 59 | for _, l := range labels { | 73 | for _, l := range labels { |
| 60 | cells := []cell{cRef(l.Name), cSwatch(l.Color), cNum(l.Issues), cNum(l.MRs)} | 74 | cells := []cell{cRef(l.Name), cSwatch(l.Color), cNum(l.Issues), cNum(l.MRs)} |
| @@ -64,6 +78,12 @@ func runLabelList(c *Ctx, args []string) int { | |||
| 64 | tb.row(cells...) | 78 | tb.row(cells...) |
| 65 | } | 79 | } |
| 66 | tb.flush() | 80 | tb.flush() |
| 81 | }, func() screen { | ||
| 82 | s := labelsScreen(labels, action{"Labels", []string{"label", "set", repo.Path(), "<label>", "--color", "rrggbb"}}) | ||
| 83 | if len(labels) > 0 { | ||
| 84 | s.actions = append(s.actions, action{"Read", []string{"issue", "list", repo.Path(), "--label", labels[0].Name}}) | ||
| 85 | } | ||
| 86 | return s | ||
| 67 | }) | 87 | }) |
| 68 | } | 88 | } |
| 69 | 89 | ||
internal/control/milestone.go +24 −26
| @@ -116,11 +116,14 @@ func runMilestoneList(c *Ctx, args []string) int { | |||
| 116 | if err != nil { | 116 | if err != nil { |
| 117 | return c.fail(protocol.ExitFailure, "%v", err) | 117 | return c.fail(protocol.ExitFailure, "%v", err) |
| 118 | } | 118 | } |
| 119 | return emitMilestones(c, ms) | 119 | return emitMilestones(c, ms, |
| 120 | action{"Milestones", []string{"milestone", "create", repo.Path(), "<title>"}}, | ||
| 121 | action{"Milestones", []string{"milestone", "list", repo.Path(), "--state", "all"}}, | ||
| 122 | ) | ||
| 120 | } | 123 | } |
| 121 | 124 | ||
| 122 | // emitMilestones renders a milestone list for the caller, JSON or plain. | 125 | // emitMilestones renders a milestone list for the caller, JSON or plain. |
| 123 | func emitMilestones(c *Ctx, ms []store.Milestone) int { | 126 | func emitMilestones(c *Ctx, ms []store.Milestone, actions ...action) int { |
| 124 | type out struct { | 127 | type out struct { |
| 125 | Title string `json:"title"` | 128 | Title string `json:"title"` |
| 126 | Description string `json:"description,omitempty"` | 129 | Description string `json:"description,omitempty"` |
| @@ -134,30 +137,7 @@ func emitMilestones(c *Ctx, ms []store.Milestone) int { | |||
| 134 | for _, m := range ms { | 137 | for _, m := range ms { |
| 135 | ds = append(ds, out{m.Title, m.Description, m.DueDate, m.State, m.OrgID != 0, m.OpenItems, m.ClosedItems}) | 138 | ds = append(ds, out{m.Title, m.Description, m.DueDate, m.State, m.OrgID != 0, m.OpenItems, m.ClosedItems}) |
| 136 | } | 139 | } |
| 137 | return c.emit(ds, func(w io.Writer) { | 140 | return c.emitView(ds, func(w io.Writer) { |
| 138 | if c.Term.Cols > 0 { | ||
| 139 | tb := c.table(w, "TITLE", "STATE", "DUE", "PROGRESS", "SCOPE") | ||
| 140 | for _, d := range ds { | ||
| 141 | due := cText("") | ||
| 142 | if t, ok := parseStamp(d.Due + " 00:00:00"); ok { | ||
| 143 | due = cText(relAge(d.Due+" 00:00:00", termNow())) | ||
| 144 | if d.State == "open" && t.Before(termNow()) { | ||
| 145 | due = cMark("overdue "+d.Due, sgrRed) | ||
| 146 | } | ||
| 147 | } | ||
| 148 | progress := "" | ||
| 149 | if total := d.Open + d.Closed; total > 0 { | ||
| 150 | progress = fmt.Sprintf("%d/%d closed (%d%%)", d.Closed, total, d.Closed*100/total) | ||
| 151 | } | ||
| 152 | scope := "" | ||
| 153 | if d.Org { | ||
| 154 | scope = "org" | ||
| 155 | } | ||
| 156 | tb.row(cRef(d.Title), cState(d.State), due, cText(progress), cText(scope)) | ||
| 157 | } | ||
| 158 | tb.flush() | ||
| 159 | return | ||
| 160 | } | ||
| 161 | tb := c.table(w, "TITLE", "STATE", "DUE", "PROGRESS") | 141 | tb := c.table(w, "TITLE", "STATE", "DUE", "PROGRESS") |
| 162 | for _, d := range ds { | 142 | for _, d := range ds { |
| 163 | due := d.Due | 143 | due := d.Due |
| @@ -171,6 +151,24 @@ func emitMilestones(c *Ctx, ms []store.Milestone) int { | |||
| 171 | tb.row(cells...) | 151 | tb.row(cells...) |
| 172 | } | 152 | } |
| 173 | tb.flush() | 153 | tb.flush() |
| 154 | }, func() screen { | ||
| 155 | rows := make([]row, len(ds)) | ||
| 156 | for i, d := range ds { | ||
| 157 | lead, due := cGlyph(d.State), "" | ||
| 158 | if t, ok := parseStamp(d.Due + " 00:00:00"); ok { | ||
| 159 | due = "due " + relAge(d.Due+" 00:00:00", termNow()) | ||
| 160 | if d.State == "open" && t.Before(termNow()) { | ||
| 161 | lead, due = cYou(), "overdue "+d.Due | ||
| 162 | } | ||
| 163 | } | ||
| 164 | progress := fmt.Sprintf("%d/%d closed", d.Closed, d.Open+d.Closed) | ||
| 165 | org := "" | ||
| 166 | if d.Org { | ||
| 167 | org = "org" | ||
| 168 | } | ||
| 169 | rows[i] = rowOf(cRef(d.Title), lead, cFlex(d.Description), cMeta(due, progress, org)) | ||
| 170 | } | ||
| 171 | return listScreen("Milestones", rows, actions...) | ||
| 174 | }) | 172 | }) |
| 175 | } | 173 | } |
| 176 | 174 | ||
internal/control/org.go +27 −2
| @@ -101,12 +101,22 @@ func runOrgList(c *Ctx, args []string) int { | |||
| 101 | for _, o := range orgs { | 101 | for _, o := range orgs { |
| 102 | ds = append(ds, out{o.Username, o.Role}) | 102 | ds = append(ds, out{o.Username, o.Role}) |
| 103 | } | 103 | } |
| 104 | return c.emit(ds, func(w io.Writer) { | 104 | return c.emitView(ds, func(w io.Writer) { |
| 105 | tb := c.table(w, "ORG", "ROLE") | 105 | tb := c.table(w, "ORG", "ROLE") |
| 106 | for _, d := range ds { | 106 | for _, d := range ds { |
| 107 | tb.row(cRef(d.Org), cState(d.Role)) | 107 | tb.row(cRef(d.Org), cState(d.Role)) |
| 108 | } | 108 | } |
| 109 | tb.flush() | 109 | tb.flush() |
| 110 | }, func() screen { | ||
| 111 | rows := make([]row, len(ds)) | ||
| 112 | for i, d := range ds { | ||
| 113 | rows[i] = rowOf(cLink(d.Org, c.siteURL(d.Org)), cState(d.Role)) | ||
| 114 | } | ||
| 115 | s := listScreen("Organizations", rows) | ||
| 116 | if len(ds) > 0 { | ||
| 117 | s.actions = []action{{"Read", []string{"org", "show", ds[0].Org}}} | ||
| 118 | } | ||
| 119 | return s | ||
| 110 | }) | 120 | }) |
| 111 | } | 121 | } |
| 112 | 122 | ||
| @@ -137,7 +147,7 @@ func runOrgShow(c *Ctx, args []string) int { | |||
| 137 | Org string `json:"org"` | 147 | Org string `json:"org"` |
| 138 | Members []memberOut `json:"members"` | 148 | Members []memberOut `json:"members"` |
| 139 | }{org.Name, ms} | 149 | }{org.Name, ms} |
| 140 | return c.emit(d, func(w io.Writer) { | 150 | return c.emitView(d, func(w io.Writer) { |
| 141 | v := c.view(w) | 151 | v := c.view(w) |
| 142 | v.title(d.Org, "", "") | 152 | v.title(d.Org, "", "") |
| 143 | if len(ms) > 0 { | 153 | if len(ms) > 0 { |
| @@ -148,6 +158,21 @@ func runOrgShow(c *Ctx, args []string) int { | |||
| 148 | } | 158 | } |
| 149 | tb.flush() | 159 | tb.flush() |
| 150 | } | 160 | } |
| 161 | }, func() screen { | ||
| 162 | members := section{title: "Members", n: len(ms)} | ||
| 163 | for _, m := range ms { | ||
| 164 | members.rows = append(members.rows, rowOf(cRef(m.User), cState(m.Role))) | ||
| 165 | } | ||
| 166 | return screen{ | ||
| 167 | fields: []field{{"Org", []cell{cLink(d.Org, c.siteURL(d.Org))}}}, | ||
| 168 | sections: []section{members}, | ||
| 169 | actions: []action{ | ||
| 170 | {"Members", []string{"org", "members", "add", d.Org, "<user>"}}, | ||
| 171 | {"Org", []string{"org", "team", "list", d.Org}}, | ||
| 172 | {"Org", []string{"org", "label", "list", d.Org}}, | ||
| 173 | {"Org", []string{"org", "milestone", "list", d.Org}}, | ||
| 174 | }, | ||
| 175 | } | ||
| 151 | }) | 176 | }) |
| 152 | } | 177 | } |
| 153 | 178 | ||
internal/control/orglabel.go +7 −2
| @@ -151,12 +151,14 @@ func runOrgLabelList(c *Ctx, args []string) int { | |||
| 151 | if err != nil { | 151 | if err != nil { |
| 152 | return c.fail(protocol.ExitFailure, "%v", err) | 152 | return c.fail(protocol.ExitFailure, "%v", err) |
| 153 | } | 153 | } |
| 154 | return c.emit(labels, func(w io.Writer) { | 154 | return c.emitView(labels, func(w io.Writer) { |
| 155 | tb := c.table(w, "NAME", "COLOR", "ISSUES", "MRS") | 155 | tb := c.table(w, "NAME", "COLOR", "ISSUES", "MRS") |
| 156 | for _, l := range labels { | 156 | for _, l := range labels { |
| 157 | tb.row(cRef(l.Name), cSwatch(l.Color), cNum(l.Issues), cNum(l.MRs)) | 157 | tb.row(cRef(l.Name), cSwatch(l.Color), cNum(l.Issues), cNum(l.MRs)) |
| 158 | } | 158 | } |
| 159 | tb.flush() | 159 | tb.flush() |
| 160 | }, func() screen { | ||
| 161 | return labelsScreen(labels, action{"Labels", []string{"org", "label", "set", org.Name, "<label>", "--color", "rrggbb"}}) | ||
| 160 | }) | 162 | }) |
| 161 | } | 163 | } |
| 162 | 164 | ||
| @@ -231,7 +233,10 @@ func runOrgMilestoneList(c *Ctx, args []string) int { | |||
| 231 | if err != nil { | 233 | if err != nil { |
| 232 | return c.fail(protocol.ExitFailure, "%v", err) | 234 | return c.fail(protocol.ExitFailure, "%v", err) |
| 233 | } | 235 | } |
| 234 | return emitMilestones(c, ms) | 236 | return emitMilestones(c, ms, |
| 237 | action{"Milestones", []string{"org", "milestone", "create", org.Name, "<title>"}}, | ||
| 238 | action{"Milestones", []string{"org", "milestone", "list", org.Name, "--state", "all"}}, | ||
| 239 | ) | ||
| 235 | } | 240 | } |
| 236 | 241 | ||
| 237 | func runOrgMilestoneClose(c *Ctx, args []string) int { return setOrgMilestoneState(c, args, "closed") } | 242 | func runOrgMilestoneClose(c *Ctx, args []string) int { return setOrgMilestoneState(c, args, "closed") } |
internal/control/repos/acme/site.git/HEAD added +1
| @@ -0,0 +1 @@ | |||
| 1 | ref: refs/heads/main | ||
internal/control/repos/acme/site.git/config added +7
| @@ -0,0 +1,7 @@ | |||
| 1 | [core] | ||
| 2 | repositoryformatversion = 0 | ||
| 3 | filemode = true | ||
| 4 | bare = true | ||
| 5 | ignorecase = true | ||
| 6 | precomposeunicode = true | ||
| 7 | hooksPath = hooks | ||
internal/control/repos/acme/site.git/description added +1
| @@ -0,0 +1 @@ | |||
| 1 | Unnamed repository; edit this file 'description' to name the repository. | ||
internal/control/repos/acme/site.git/hooks/applypatch-msg.sample added +15
| @@ -0,0 +1,15 @@ | |||
| 1 | #!/bin/sh | ||
| 2 | # | ||
| 3 | # An example hook script to check the commit log message taken by | ||
| 4 | # applypatch from an e-mail message. | ||
| 5 | # | ||
| 6 | # The hook should exit with non-zero status after issuing an | ||
| 7 | # appropriate message if it wants to stop the commit. The hook is | ||
| 8 | # allowed to edit the commit message file. | ||
| 9 | # | ||
| 10 | # To enable this hook, rename this file to "applypatch-msg". | ||
| 11 | |||
| 12 | . git-sh-setup | ||
| 13 | commitmsg="$(git rev-parse --git-path hooks/commit-msg)" | ||
| 14 | test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} | ||
| 15 | : | ||
internal/control/repos/acme/site.git/hooks/commit-msg.sample added +74
| @@ -0,0 +1,74 @@ | |||
| 1 | #!/bin/sh | ||
| 2 | # | ||
| 3 | # An example hook script to check the commit log message. | ||
| 4 | # Called by "git commit" with one argument, the name of the file | ||
| 5 | # that has the commit message. The hook should exit with non-zero | ||
| 6 | # status after issuing an appropriate message if it wants to stop the | ||
| 7 | # commit. The hook is allowed to edit the commit message file. | ||
| 8 | # | ||
| 9 | # To enable this hook, rename this file to "commit-msg". | ||
| 10 | |||
| 11 | # Uncomment the below to add a Signed-off-by line to the message. | ||
| 12 | # Doing this in a hook is a bad idea in general, but the prepare-commit-msg | ||
| 13 | # hook is more suited to it. | ||
| 14 | # | ||
| 15 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') | ||
| 16 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" | ||
| 17 | |||
| 18 | # This example catches duplicate Signed-off-by lines and messages that | ||
| 19 | # would confuse 'git am'. | ||
| 20 | |||
| 21 | ret=0 | ||
| 22 | |||
| 23 | test "" = "$(grep '^Signed-off-by: ' "$1" | | ||
| 24 | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { | ||
| 25 | echo >&2 Duplicate Signed-off-by lines. | ||
| 26 | ret=1 | ||
| 27 | } | ||
| 28 | |||
| 29 | comment_re="$( | ||
| 30 | { | ||
| 31 | git config --get-regexp "^core\.comment(char|string)\$" || | ||
| 32 | echo '#' | ||
| 33 | } | sed -n -e ' | ||
| 34 | ${ | ||
| 35 | s/^[^ ]* // | ||
| 36 | s|[][*./\]|\\&|g | ||
| 37 | s/^auto$/[#;@!$%^&|:]/ | ||
| 38 | p | ||
| 39 | }' | ||
| 40 | )" | ||
| 41 | scissors_line="^${comment_re} -\{8,\} >8 -\{8,\}\$" | ||
| 42 | comment_line="^${comment_re}.*" | ||
| 43 | blank_line='^[ ]*$' | ||
| 44 | # Disallow lines starting with "diff -" or "Index: " in the body of the | ||
| 45 | # message. Stop looking if we see a scissors line. | ||
| 46 | line="$(sed -n -e " | ||
| 47 | # Skip comments and blank lines at the start of the file. | ||
| 48 | /${scissors_line}/q | ||
| 49 | /${comment_line}/d | ||
| 50 | /${blank_line}/d | ||
| 51 | # The first paragraph will become the subject header so | ||
| 52 | # does not need to be checked. | ||
| 53 | : subject | ||
| 54 | n | ||
| 55 | /${scissors_line}/q | ||
| 56 | /${blank_line}/!b subject | ||
| 57 | # Check the body of the message for problematic | ||
| 58 | # prefixes. | ||
| 59 | : body | ||
| 60 | n | ||
| 61 | /${scissors_line}/q | ||
| 62 | /${comment_line}/b body | ||
| 63 | /^diff -/{p;q;} | ||
| 64 | /^Index: /{p;q;} | ||
| 65 | b body | ||
| 66 | " "$1")" | ||
| 67 | if test -n "$line" | ||
| 68 | then | ||
| 69 | echo >&2 "Message contains a diff that will confuse 'git am'." | ||
| 70 | echo >&2 "To fix this indent the diff." | ||
| 71 | ret=1 | ||
| 72 | fi | ||
| 73 | |||
| 74 | exit $ret | ||
internal/control/repos/acme/site.git/hooks/fsmonitor-watchman.sample added +168
| @@ -0,0 +1,168 @@ | |||
| 1 | #!/usr/bin/perl | ||
| 2 | |||
| 3 | use strict; | ||
| 4 | use warnings; | ||
| 5 | use IPC::Open2; | ||
| 6 | |||
| 7 | # An example hook script to integrate Watchman | ||
| 8 | # (https://facebook.github.io/watchman/) with git to speed up detecting | ||
| 9 | # new and modified files. | ||
| 10 | # | ||
| 11 | # The hook is passed a version (currently 2) and last update token | ||
| 12 | # formatted as a string and outputs to stdout a new update token and | ||
| 13 | # all files that have been modified since the update token. Paths must | ||
| 14 | # be relative to the root of the working tree and separated by a single NUL. | ||
| 15 | # | ||
| 16 | # To enable this hook, rename this file to "query-watchman" and set | ||
| 17 | # 'git config core.fsmonitor .git/hooks/query-watchman' | ||
| 18 | # | ||
| 19 | my ($version, $last_update_token) = @ARGV; | ||
| 20 | |||
| 21 | # Uncomment for debugging | ||
| 22 | # print STDERR "$0 $version $last_update_token\n"; | ||
| 23 | |||
| 24 | # Check the hook interface version | ||
| 25 | if ($version ne 2) { | ||
| 26 | die "Unsupported query-fsmonitor hook version '$version'.\n" . | ||
| 27 | "Falling back to scanning...\n"; | ||
| 28 | } | ||
| 29 | |||
| 30 | my $git_work_tree = get_working_dir(); | ||
| 31 | |||
| 32 | my $json_pkg; | ||
| 33 | eval { | ||
| 34 | require JSON::XS; | ||
| 35 | $json_pkg = "JSON::XS"; | ||
| 36 | 1; | ||
| 37 | } or do { | ||
| 38 | require JSON::PP; | ||
| 39 | $json_pkg = "JSON::PP"; | ||
| 40 | }; | ||
| 41 | |||
| 42 | launch_watchman(); | ||
| 43 | |||
| 44 | sub launch_watchman { | ||
| 45 | my $o = watchman_query(); | ||
| 46 | if (is_work_tree_watched($o)) { | ||
| 47 | output_result($o->{clock}, @{$o->{files}}); | ||
| 48 | } | ||
| 49 | } | ||
| 50 | |||
| 51 | sub output_result { | ||
| 52 | my ($clockid, @files) = @_; | ||
| 53 | |||
| 54 | # Uncomment for debugging watchman output | ||
| 55 | # open (my $fh, ">", ".git/watchman-output.out"); | ||
| 56 | # binmode $fh, ":utf8"; | ||
| 57 | # print $fh "$clockid\n@files\n"; | ||
| 58 | # close $fh; | ||
| 59 | |||
| 60 | binmode STDOUT, ":utf8"; | ||
| 61 | print $clockid; | ||
| 62 | print "\0"; | ||
| 63 | local $, = "\0"; | ||
| 64 | print @files; | ||
| 65 | } | ||
| 66 | |||
| 67 | sub watchman_clock { | ||
| 68 | my $response = qx/watchman clock "$git_work_tree"/; | ||
| 69 | die "Failed to get clock id on '$git_work_tree'.\n" . | ||
| 70 | "Falling back to scanning...\n" if $? != 0; | ||
| 71 | |||
| 72 | return $json_pkg->new->utf8->decode($response); | ||
| 73 | } | ||
| 74 | |||
| 75 | sub watchman_query { | ||
| 76 | my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') | ||
| 77 | or die "open2() failed: $!\n" . | ||
| 78 | "Falling back to scanning...\n"; | ||
| 79 | |||
| 80 | # In the query expression below we're asking for names of files that | ||
| 81 | # changed since $last_update_token but not from the .git folder. | ||
| 82 | # | ||
| 83 | # To accomplish this, we're using the "since" generator to use the | ||
| 84 | # recency index to select candidate nodes and "fields" to limit the | ||
| 85 | # output to file names only. Then we're using the "expression" term to | ||
| 86 | # further constrain the results. | ||
| 87 | my $last_update_line = ""; | ||
| 88 | if (substr($last_update_token, 0, 1) eq "c") { | ||
| 89 | $last_update_token = "\"$last_update_token\""; | ||
| 90 | $last_update_line = qq[\n"since": $last_update_token,]; | ||
| 91 | } | ||
| 92 | my $query = <<" END"; | ||
| 93 | ["query", "$git_work_tree", {$last_update_line | ||
| 94 | "fields": ["name"], | ||
| 95 | "expression": ["not", ["dirname", ".git"]] | ||
| 96 | }] | ||
| 97 | END | ||
| 98 | |||
| 99 | # Uncomment for debugging the watchman query | ||
| 100 | # open (my $fh, ">", ".git/watchman-query.json"); | ||
| 101 | # print $fh $query; | ||
| 102 | # close $fh; | ||
| 103 | |||
| 104 | print CHLD_IN $query; | ||
| 105 | close CHLD_IN; | ||
| 106 | my $response = do {local $/; <CHLD_OUT>}; | ||
| 107 | |||
| 108 | # Uncomment for debugging the watch response | ||
| 109 | # open ($fh, ">", ".git/watchman-response.json"); | ||
| 110 | # print $fh $response; | ||
| 111 | # close $fh; | ||
| 112 | |||
| 113 | die "Watchman: command returned no output.\n" . | ||
| 114 | "Falling back to scanning...\n" if $response eq ""; | ||
| 115 | die "Watchman: command returned invalid output: $response\n" . | ||
| 116 | "Falling back to scanning...\n" unless $response =~ /^\{/; | ||
| 117 | |||
| 118 | return $json_pkg->new->utf8->decode($response); | ||
| 119 | } | ||
| 120 | |||
| 121 | sub is_work_tree_watched { | ||
| 122 | my ($output) = @_; | ||
| 123 | my $error = $output->{error}; | ||
| 124 | if ($error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) { | ||
| 125 | my $response = qx/watchman watch "$git_work_tree"/; | ||
| 126 | die "Failed to make watchman watch '$git_work_tree'.\n" . | ||
| 127 | "Falling back to scanning...\n" if $? != 0; | ||
| 128 | $output = $json_pkg->new->utf8->decode($response); | ||
| 129 | $error = $output->{error}; | ||
| 130 | die "Watchman: $error.\n" . | ||
| 131 | "Falling back to scanning...\n" if $error; | ||
| 132 | |||
| 133 | # Uncomment for debugging watchman output | ||
| 134 | # open (my $fh, ">", ".git/watchman-output.out"); | ||
| 135 | # close $fh; | ||
| 136 | |||
| 137 | # Watchman will always return all files on the first query so | ||
| 138 | # return the fast "everything is dirty" flag to git and do the | ||
| 139 | # Watchman query just to get it over with now so we won't pay | ||
| 140 | # the cost in git to look up each individual file. | ||
| 141 | my $o = watchman_clock(); | ||
| 142 | $error = $o->{error}; | ||
| 143 | |||
| 144 | die "Watchman: $error.\n" . | ||
| 145 | "Falling back to scanning...\n" if $error; | ||
| 146 | |||
| 147 | output_result($o->{clock}, ("/")); | ||
| 148 | return 0; | ||
| 149 | } | ||
| 150 | |||
| 151 | die "Watchman: $error.\n" . | ||
| 152 | "Falling back to scanning...\n" if $error; | ||
| 153 | |||
| 154 | return 1; | ||
| 155 | } | ||
| 156 | |||
| 157 | sub get_working_dir { | ||
| 158 | my $working_dir; | ||
| 159 | if ($^O =~ 'msys' || $^O =~ 'cygwin') { | ||
| 160 | $working_dir = Win32::GetCwd(); | ||
| 161 | $working_dir =~ tr/\\/\//; | ||
| 162 | } else { | ||
| 163 | require Cwd; | ||
| 164 | $working_dir = Cwd::cwd(); | ||
| 165 | } | ||
| 166 | |||
| 167 | return $working_dir; | ||
| 168 | } | ||
internal/control/repos/acme/site.git/hooks/post-update.sample added +8
| @@ -0,0 +1,8 @@ | |||
| 1 | #!/bin/sh | ||
| 2 | # | ||
| 3 | # An example hook script to prepare a packed repository for use over | ||
| 4 | # dumb transports. | ||
| 5 | # | ||
| 6 | # To enable this hook, rename this file to "post-update". | ||
| 7 | |||
| 8 | exec git update-server-info | ||
internal/control/repos/acme/site.git/hooks/pre-applypatch.sample added +14
| @@ -0,0 +1,14 @@ | |||
| 1 | #!/bin/sh | ||
| 2 | # | ||
| 3 | # An example hook script to verify what is about to be committed | ||
| 4 | # by applypatch from an e-mail message. | ||
| 5 | # | ||
| 6 | # The hook should exit with non-zero status after issuing an | ||
| 7 | # appropriate message if it wants to stop the commit. | ||
| 8 | # | ||
| 9 | # To enable this hook, rename this file to "pre-applypatch". | ||
| 10 | |||
| 11 | . git-sh-setup | ||
| 12 | precommit="$(git rev-parse --git-path hooks/pre-commit)" | ||
| 13 | test -x "$precommit" && exec "$precommit" ${1+"$@"} | ||
| 14 | : | ||
internal/control/repos/acme/site.git/hooks/pre-commit.sample added +49
| @@ -0,0 +1,49 @@ | |||
| 1 | #!/bin/sh | ||
| 2 | # | ||
| 3 | # An example hook script to verify what is about to be committed. | ||
| 4 | # Called by "git commit" with no arguments. The hook should | ||
| 5 | # exit with non-zero status after issuing an appropriate message if | ||
| 6 | # it wants to stop the commit. | ||
| 7 | # | ||
| 8 | # To enable this hook, rename this file to "pre-commit". | ||
| 9 | |||
| 10 | if git rev-parse --verify HEAD >/dev/null 2>&1 | ||
| 11 | then | ||
| 12 | against=HEAD | ||
| 13 | else | ||
| 14 | # Initial commit: diff against an empty tree object | ||
| 15 | against=$(git hash-object -t tree /dev/null) | ||
| 16 | fi | ||
| 17 | |||
| 18 | # If you want to allow non-ASCII filenames set this variable to true. | ||
| 19 | allownonascii=$(git config --type=bool hooks.allownonascii) | ||
| 20 | |||
| 21 | # Redirect output to stderr. | ||
| 22 | exec 1>&2 | ||
| 23 | |||
| 24 | # Cross platform projects tend to avoid non-ASCII filenames; prevent | ||
| 25 | # them from being added to the repository. We exploit the fact that the | ||
| 26 | # printable range starts at the space character and ends with tilde. | ||
| 27 | if [ "$allownonascii" != "true" ] && | ||
| 28 | # Note that the use of brackets around a tr range is ok here, (it's | ||
| 29 | # even required, for portability to Solaris 10's /usr/bin/tr), since | ||
| 30 | # the square bracket bytes happen to fall in the designated range. | ||
| 31 | test $(git diff-index --cached --name-only --diff-filter=A -z $against | | ||
| 32 | LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 | ||
| 33 | then | ||
| 34 | cat <<\EOF | ||
| 35 | Error: Attempt to add a non-ASCII file name. | ||
| 36 | |||
| 37 | This can cause problems if you want to work with people on other platforms. | ||
| 38 | |||
| 39 | To be portable it is advisable to rename the file. | ||
| 40 | |||
| 41 | If you know what you are doing you can disable this check using: | ||
| 42 | |||
| 43 | git config hooks.allownonascii true | ||
| 44 | EOF | ||
| 45 | exit 1 | ||
| 46 | fi | ||
| 47 | |||
| 48 | # If there are whitespace errors, print the offending file names and fail. | ||
| 49 | exec git diff-index --check --cached $against -- | ||
internal/control/repos/acme/site.git/hooks/pre-merge-commit.sample added +13
| @@ -0,0 +1,13 @@ | |||
| 1 | #!/bin/sh | ||
| 2 | # | ||
| 3 | # An example hook script to verify what is about to be committed. | ||
| 4 | # Called by "git merge" with no arguments. The hook should | ||
| 5 | # exit with non-zero status after issuing an appropriate message to | ||
| 6 | # stderr if it wants to stop the merge commit. | ||
| 7 | # | ||
| 8 | # To enable this hook, rename this file to "pre-merge-commit". | ||
| 9 | |||
| 10 | . git-sh-setup | ||
| 11 | test -x "$GIT_DIR/hooks/pre-commit" && | ||
| 12 | exec "$GIT_DIR/hooks/pre-commit" | ||
| 13 | : | ||
internal/control/repos/acme/site.git/hooks/pre-push.sample added +53
| @@ -0,0 +1,53 @@ | |||
| 1 | #!/bin/sh | ||
| 2 | |||
| 3 | # An example hook script to verify what is about to be pushed. Called by "git | ||
| 4 | # push" after it has checked the remote status, but before anything has been | ||
| 5 | # pushed. If this script exits with a non-zero status nothing will be pushed. | ||
| 6 | # | ||
| 7 | # This hook is called with the following parameters: | ||
| 8 | # | ||
| 9 | # $1 -- Name of the remote to which the push is being done | ||
| 10 | # $2 -- URL to which the push is being done | ||
| 11 | # | ||
| 12 | # If pushing without using a named remote those arguments will be equal. | ||
| 13 | # | ||
| 14 | # Information about the commits which are being pushed is supplied as lines to | ||
| 15 | # the standard input in the form: | ||
| 16 | # | ||
| 17 | # <local ref> <local oid> <remote ref> <remote oid> | ||
| 18 | # | ||
| 19 | # This sample shows how to prevent push of commits where the log message starts | ||
| 20 | # with "WIP" (work in progress). | ||
| 21 | |||
| 22 | remote="$1" | ||
| 23 | url="$2" | ||
| 24 | |||
| 25 | zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0') | ||
| 26 | |||
| 27 | while read local_ref local_oid remote_ref remote_oid | ||
| 28 | do | ||
| 29 | if test "$local_oid" = "$zero" | ||
| 30 | then | ||
| 31 | # Handle delete | ||
| 32 | : | ||
| 33 | else | ||
| 34 | if test "$remote_oid" = "$zero" | ||
| 35 | then | ||
| 36 | # New branch, examine all commits | ||
| 37 | range="$local_oid" | ||
| 38 | else | ||
| 39 | # Update to existing branch, examine new commits | ||
| 40 | range="$remote_oid..$local_oid" | ||
| 41 | fi | ||
| 42 | |||
| 43 | # Check for WIP commit | ||
| 44 | commit=$(git rev-list -n 1 --grep '^WIP' "$range") | ||
| 45 | if test -n "$commit" | ||
| 46 | then | ||
| 47 | echo >&2 "Found WIP commit in $local_ref, not pushing" | ||
| 48 | exit 1 | ||
| 49 | fi | ||
| 50 | fi | ||
| 51 | done | ||
| 52 | |||
| 53 | exit 0 | ||
internal/control/repos/acme/site.git/hooks/pre-rebase.sample added +169
| @@ -0,0 +1,169 @@ | |||
| 1 | #!/bin/sh | ||
| 2 | # | ||
| 3 | # Copyright (c) 2006, 2008 Junio C Hamano | ||
| 4 | # | ||
| 5 | # The "pre-rebase" hook is run just before "git rebase" starts doing | ||
| 6 | # its job, and can prevent the command from running by exiting with | ||
| 7 | # non-zero status. | ||
| 8 | # | ||
| 9 | # The hook is called with the following parameters: | ||
| 10 | # | ||
| 11 | # $1 -- the upstream the series was forked from. | ||
| 12 | # $2 -- the branch being rebased (or empty when rebasing the current branch). | ||
| 13 | # | ||
| 14 | # This sample shows how to prevent topic branches that are already | ||
| 15 | # merged to 'next' branch from getting rebased, because allowing it | ||
| 16 | # would result in rebasing already published history. | ||
| 17 | |||
| 18 | publish=next | ||
| 19 | basebranch="$1" | ||
| 20 | if test "$#" = 2 | ||
| 21 | then | ||
| 22 | topic="refs/heads/$2" | ||
| 23 | else | ||
| 24 | topic=`git symbolic-ref HEAD` || | ||
| 25 | exit 0 ;# we do not interrupt rebasing detached HEAD | ||
| 26 | fi | ||
| 27 | |||
| 28 | case "$topic" in | ||
| 29 | refs/heads/??/*) | ||
| 30 | ;; | ||
| 31 | *) | ||
| 32 | exit 0 ;# we do not interrupt others. | ||
| 33 | ;; | ||
| 34 | esac | ||
| 35 | |||
| 36 | # Now we are dealing with a topic branch being rebased | ||
| 37 | # on top of master. Is it OK to rebase it? | ||
| 38 | |||
| 39 | # Does the topic really exist? | ||
| 40 | git show-ref -q "$topic" || { | ||
| 41 | echo >&2 "No such branch $topic" | ||
| 42 | exit 1 | ||
| 43 | } | ||
| 44 | |||
| 45 | # Is topic fully merged to master? | ||
| 46 | not_in_master=`git rev-list --pretty=oneline ^master "$topic"` | ||
| 47 | if test -z "$not_in_master" | ||
| 48 | then | ||
| 49 | echo >&2 "$topic is fully merged to master; better remove it." | ||
| 50 | exit 1 ;# we could allow it, but there is no point. | ||
| 51 | fi | ||
| 52 | |||
| 53 | # Is topic ever merged to next? If so you should not be rebasing it. | ||
| 54 | only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` | ||
| 55 | only_next_2=`git rev-list ^master ${publish} | sort` | ||
| 56 | if test "$only_next_1" = "$only_next_2" | ||
| 57 | then | ||
| 58 | not_in_topic=`git rev-list "^$topic" master` | ||
| 59 | if test -z "$not_in_topic" | ||
| 60 | then | ||
| 61 | echo >&2 "$topic is already up to date with master" | ||
| 62 | exit 1 ;# we could allow it, but there is no point. | ||
| 63 | else | ||
| 64 | exit 0 | ||
| 65 | fi | ||
| 66 | else | ||
| 67 | not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` | ||
| 68 | /usr/bin/perl -e ' | ||
| 69 | my $topic = $ARGV[0]; | ||
| 70 | my $msg = "* $topic has commits already merged to public branch:\n"; | ||
| 71 | my (%not_in_next) = map { | ||
| 72 | /^([0-9a-f]+) /; | ||
| 73 | ($1 => 1); | ||
| 74 | } split(/\n/, $ARGV[1]); | ||
| 75 | for my $elem (map { | ||
| 76 | /^([0-9a-f]+) (.*)$/; | ||
| 77 | [$1 => $2]; | ||
| 78 | } split(/\n/, $ARGV[2])) { | ||
| 79 | if (!exists $not_in_next{$elem->[0]}) { | ||
| 80 | if ($msg) { | ||
| 81 | print STDERR $msg; | ||
| 82 | undef $msg; | ||
| 83 | } | ||
| 84 | print STDERR " $elem->[1]\n"; | ||
| 85 | } | ||
| 86 | } | ||
| 87 | ' "$topic" "$not_in_next" "$not_in_master" | ||
| 88 | exit 1 | ||
| 89 | fi | ||
| 90 | |||
| 91 | <<\DOC_END | ||
| 92 | |||
| 93 | This sample hook safeguards topic branches that have been | ||
| 94 | published from being rewound. | ||
| 95 | |||
| 96 | The workflow assumed here is: | ||
| 97 | |||
| 98 | * Once a topic branch forks from "master", "master" is never | ||
| 99 | merged into it again (either directly or indirectly). | ||
| 100 | |||
| 101 | * Once a topic branch is fully cooked and merged into "master", | ||
| 102 | it is deleted. If you need to build on top of it to correct | ||
| 103 | earlier mistakes, a new topic branch is created by forking at | ||
| 104 | the tip of the "master". This is not strictly necessary, but | ||
| 105 | it makes it easier to keep your history simple. | ||
| 106 | |||
| 107 | * Whenever you need to test or publish your changes to topic | ||
| 108 | branches, merge them into "next" branch. | ||
| 109 | |||
| 110 | The script, being an example, hardcodes the publish branch name | ||
| 111 | to be "next", but it is trivial to make it configurable via | ||
| 112 | $GIT_DIR/config mechanism. | ||
| 113 | |||
| 114 | With this workflow, you would want to know: | ||
| 115 | |||
| 116 | (1) ... if a topic branch has ever been merged to "next". Young | ||
| 117 | topic branches can have stupid mistakes you would rather | ||
| 118 | clean up before publishing, and things that have not been | ||
| 119 | merged into other branches can be easily rebased without | ||
| 120 | affecting other people. But once it is published, you would | ||
| 121 | not want to rewind it. | ||
| 122 | |||
| 123 | (2) ... if a topic branch has been fully merged to "master". | ||
| 124 | Then you can delete it. More importantly, you should not | ||
| 125 | build on top of it -- other people may already want to | ||
| 126 | change things related to the topic as patches against your | ||
| 127 | "master", so if you need further changes, it is better to | ||
| 128 | fork the topic (perhaps with the same name) afresh from the | ||
| 129 | tip of "master". | ||
| 130 | |||
| 131 | Let's look at this example: | ||
| 132 | |||
| 133 | o---o---o---o---o---o---o---o---o---o "next" | ||
| 134 | / / / / | ||
| 135 | / a---a---b A / / | ||
| 136 | / / / / | ||
| 137 | / / c---c---c---c B / | ||
| 138 | / / / \ / | ||
| 139 | / / / b---b C \ / | ||
| 140 | / / / / \ / | ||
| 141 | ---o---o---o---o---o---o---o---o---o---o---o "master" | ||
| 142 | |||
| 143 | |||
| 144 | A, B and C are topic branches. | ||
| 145 | |||
| 146 | * A has one fix since it was merged up to "next". | ||
| 147 | |||
| 148 | * B has finished. It has been fully merged up to "master" and "next", | ||
| 149 | and is ready to be deleted. | ||
| 150 | |||
| 151 | * C has not merged to "next" at all. | ||
| 152 | |||
| 153 | We would want to allow C to be rebased, refuse A, and encourage | ||
| 154 | B to be deleted. | ||
| 155 | |||
| 156 | To compute (1): | ||
| 157 | |||
| 158 | git rev-list ^master ^topic next | ||
| 159 | git rev-list ^master next | ||
| 160 | |||
| 161 | if these match, topic has not merged in next at all. | ||
| 162 | |||
| 163 | To compute (2): | ||
| 164 | |||
| 165 | git rev-list master..topic | ||
| 166 | |||
| 167 | if this is empty, it is fully merged to "master". | ||
| 168 | |||
| 169 | DOC_END | ||
internal/control/repos/acme/site.git/hooks/pre-receive.sample added +24
| @@ -0,0 +1,24 @@ | |||
| 1 | #!/bin/sh | ||
| 2 | # | ||
| 3 | # An example hook script to make use of push options. | ||
| 4 | # The example simply echoes all push options that start with 'echoback=' | ||
| 5 | # and rejects all pushes when the "reject" push option is used. | ||
| 6 | # | ||
| 7 | # To enable this hook, rename this file to "pre-receive". | ||
| 8 | |||
| 9 | if test -n "$GIT_PUSH_OPTION_COUNT" | ||
| 10 | then | ||
| 11 | i=0 | ||
| 12 | while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" | ||
| 13 | do | ||
| 14 | eval "value=\$GIT_PUSH_OPTION_$i" | ||
| 15 | case "$value" in | ||
| 16 | echoback=*) | ||
| 17 | echo "echo from the pre-receive-hook: ${value#*=}" >&2 | ||
| 18 | ;; | ||
| 19 | reject) | ||
| 20 | exit 1 | ||
| 21 | esac | ||
| 22 | i=$((i + 1)) | ||
| 23 | done | ||
| 24 | fi | ||
internal/control/repos/acme/site.git/hooks/prepare-commit-msg.sample added +42
| @@ -0,0 +1,42 @@ | |||
| 1 | #!/bin/sh | ||
| 2 | # | ||
| 3 | # An example hook script to prepare the commit log message. | ||
| 4 | # Called by "git commit" with the name of the file that has the | ||
| 5 | # commit message, followed by the description of the commit | ||
| 6 | # message's source. The hook's purpose is to edit the commit | ||
| 7 | # message file. If the hook fails with a non-zero status, | ||
| 8 | # the commit is aborted. | ||
| 9 | # | ||
| 10 | # To enable this hook, rename this file to "prepare-commit-msg". | ||
| 11 | |||
| 12 | # This hook includes three examples. The first one removes the | ||
| 13 | # "# Please enter the commit message..." help message. | ||
| 14 | # | ||
| 15 | # The second includes the output of "git diff --name-status -r" | ||
| 16 | # into the message, just before the "git status" output. It is | ||
| 17 | # commented because it doesn't cope with --amend or with squashed | ||
| 18 | # commits. | ||
| 19 | # | ||
| 20 | # The third example adds a Signed-off-by line to the message, that can | ||
| 21 | # still be edited. This is rarely a good idea. | ||
| 22 | |||
| 23 | COMMIT_MSG_FILE=$1 | ||
| 24 | COMMIT_SOURCE=$2 | ||
| 25 | SHA1=$3 | ||
| 26 | |||
| 27 | /usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" | ||
| 28 | |||
| 29 | # case "$COMMIT_SOURCE,$SHA1" in | ||
| 30 | # ,|template,) | ||
| 31 | # /usr/bin/perl -i.bak -pe ' | ||
| 32 | # print "\n" . `git diff --cached --name-status -r` | ||
| 33 | # if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; | ||
| 34 | # *) ;; | ||
| 35 | # esac | ||
| 36 | |||
| 37 | # SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') | ||
| 38 | # git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" | ||
| 39 | # if test -z "$COMMIT_SOURCE" | ||
| 40 | # then | ||
| 41 | # /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" | ||
| 42 | # fi | ||
internal/control/repos/acme/site.git/hooks/push-to-checkout.sample added +78
| @@ -0,0 +1,78 @@ | |||
| 1 | #!/bin/sh | ||
| 2 | |||
| 3 | # An example hook script to update a checked-out tree on a git push. | ||
| 4 | # | ||
| 5 | # This hook is invoked by git-receive-pack(1) when it reacts to git | ||
| 6 | # push and updates reference(s) in its repository, and when the push | ||
| 7 | # tries to update the branch that is currently checked out and the | ||
| 8 | # receive.denyCurrentBranch configuration variable is set to | ||
| 9 | # updateInstead. | ||
| 10 | # | ||
| 11 | # By default, such a push is refused if the working tree and the index | ||
| 12 | # of the remote repository has any difference from the currently | ||
| 13 | # checked out commit; when both the working tree and the index match | ||
| 14 | # the current commit, they are updated to match the newly pushed tip | ||
| 15 | # of the branch. This hook is to be used to override the default | ||
| 16 | # behaviour; however the code below reimplements the default behaviour | ||
| 17 | # as a starting point for convenient modification. | ||
| 18 | # | ||
| 19 | # The hook receives the commit with which the tip of the current | ||
| 20 | # branch is going to be updated: | ||
| 21 | commit=$1 | ||
| 22 | |||
| 23 | # It can exit with a non-zero status to refuse the push (when it does | ||
| 24 | # so, it must not modify the index or the working tree). | ||
| 25 | die () { | ||
| 26 | echo >&2 "$*" | ||
| 27 | exit 1 | ||
| 28 | } | ||
| 29 | |||
| 30 | # Or it can make any necessary changes to the working tree and to the | ||
| 31 | # index to bring them to the desired state when the tip of the current | ||
| 32 | # branch is updated to the new commit, and exit with a zero status. | ||
| 33 | # | ||
| 34 | # For example, the hook can simply run git read-tree -u -m HEAD "$1" | ||
| 35 | # in order to emulate git fetch that is run in the reverse direction | ||
| 36 | # with git push, as the two-tree form of git read-tree -u -m is | ||
| 37 | # essentially the same as git switch or git checkout that switches | ||
| 38 | # branches while keeping the local changes in the working tree that do | ||
| 39 | # not interfere with the difference between the branches. | ||
| 40 | |||
| 41 | # The below is a more-or-less exact translation to shell of the C code | ||
| 42 | # for the default behaviour for git's push-to-checkout hook defined in | ||
| 43 | # the push_to_deploy() function in builtin/receive-pack.c. | ||
| 44 | # | ||
| 45 | # Note that the hook will be executed from the repository directory, | ||
| 46 | # not from the working tree, so if you want to perform operations on | ||
| 47 | # the working tree, you will have to adapt your code accordingly, e.g. | ||
| 48 | # by adding "cd .." or using relative paths. | ||
| 49 | |||
| 50 | if ! git update-index -q --ignore-submodules --refresh | ||
| 51 | then | ||
| 52 | die "Up-to-date check failed" | ||
| 53 | fi | ||
| 54 | |||
| 55 | if ! git diff-files --quiet --ignore-submodules -- | ||
| 56 | then | ||
| 57 | die "Working directory has unstaged changes" | ||
| 58 | fi | ||
| 59 | |||
| 60 | # This is a rough translation of: | ||
| 61 | # | ||
| 62 | # head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX | ||
| 63 | if git cat-file -e HEAD 2>/dev/null | ||
| 64 | then | ||
| 65 | head=HEAD | ||
| 66 | else | ||
| 67 | head=$(git hash-object -t tree --stdin </dev/null) | ||
| 68 | fi | ||
| 69 | |||
| 70 | if ! git diff-index --quiet --cached --ignore-submodules $head -- | ||
| 71 | then | ||
| 72 | die "Working directory has staged changes" | ||
| 73 | fi | ||
| 74 | |||
| 75 | if ! git read-tree -u -m "$commit" | ||
| 76 | then | ||
| 77 | die "Could not update working tree to new HEAD" | ||
| 78 | fi | ||
internal/control/repos/acme/site.git/hooks/sendemail-validate.sample added +77
| @@ -0,0 +1,77 @@ | |||
| 1 | #!/bin/sh | ||
| 2 | |||
| 3 | # An example hook script to validate a patch (and/or patch series) before | ||
| 4 | # sending it via email. | ||
| 5 | # | ||
| 6 | # The hook should exit with non-zero status after issuing an appropriate | ||
| 7 | # message if it wants to prevent the email(s) from being sent. | ||
| 8 | # | ||
| 9 | # To enable this hook, rename this file to "sendemail-validate". | ||
| 10 | # | ||
| 11 | # By default, it will only check that the patch(es) can be applied on top of | ||
| 12 | # the default upstream branch without conflicts in a secondary worktree. After | ||
| 13 | # validation (successful or not) of the last patch of a series, the worktree | ||
| 14 | # will be deleted. | ||
| 15 | # | ||
| 16 | # The following config variables can be set to change the default remote and | ||
| 17 | # remote ref that are used to apply the patches against: | ||
| 18 | # | ||
| 19 | # sendemail.validateRemote (default: origin) | ||
| 20 | # sendemail.validateRemoteRef (default: HEAD) | ||
| 21 | # | ||
| 22 | # Replace the TODO placeholders with appropriate checks according to your | ||
| 23 | # needs. | ||
| 24 | |||
| 25 | validate_cover_letter () { | ||
| 26 | file="$1" | ||
| 27 | # TODO: Replace with appropriate checks (e.g. spell checking). | ||
| 28 | true | ||
| 29 | } | ||
| 30 | |||
| 31 | validate_patch () { | ||
| 32 | file="$1" | ||
| 33 | # Ensure that the patch applies without conflicts. | ||
| 34 | git am -3 "$file" || return | ||
| 35 | # TODO: Replace with appropriate checks for this patch | ||
| 36 | # (e.g. checkpatch.pl). | ||
| 37 | true | ||
| 38 | } | ||
| 39 | |||
| 40 | validate_series () { | ||
| 41 | # TODO: Replace with appropriate checks for the whole series | ||
| 42 | # (e.g. quick build, coding style checks, etc.). | ||
| 43 | true | ||
| 44 | } | ||
| 45 | |||
| 46 | # main ------------------------------------------------------------------------- | ||
| 47 | |||
| 48 | if test "$GIT_SENDEMAIL_FILE_COUNTER" = 1 | ||
| 49 | then | ||
| 50 | remote=$(git config --default origin --get sendemail.validateRemote) && | ||
| 51 | ref=$(git config --default HEAD --get sendemail.validateRemoteRef) && | ||
| 52 | worktree=$(mktemp --tmpdir -d sendemail-validate.XXXXXXX) && | ||
| 53 | git worktree add -fd --checkout "$worktree" "refs/remotes/$remote/$ref" && | ||
| 54 | git config --replace-all sendemail.validateWorktree "$worktree" | ||
| 55 | else | ||
| 56 | worktree=$(git config --get sendemail.validateWorktree) | ||
| 57 | fi || { | ||
| 58 | echo "sendemail-validate: error: failed to prepare worktree" >&2 | ||
| 59 | exit 1 | ||
| 60 | } | ||
| 61 | |||
| 62 | unset GIT_DIR GIT_WORK_TREE | ||
| 63 | cd "$worktree" && | ||
| 64 | |||
| 65 | if grep -q "^diff --git " "$1" | ||
| 66 | then | ||
| 67 | validate_patch "$1" | ||
| 68 | else | ||
| 69 | validate_cover_letter "$1" | ||
| 70 | fi && | ||
| 71 | |||
| 72 | if test "$GIT_SENDEMAIL_FILE_COUNTER" = "$GIT_SENDEMAIL_FILE_TOTAL" | ||
| 73 | then | ||
| 74 | git config --unset-all sendemail.validateWorktree && | ||
| 75 | trap 'git worktree remove -ff "$worktree"' EXIT && | ||
| 76 | validate_series | ||
| 77 | fi | ||
internal/control/repos/acme/site.git/hooks/update.sample added +128
| @@ -0,0 +1,128 @@ | |||
| 1 | #!/bin/sh | ||
| 2 | # | ||
| 3 | # An example hook script to block unannotated tags from entering. | ||
| 4 | # Called by "git receive-pack" with arguments: refname sha1-old sha1-new | ||
| 5 | # | ||
| 6 | # To enable this hook, rename this file to "update". | ||
| 7 | # | ||
| 8 | # Config | ||
| 9 | # ------ | ||
| 10 | # hooks.allowunannotated | ||
| 11 | # This boolean sets whether unannotated tags will be allowed into the | ||
| 12 | # repository. By default they won't be. | ||
| 13 | # hooks.allowdeletetag | ||
| 14 | # This boolean sets whether deleting tags will be allowed in the | ||
| 15 | # repository. By default they won't be. | ||
| 16 | # hooks.allowmodifytag | ||
| 17 | # This boolean sets whether a tag may be modified after creation. By default | ||
| 18 | # it won't be. | ||
| 19 | # hooks.allowdeletebranch | ||
| 20 | # This boolean sets whether deleting branches will be allowed in the | ||
| 21 | # repository. By default they won't be. | ||
| 22 | # hooks.denycreatebranch | ||
| 23 | # This boolean sets whether remotely creating branches will be denied | ||
| 24 | # in the repository. By default this is allowed. | ||
| 25 | # | ||
| 26 | |||
| 27 | # --- Command line | ||
| 28 | refname="$1" | ||
| 29 | oldrev="$2" | ||
| 30 | newrev="$3" | ||
| 31 | |||
| 32 | # --- Safety check | ||
| 33 | if [ -z "$GIT_DIR" ]; then | ||
| 34 | echo "Don't run this script from the command line." >&2 | ||
| 35 | echo " (if you want, you could supply GIT_DIR then run" >&2 | ||
| 36 | echo " $0 <ref> <oldrev> <newrev>)" >&2 | ||
| 37 | exit 1 | ||
| 38 | fi | ||
| 39 | |||
| 40 | if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then | ||
| 41 | echo "usage: $0 <ref> <oldrev> <newrev>" >&2 | ||
| 42 | exit 1 | ||
| 43 | fi | ||
| 44 | |||
| 45 | # --- Config | ||
| 46 | allowunannotated=$(git config --type=bool hooks.allowunannotated) | ||
| 47 | allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch) | ||
| 48 | denycreatebranch=$(git config --type=bool hooks.denycreatebranch) | ||
| 49 | allowdeletetag=$(git config --type=bool hooks.allowdeletetag) | ||
| 50 | allowmodifytag=$(git config --type=bool hooks.allowmodifytag) | ||
| 51 | |||
| 52 | # check for no description | ||
| 53 | projectdesc=$(sed -e '1q' "$GIT_DIR/description") | ||
| 54 | case "$projectdesc" in | ||
| 55 | "Unnamed repository"* | "") | ||
| 56 | echo "*** Project description file hasn't been set" >&2 | ||
| 57 | exit 1 | ||
| 58 | ;; | ||
| 59 | esac | ||
| 60 | |||
| 61 | # --- Check types | ||
| 62 | # if $newrev is 0000...0000, it's a commit to delete a ref. | ||
| 63 | zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0') | ||
| 64 | if [ "$newrev" = "$zero" ]; then | ||
| 65 | newrev_type=delete | ||
| 66 | else | ||
| 67 | newrev_type=$(git cat-file -t $newrev) | ||
| 68 | fi | ||
| 69 | |||
| 70 | case "$refname","$newrev_type" in | ||
| 71 | refs/tags/*,commit) | ||
| 72 | # un-annotated tag | ||
| 73 | short_refname=${refname##refs/tags/} | ||
| 74 | if [ "$allowunannotated" != "true" ]; then | ||
| 75 | echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 | ||
| 76 | echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 | ||
| 77 | exit 1 | ||
| 78 | fi | ||
| 79 | ;; | ||
| 80 | refs/tags/*,delete) | ||
| 81 | # delete tag | ||
| 82 | if [ "$allowdeletetag" != "true" ]; then | ||
| 83 | echo "*** Deleting a tag is not allowed in this repository" >&2 | ||
| 84 | exit 1 | ||
| 85 | fi | ||
| 86 | ;; | ||
| 87 | refs/tags/*,tag) | ||
| 88 | # annotated tag | ||
| 89 | if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 | ||
| 90 | then | ||
| 91 | echo "*** Tag '$refname' already exists." >&2 | ||
| 92 | echo "*** Modifying a tag is not allowed in this repository." >&2 | ||
| 93 | exit 1 | ||
| 94 | fi | ||
| 95 | ;; | ||
| 96 | refs/heads/*,commit) | ||
| 97 | # branch | ||
| 98 | if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then | ||
| 99 | echo "*** Creating a branch is not allowed in this repository" >&2 | ||
| 100 | exit 1 | ||
| 101 | fi | ||
| 102 | ;; | ||
| 103 | refs/heads/*,delete) | ||
| 104 | # delete branch | ||
| 105 | if [ "$allowdeletebranch" != "true" ]; then | ||
| 106 | echo "*** Deleting a branch is not allowed in this repository" >&2 | ||
| 107 | exit 1 | ||
| 108 | fi | ||
| 109 | ;; | ||
| 110 | refs/remotes/*,commit) | ||
| 111 | # tracking branch | ||
| 112 | ;; | ||
| 113 | refs/remotes/*,delete) | ||
| 114 | # delete tracking branch | ||
| 115 | if [ "$allowdeletebranch" != "true" ]; then | ||
| 116 | echo "*** Deleting a tracking branch is not allowed in this repository" >&2 | ||
| 117 | exit 1 | ||
| 118 | fi | ||
| 119 | ;; | ||
| 120 | *) | ||
| 121 | # Anything else (is there anything else?) | ||
| 122 | echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 | ||
| 123 | exit 1 | ||
| 124 | ;; | ||
| 125 | esac | ||
| 126 | |||
| 127 | # --- Finished | ||
| 128 | exit 0 | ||
internal/control/repos/acme/site.git/info/exclude added +6
| @@ -0,0 +1,6 @@ | |||
| 1 | # git ls-files --others --exclude-from=.git/info/exclude | ||
| 2 | # Lines that start with '#' are comments. | ||
| 3 | # For a project mostly in C, the following would be a good set of | ||
| 4 | # exclude patterns (uncomment them if you want to use them): | ||
| 5 | # *.[oa] | ||
| 6 | # *~ | ||
internal/control/stage3org_test.go added +73
| @@ -0,0 +1,73 @@ | |||
| 1 | package control | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "strings" | ||
| 5 | "testing" | ||
| 6 | |||
| 7 | "gitbay.org/gitbay/internal/store" | ||
| 8 | ) | ||
| 9 | |||
| 10 | // orgScreensFixture is alice with a repository carrying a label and a | ||
| 11 | // milestone, and an org with a label, a milestone, a repository and a | ||
| 12 | // team that holds a grant on it. | ||
| 13 | func orgScreensFixture(t *testing.T) (*store.Store, store.Repo, store.User) { | ||
| 14 | t.Helper() | ||
| 15 | st, repo, uid := newQueueTestRepo(t) | ||
| 16 | u := store.User{ID: uid, Username: "alice"} | ||
| 17 | p := repo.Path() | ||
| 18 | dispatchAs(t, st, u, "", "label", "set", p, "bug", "--color", "d73a4a") | ||
| 19 | dispatchAs(t, st, u, "", "milestone", "create", p, "v1", "--due", "2030-01-01") | ||
| 20 | dispatchAs(t, st, u, "", "org", "create", "acme") | ||
| 21 | dispatchAs(t, st, u, "", "org", "label", "set", "acme", "triage", "--color", "0e8a16") | ||
| 22 | dispatchAs(t, st, u, "", "org", "milestone", "create", "acme", "Q4") | ||
| 23 | dispatchAs(t, st, u, "", "repo", "create", "acme/site") | ||
| 24 | dispatchAs(t, st, u, "", "org", "team", "create", "acme", "core") | ||
| 25 | dispatchAs(t, st, u, "", "org", "team", "add", "acme", "core", "alice") | ||
| 26 | dispatchAs(t, st, u, "", "org", "team", "grant", "acme", "core", "acme/site", "write") | ||
| 27 | return st, repo, u | ||
| 28 | } | ||
| 29 | |||
| 30 | func TestOrgPlainPinned(t *testing.T) { | ||
| 31 | st, repo, u := orgScreensFixture(t) | ||
| 32 | p := repo.Path() | ||
| 33 | for name, argv := range map[string][]string{ | ||
| 34 | "org-list": {"org", "list"}, | ||
| 35 | "org-show": {"org", "show", "acme"}, | ||
| 36 | "org-members-list": {"org", "members", "list", "acme"}, | ||
| 37 | "org-label-list": {"org", "label", "list", "acme"}, | ||
| 38 | "label-list": {"label", "list", p}, | ||
| 39 | "milestone-list": {"milestone", "list", p}, | ||
| 40 | "org-milestone-list": {"org", "milestone", "list", "acme"}, | ||
| 41 | "org-team-list": {"org", "team", "list", "acme"}, | ||
| 42 | "org-team-show": {"org", "team", "show", "acme", "core"}, | ||
| 43 | } { | ||
| 44 | pinPlain(t, name, dispatchAs(t, st, u, "", argv...)) | ||
| 45 | } | ||
| 46 | } | ||
| 47 | |||
| 48 | func TestOrgScreens(t *testing.T) { | ||
| 49 | st, repo, u := orgScreensFixture(t) | ||
| 50 | p := repo.Path() | ||
| 51 | for _, tc := range []struct { | ||
| 52 | argv []string | ||
| 53 | want []string | ||
| 54 | }{ | ||
| 55 | {[]string{"org", "list"}, []string{"Organizations (1)\nacme admin\n"}}, | ||
| 56 | {[]string{"org", "show", "acme"}, []string{"Org: acme\n", "Members (1)\nalice admin\n"}}, | ||
| 57 | {[]string{"org", "members", "list", "acme"}, []string{"Members (1)\n"}}, | ||
| 58 | {[]string{"org", "label", "list", "acme"}, []string{"Labels (1)\n", "triage", "0 issues · 0 MRs"}}, | ||
| 59 | {[]string{"label", "list", p}, []string{"Labels (1)\n", "bug", "0 issues · 0 MRs"}}, | ||
| 60 | {[]string{"milestone", "list", p}, []string{"Milestones (1)\n", "v1", "0/0 closed"}}, | ||
| 61 | {[]string{"org", "milestone", "list", "acme"}, []string{"Milestones (1)\n", "Q4"}}, | ||
| 62 | {[]string{"org", "team", "list", "acme"}, []string{"Teams (1)\ncore\n"}}, | ||
| 63 | {[]string{"org", "team", "show", "acme", "core"}, []string{"Team: acme/core\n", "Members: alice\n", "Grants (1)\nacme/site write\n"}}, | ||
| 64 | } { | ||
| 65 | out := atTerminal(t, st, u, tc.argv...) | ||
| 66 | for _, w := range tc.want { | ||
| 67 | if !strings.Contains(out, w) { | ||
| 68 | t.Errorf("%v: missing %q in:\n%s", tc.argv, w, out) | ||
| 69 | } | ||
| 70 | } | ||
| 71 | checkLegend(t, out) | ||
| 72 | } | ||
| 73 | } | ||
internal/control/teams.go +26 −2
| @@ -154,12 +154,22 @@ func runTeamList(c *Ctx, args []string) int { | |||
| 154 | for _, t := range teams { | 154 | for _, t := range teams { |
| 155 | names = append(names, t.Name) | 155 | names = append(names, t.Name) |
| 156 | } | 156 | } |
| 157 | return c.emit(names, func(w io.Writer) { | 157 | return c.emitView(names, func(w io.Writer) { |
| 158 | tb := c.table(w, "TEAM") | 158 | tb := c.table(w, "TEAM") |
| 159 | for _, n := range names { | 159 | for _, n := range names { |
| 160 | tb.row(cRef(n)) | 160 | tb.row(cRef(n)) |
| 161 | } | 161 | } |
| 162 | tb.flush() | 162 | tb.flush() |
| 163 | }, func() screen { | ||
| 164 | rows := make([]row, len(names)) | ||
| 165 | for i, n := range names { | ||
| 166 | rows[i] = rowOf(cRef(n)) | ||
| 167 | } | ||
| 168 | s := listScreen("Teams", rows) | ||
| 169 | if len(names) > 0 { | ||
| 170 | s.actions = []action{{"Read", []string{"org", "team", "show", org.Name, names[0]}}} | ||
| 171 | } | ||
| 172 | return s | ||
| 163 | }) | 173 | }) |
| 164 | } | 174 | } |
| 165 | 175 | ||
| @@ -188,7 +198,7 @@ func runTeamShow(c *Ctx, args []string) int { | |||
| 188 | Members []string `json:"members,omitempty"` | 198 | Members []string `json:"members,omitempty"` |
| 189 | Grants []store.TeamGrant `json:"grants,omitempty"` | 199 | Grants []store.TeamGrant `json:"grants,omitempty"` |
| 190 | }{team.Name, members, grants} | 200 | }{team.Name, members, grants} |
| 191 | return c.emit(d, func(w io.Writer) { | 201 | return c.emitView(d, func(w io.Writer) { |
| 192 | v := c.view(w) | 202 | v := c.view(w) |
| 193 | v.title(org.Name+"/"+team.Name, "", "") | 203 | v.title(org.Name+"/"+team.Name, "", "") |
| 194 | v.fields("members", strings.Join(members, ", ")) | 204 | v.fields("members", strings.Join(members, ", ")) |
| @@ -200,6 +210,20 @@ func runTeamShow(c *Ctx, args []string) int { | |||
| 200 | } | 210 | } |
| 201 | tb.flush() | 211 | tb.flush() |
| 202 | } | 212 | } |
| 213 | }, func() screen { | ||
| 214 | grantsSec := section{title: "Grants", n: len(grants)} | ||
| 215 | for _, g := range grants { | ||
| 216 | grantsSec.rows = append(grantsSec.rows, rowOf(cLink(g.RepoPath, c.siteURL(g.RepoPath)), cState(g.Role))) | ||
| 217 | } | ||
| 218 | s := screen{fields: []field{{"Team", []cell{cText(org.Name + "/" + team.Name)}}}, sections: []section{grantsSec}} | ||
| 219 | if len(members) > 0 { | ||
| 220 | s.fields = append(s.fields, field{"Members", []cell{cText(strings.Join(members, ", "))}}) | ||
| 221 | } | ||
| 222 | s.actions = []action{ | ||
| 223 | {"Team", []string{"org", "team", "add", org.Name, team.Name, "<user>"}}, | ||
| 224 | {"Team", []string{"org", "team", "grant", org.Name, team.Name, "<owner/name>", "write"}}, | ||
| 225 | } | ||
| 226 | return s | ||
| 203 | }) | 227 | }) |
| 204 | } | 228 | } |
| 205 | 229 | ||
internal/control/testdata/plain/label-list.txt added +1
| @@ -0,0 +1 @@ | |||
| 1 | bug #d73a4a 0 0 | ||
internal/control/testdata/plain/milestone-list.txt added +1
| @@ -0,0 +1 @@ | |||
| 1 | v1 open due 2030-01-01 0 open, 0 closed | ||
internal/control/testdata/plain/org-label-list.txt added +1
| @@ -0,0 +1 @@ | |||
| 1 | triage #0e8a16 0 0 | ||
internal/control/testdata/plain/org-list.txt added +1
| @@ -0,0 +1 @@ | |||
| 1 | acme admin | ||
internal/control/testdata/plain/org-members-list.txt added +4
| @@ -0,0 +1,4 @@ | |||
| 1 | acme | ||
| 2 | |||
| 3 | members: | ||
| 4 | alice admin | ||
internal/control/testdata/plain/org-milestone-list.txt added +1
| @@ -0,0 +1 @@ | |||
| 1 | Q4 open due - 0 open, 0 closed org | ||
internal/control/testdata/plain/org-show.txt added +4
| @@ -0,0 +1,4 @@ | |||
| 1 | acme | ||
| 2 | |||
| 3 | members: | ||
| 4 | alice admin | ||
internal/control/testdata/plain/org-team-list.txt added +1
| @@ -0,0 +1 @@ | |||
| 1 | core | ||
internal/control/testdata/plain/org-team-show.txt added +6
| @@ -0,0 +1,6 @@ | |||
| 1 | acme/core | ||
| 2 | |||
| 3 | members alice | ||
| 4 | |||
| 5 | grants: | ||
| 6 | acme/site write | ||