summaryrefslogtreecommitdiff
path: root/lib/gitano/command.lua
blob: c8f537a90577f6c89c27314c08aae30075c89ea9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
-- gitano.command
--
-- Gitano command processing
--
-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>

local log = require 'gitano.log'
local util = require 'gitano.util'
local repository = require 'gitano.repository'

local sp = require "luxio.subprocess"

local cmds = {}

local function register_cmd(cmdname, short, helptext,
			    validate_fn, prep_fn, run_fn,
			    takes_repo, hidden, is_admin)
   --[[
   log.ddebug("Register command", cmdname)
   if takes_repo then
      log.ddebug("  =>  Takes a repo")
   end
   --]]
   if cmds[cmdname] then
      log.warn("Attempt to double-register", cmdname)
      return false, "Attempt to double-register " .. cmdname
   end
   cmds[cmdname] = {
      name = cmdname,
      validate = validate_fn,
      prep = prep_fn,
      run = run_fn,
      takes_repo = takes_repo,
      hidden = hidden,
      admin = is_admin,
      short = short,
      helptext = helptext
   }
   cmds[#cmds+1] = cmdname
   table.sort(cmds)
   return true
end

local function get_cmd(cmdname)
   local cmd = cmds[cmdname]
   if not cmd then
      return nil, "Unknown command " .. cmdname
   end
   return {
      validate = cmd.validate,
      prep = cmd.prep,
      run = cmd.run,
      takes_repo = cmd.takes_repo,
   }
end

local builtin_help_short = "Ask for help"
local builtin_help_helptext = [[
usage: help [admin|command]

Without the command argument, lists all visible commands.  With the
command argument, provides detailed help about the given command.

If the command argument is specifically 'admin' then list the
admin commands instead of the normal commands.  If it is 'all' then
list all the commands, even the hidden commands.
]]

local function builtin_help_validate(config, repo, cmdline)
   if #cmdline > 2 then
      log.error("usage: help [admin|command]")
      return false
   end
   if #cmdline == 2 then
      if cmdline[2] == "all" or cmdline[2] == "admin" then
	 return true
      end
      if not cmds[cmdline[2]] then
	 log.error("Unknown command:", help)
	 return false
      end
   end
   return true
end

local function builtin_help_prep(config, repo, cmdline, context)
   return "allow", "Always allowed to ask for help"
end

local function builtin_help_run(config, repo, cmdline, env)
   local function do_want(cmd)
      if cmdline[2] == "all" then
	 return true
      end
      if cmdline[2] == "admin" then
	 return cmd.admin
      end
      return not (cmd.hidden or cmd.admin)
   end
   local function do_sep(cmd)
      local first = cmd.hidden and "-H" or "--"
      local second = cmd.admin and "A-" or "--"
      return first .. second
   end
   if #cmdline == 1 or cmdline[2] == "admin" or cmdline[2] == "all" then
      -- List all commands
      local maxcmdn = 0
      for i = 1, #cmds do
	 local cmd = cmds[cmds[i]]
	 local wanted = do_want(cmd)
	 if wanted then
	    if #cmd.name > maxcmdn then
	       maxcmdn = #cmd.name
	    end
	 end
      end
      for i = 1, #cmds do
	 local cmd = cmds[cmds[i]]
	 local wanted = do_want(cmd)
	 if wanted then
	    local gap = (" "):rep(maxcmdn - #cmd.name)
	    local desc = (cmd.short or "No description")
	    if cmd.takes_repo then
	       desc = desc .. " (Takes a repo)"
	    end
	    log.state(gap .. cmd.name, do_sep(cmd), desc)
	 end
      end
   else
      local cmd = cmds[cmdline[2]]
      local desc = (cmd.short or "No description")
      if cmd.takes_repo then
	 desc = desc .. " (Takes a repo)"
      end
      log.state(cmd.name, do_sep(cmd), desc)
      if cmd.helptext then
	 log.state("")
	 for line in (cmd.helptext):gmatch("([^\n]*)\n") do
	    log.state("=>", line)
	 end
      end
   end
   return "exit", 0
end

assert(register_cmd("help", builtin_help_short, builtin_help_helptext,
		    builtin_help_validate, builtin_help_prep,
		    builtin_help_run, false, false))

local function builtin_upload_pack_validate(config, repo, cmdline)
   -- git-upload-pack repo
   if #cmdline > 2 then
      return false
   end
   cmdline[2] = repo:fs_path()
   return true
end

local function builtin_upload_pack_prep(config, repo, cmdline, context)
   if repo.is_nascent then
      return "deny", "Repository " .. repo.name .. " does not exist"
   end
   -- git-upload-pack is always a read operation
   context.operation = "read"
   return repo:run_lace(context)
end

local function builtin_upload_pack_run(config, repo, cmdline, env)
   local cmdcopy = {env=env}
   for i = 1, #cmdline do cmdcopy[i] = cmdline[i] end
   local proc = sp.spawn(cmdcopy)
   return proc:wait()
end

assert(register_cmd("git-upload-pack", nil, nil,
		    builtin_upload_pack_validate,
		    builtin_upload_pack_prep,
		    builtin_upload_pack_run,
		    true, true))

local function builtin_receive_pack_validate(config, repo, cmdline)
   -- git-receive-pack repo
   if #cmdline > 2 then
      return false
   end
   cmdline[2] = repo:fs_path()
   return true
end

local function builtin_receive_pack_prep(config, repo, cmdline, context)
   if repo.is_nascent then
      return "deny", "Repository " .. repo.name .. " does not exist"
   end
   -- git-receive-pack is always a simple write operation
   context.operation = "write"
   return repo:run_lace(context)
end

local function builtin_receive_pack_run(config, repo, cmdline, env)
   local cmdcopy = {env=env}
   for i = 1, #cmdline do cmdcopy[i] = cmdline[i] end
   local proc = sp.spawn(cmdcopy)
   return proc:wait()
end

assert(register_cmd("git-receive-pack", nil, nil,
		    builtin_receive_pack_validate,
		    builtin_receive_pack_prep,
		    builtin_receive_pack_run,
		    true, true))

local builtin_create_short = "Create a new repository"
local builtin_create_helptext = [[
usage: create <reponame> [<owner>]

Create a new repository, optionally setting its owner directly.

In order to create a repository, the site administrators must grant you the
ability in some part of the namespace.  Specifying an owner is equivalent
to creating the repository and then calling set-owner to re-assign it.
]]

local function builtin_create_validate(config, repo, cmdline)
   -- create reponame
   if #cmdline > 3 then
      log.error("usage: create <reponame> [<owner>]")
      return false
   end
   if not repo then
      log.error("No repository?")
      return false
   end
   if repo and not repo.is_nascent then
      log.error("Repository", repo.name, "already exists")
      return false
   end
   return true
end

local function builtin_create_prep(config, repo, cmdline, context)
   context.operation = "createrepo"
   return repo:run_lace(context)
end

local function builtin_create_run(config, repo, cmdline, env)
   -- Realise the repository
   log.chat("Creating repository:", repo.name)
   local ok, msg = repo:realise()
   if not ok then
      log.error(msg)
      return "exit", 1
   end
   local owner = cmdline[3] or env["GITANO_USER"]
   log.chat("Setting repository owner to", owner)
   ok, msg = repo:set_owner(owner)
   if not ok then
      log.error(msg)
      return "exit", 1
   end
   log.chat("Running checks to ensure hooks etc are configured")
   ok, msg = repo:run_checks()
   if not ok then
      log.error(msg)
      return "exit", 1
   end
   log.state("Repository", repo.name, 
	    "created ok.  Remember to configure rules etc.")
   return "exit", 0
end

assert(register_cmd("create", builtin_create_short, builtin_create_helptext,
		    builtin_create_validate, builtin_create_prep, 
		    builtin_create_run, true, false))

local builtin_set_owner_short = "Sets the owner of a repository"
local builtin_set_owner_helptext = [[
usage: set-owner <reponame> <owner>

Set the owner of a repository.  Who is allowed to do this is configured
by the site administrators.  Typically site admins and repository owners
are the only people allowed to change the ownership of a repository.
]]

local function builtin_set_owner_validate(config, repo, cmdline)
   -- set-owner reponame ownername
   if #cmdline ~= 3 then
      log.error("usage: set-owner <reponame> <owner>")
      return false
   end
   if not repo then
      log.error("No repository?")
      return false
   end
   if repo.is_nascent then
      log.error("Repository", repo.name, "does not exist, use create")
      return false
   end
   if not config.users[cmdline[3]] then
      log.error("Unknown user:", cmdline[3])
      return false
   end
   return true
end

local function builtin_set_owner_prep(config, repo, cmdline, context)
   context.operation = "setowner"
   return repo:run_lace(context)
end

local function builtin_set_owner_run(config, repo, cmdline, env)
   local owner = cmdline[3]
   log.chat("Setting repository owner to", owner)
   ok, msg = repo:set_owner(owner)
   if not ok then
      log.error(msg)
      return "exit", 1
   end
   log.state("Ownership of repository", repo.name, "transferred ok.")
   return "exit", 0
end

assert(register_cmd("set-owner", builtin_set_owner_short, 
		    builtin_set_owner_helptext, builtin_set_owner_validate,
		    builtin_set_owner_prep, builtin_set_owner_run, 
		    true, false))

local builtin_set_head_short = "Set the repo's HEAD symbolic reference"
local builtin_set_head_helptext = [[
usage: set-head <repo> <ref>

Sets the HEAD of the repository to the given ref.  You may need to be
the owner of the repository to use this command.
]]

local function builtin_set_head_validate(conf, repo, cmdline)
   if not repo or repo.is_nascent then
      log.error("Cannot set the HEAD on a nascent repository")
      return false
   end
   if #cmdline ~= 3 then
      log.error("set-head takes a repository and a ref")
      return false
   end
   return true
end

local function builtin_set_head_prep(conf, repo, cmdline, context)
   context.operation = "config_set"
   context.key = "project.head"
   context.value = cmdline[3]
   return repo:run_lace(context)
end

local function builtin_set_head_run(conf, repo, cmdline, env)
   local ok, msg = repo:set_head(cmdline[3])
   if not ok then
      log.fatal(msg)
   end
   return "exit", 0
end

assert(register_cmd("set-head", builtin_set_head_short,
		    builtin_set_head_helptext,
		    builtin_set_head_validate, builtin_set_head_prep,
		    builtin_set_head_run, true, false))

local builtin_set_description_short = "Set the repo's short description"
local builtin_set_description_helptext = [[
usage: set-description <repo> Description text

Sets the short description of the repository to the given text.  This text
may be used in display tools such as gitweb.
]]

local function builtin_set_description_validate(conf, repo, cmdline)
   if not repo or repo.is_nascent then
      log.error("Cannot set the description on a nascent repository")
      return false
   end
   if #cmdline < 3 then
      log.error("set-description takes a repository and a description")
      return false
   end
   if #cmdline > 3 then
      local cc = util.deep_copy(cmdline)
      table.remove(cc,1)
      table.remove(cc,1)
      cmdline[3] = table.concat(cc, " ")
   end
   return true
end

local function builtin_set_description_prep(conf, repo, cmdline, context)
   context.operation = "config_set"
   context.key = "project.description"
   context.value = cmdline[3]
   return repo:run_lace(context)
end

local function builtin_set_description_run(conf, repo, cmdline, env)
   local ok, msg = repo:set_description(cmdline[3])
   if not ok then
      log.fatal(msg)
   end
   return "exit", 0
end

assert(register_cmd("set-description", builtin_set_description_short,
		    builtin_set_description_helptext,
		    builtin_set_description_validate, builtin_set_description_prep,
		    builtin_set_description_run, true, false))

local builtin_config_short = "View and change configuration for a repository"
local builtin_config_helptext = [[
usage: config <reponame> <cmd> [args...]

View and manipulate the configuration of a repository.

* config <reponame> show [<filter>...]
  List all configuration variables in <reponame> which match any of
  the filters provided.  The filters are prefixes which are matched
  against the keys of the configuration variables.

  For example: `config sampler list project` will list all the
  project configuration entries for the sampler.git repository.

  Keys which represent lists are shown as `foo.*`  If you wish to
  show the detailed key, showing the index of the entry in the list
  then you should set the filter exactly to `foo.*` which will cause
  the show command to expand list keys into the form `foo.i_N` where N
  is the index in the list.
* config <reponame> set key value
  Set the given configuration key to the given value.  If the key ends
  in `.*` then the system will add the given value to the end of the
  list represented by the key.  To replace a specific entry, set the
  specific `i_N` entry to the value you want to replace it.
* config <reponame> {del,delete,rm} key
  Removes the given key from the configuration set.  If the key ends in `.*`
  then the system will remove all configuration values below that prefix.
  To remove a specific element of a list, instead, be sure to delete
  the `i_N` entry instead.
]]
local function builtin_config_validate(conf, repo, cmdline)
   if not repo or repo.is_nascent then
      log.error("Cannot access configuration of a nascent repository")
      return false
   end
   if #cmdline < 3 then
      cmdline[3] = "show"
   end
   if cmdline[3] == "show" then
      -- No validation to do yet
   elseif cmdline[3] == "set" then
      if #cmdline < 5 then
	 log.error("config <repo> set: takes a key and a value to set")
	 return false
      end
      if #cmdline > 5 then
	 local cpy = {}
	 for i = #cmdline, 5, -1 do
	    table.insert(cpy, 1, cmdline[i])
	    cmdline[i] = nil
	 end
	 cmdline[5] = table.concat(cpy, " ")
      end
   elseif cmdline[3] == "del" or cmdline[3] == "delete" or
      cmdline[3] == "rm" then
      cmdline[3] = "del"
      if #cmdline ~= 4 then
	 log.error("config <repo> del: takes a key and nothing more")
	 return false
      end
      cmdline.orig_key = cmdline[4]
      if cmdline[4]:match("%.%*$") then
	 -- Doing a wild removal, expand it now
	 local prefix = cmdline[4]:match("^(.+)%.%*$")
	 cmdline[4] = nil
	 for k in repo.project_config:each(prefix) do
	    cmdline[#cmdline+1] = k
	 end
      end
   else
      log.error("Unknown subcommand <" .. cmdline[3] .. "> for config.")
      log.info("Valid subcommands are <show> <set> and <del>")
      return false
   end
   return true
end

local function builtin_config_prep(conf, repo, cmdline, context)
   if cmdline[3] == "show" then
      context.operation = "config_show"
      for i = 4, #cmdline do
	 local cpy = util.deep_copy(context)
	 cpy.key = cmdline[i]
	 local action, reason = repo:run_lace(cpy)
	 if action ~= "allow" then
	    return action, reason
	 end
      end
      return "allow", "Show not denied"
   elseif cmdline[3] == "set" then
      context.operation = "config_set"
      context.key = cmdline[4]
      context.value = cmdline[5]
      return repo:run_lace(context)
   elseif cmdline[3] == "del" then
      context.operation = "config_del"
      for i = 4, #cmdline do
	 local cpy = util.deep_copy(context)
	 cpy.key = cmdline[i]
	 local action, reason = repo:run_lace(cpy)
	 if action ~= "allow" then
	    return action, reason
	 end
      end
      return "allow", "Delete not denied"
   end
   return "deny", "Unknown sub command slipped through"
end

local function builtin_config_run(conf, repo, cmdline, env)
   if cmdline[3] == "show" then
      local all_keys = {}
      if #cmdline == 3 then
	 for k in repo.project_config:each() do
	    all_keys[k] = true
	 end
      else
	 for i = 4, #cmdline do
	    for k in repo.project_config:each(cmdline[i]) do
	       all_keys[k] = true
	    end
	 end
      end
      -- Transform the all_keys set into a sorted list
      local slist = {}
      for k in pairs(all_keys) do
	 slist[#slist+1] = k
      end
      -- TODO: Fix this sort to cope with .i_N keys neatly
      table.sort(slist)
      for i = 1, #slist do
	 local key = slist[i]
	 local value = repo.project_config.settings[key]
	 local prefix = key:match("^(.+)%.i_[0-9]+$")
	 if prefix then
	    local neatkey = prefix .. ".*"
	    for i = 4, #cmdline do
	       if cmdline[i] == neatkey then
		  neatkey = key
		  break
	       end
	    end
	 end
	 log.stdout(key .. ": " .. value)
      end
   elseif cmdline[3] == "set" then
      local key, value = cmdline[4], cmdline[5]
      local vtype, rest = value:match("^([sbi]):(.*)$")
      if vtype then
	 if vtype == "s" then
	    value = rest
	 end
	 if vtype == "i" then
	    value = tonumber(rest)
	 end
	 if vtype == "b" then
	    value = ((rest:lower() == "true") or (rest == "1") or
		     (rest:lower() == "on") or (rest:lower() == "yes"))
	 end
      end
      repo.project_config.settings[key] = value
      local ok, msg = repo:save_admin("Changed project setting: " .. key,
				      env.GITANO_USER)
      if not ok then
	 log.error(msg)
	 return "exit", 2
      end
   elseif cmdline[3] == "del" then
      local key = cmdline.orig_key
      for i = 4, #cmdline do
	 repo.project_config.settings[cmdline[4]] = nil
      end
      local ok, msg = repo:save_admin("Deleted project setting: " .. key,
				      env.GITANO_USER)
      if not ok then
	 log.error(msg)
	 return "exit", 2
      end
   else
      log.error("Unknown sub command slipped through")
      return "exit", 1
   end
   return "exit", 0
end

assert(register_cmd("config", builtin_config_short,
		    builtin_config_helptext,
		    builtin_config_validate, builtin_config_prep,
		    builtin_config_run, true, false))

local builtin_readme_short = "View readme for a repository (if present)"
local builtin_readme_helptext = [[
usage: readme <reponame>

Shows you the readme for the given repository.  You must have read
access to the repository in order to see it.
]]

local function builtin_readme_validate(config, repo, cmdline)
   if #cmdline ~= 2 then
      log.error("Expected repository not provided")
      return false
   end
   if not repo or repo.is_nascent then
      log.error("Repository does not exist")
      return false
   end
   return true
end

local function builtin_readme_prep(config, repo, cmdline, context)
   context.operation = "read"
   return repo:run_lace(context)
end

local function builtin_readme_run(config, repo, cmdline, env)
   local t = repo.readme_mdwn
   if t then
      if not t:match("\n$") then
	 t = t .. "\n"
      end
      for l in t:gmatch("([^\n]*)\n") do
	 log.state(l)
      end
   else
      log.state("# No README.mdwn found")
   end
   return "exit", 0
end

assert(register_cmd("readme", builtin_readme_short,
		    builtin_readme_helptext, builtin_readme_validate,
		    builtin_readme_prep, builtin_readme_run,
		    true, false))

local builtin_destroy_short = "Destroy (delete) a repository"
local builtin_destroy_helptext = [[
usage: destroy <repo> [confirmtoken]

This command destroys a repository.  Run without a confirmation token
it will tell the caller what the confirmation token is for that repository.
The caller will then run the destroy command again with the confirmation
token if they really do wish to destroy the repository.
]]

local function builtin_destroy_validate(config, repo, cmdline)
   if #cmdline < 2 or #cmdline > 3 then
      log.error("Destroy takes a repository and a (optional) confirmation token")
      return false
   end
   if not repo or repo.is_nascent then
      log.error("Cannot destroy a repository which does not exist")
      return false
   end
   return true
end

local function builtin_destroy_prep(config, repo, cmdline, context)
   context.operation = "destroyrepo"
   return repo:run_lace(context)
end

local function builtin_destroy_run(config, repo, cmdline, env)
   if #cmdline == 2 then
      -- Generate the confirmation token
      local token = repo:generate_confirmation()
      log.state("")
      log.state("If you are *certain* you wish to destroy this repository")
      log.state("Then re-run your command with the following confirmation token:")
      log.state("")
      log.state("   ", token)
   else
      local token = repo:generate_confirmation()
      if cmdline[3] ~= token then
	 log.error("Confirmation token does not match, refusing to destroy")
	 return "exit", 1
      end
      -- Tokens match, ask the repo to destroy itself
      local nowstamp = os.date("!%Y-%m-%d.%H:%M:%S.UTC")
      local ok, msg = repo:destroy_self(nowstamp .. "." ..
					(repo.name:gsub("[^A-Za-z0-9_%.%-]",
							"_")) ..
					   "." .. token .. ".destroyed")
      if not ok then
	 log.error(msg)
	 return "exit", 1
      end
      log.state("Should you need to recover the repository you just destroyed")
      log.state("then you will need to speak with an admin as soon as possible")
      log.state("")
      log.state("When you do, be sure to include the current time (" .. 
	       nowstamp .. ").")
      log.state("It may also help if you include your token:")
      log.state("  ", token)
      log.state("")
      log.state("Successfully destroyed", repo.name)
   end
   return "exit", 0
end

assert(register_cmd("destroy", builtin_destroy_short, 
		    builtin_destroy_helptext, builtin_destroy_validate, 
		    builtin_destroy_prep, builtin_destroy_run, 
		    true, false))

local builtin_rename_short = "Rename a repository"
local builtin_rename_helptext = [[
usage: rename <repo> <newname>

Renames a repository to the given new name.  In order to do this, you must
have the ability to create repositories at the new name, the ability to
read the current repository and the ability to rename the current repository.
]]

local function builtin_rename_validate(config, repo, cmdline)
   if #cmdline ~= 3 then
      log.error("Rename takes a repository and a new name for it")
      return false
   end
   if not repo or repo.is_nascent then
      log.error("Cannot rename", repo.name, "as it does not exit")
      return false
   end
   return true
end

local function builtin_rename_prep(config, repo, cmdline, context)
   local ctx, action, reason
   -- Check 0, is the current repo nascent
   if repo.is_nascent then
      return "deny", "Cannot rename a repository which does not exit"
   end
   -- Check 1, read current repo
   ctx = util.deep_copy(context)
   ctx.operation = "read"
   action, reason = repo:run_lace(ctx)
   if action ~= "allow" then
      return action, reason
   end
   -- Check 2, rename current repo
   ctx = util.deep_copy(context)
   ctx.operation = "renamerepo"
   action, reason = repo:run_lace(ctx)
   if action ~= "allow" then
      return action, reason
   end
   -- Check 3, create new repo
   ctx = util.deep_copy(context)
   local newrepo, msg = repository.find(config, cmdline[3])
   if not newrepo then
      return "deny", msg
   end
   if not newrepo.is_nascent then
      return "deny", "Destination location is in use"
   end
   ctx.operation="createrepo"
   action, reason = newrepo:run_lace(ctx)
   if action ~= "allow" then
      return action, reason
   end
   -- Okay, we could create, read, and destroy
   -- thus we can rename
   return "allow", "Passed all checks, can rename"
end

local function builtin_rename_run(config, repo, cmdline, env)
   local ok, msg = repo:rename_to(cmdline[3])
   if not ok then
      log.error(msg)
      return "exit", 1
   end
   log.state("Renamed", cmdline[2], "to", cmdline[3])
   return "exit", 0
end

assert(register_cmd("rename", builtin_rename_short, builtin_rename_helptext,
		    builtin_rename_validate, builtin_rename_prep,
		    builtin_rename_run, true, false))

local builtin_ls_short = "List repositories on the server"
local builtin_ls_helptext = [[
usage: ls [--verbose|-v] [<pattern>...]

List repositories on the server.  If you do not provide a pattern then all
repositories are considered, otherwise only ones which match the given patterns
will be considered.  If you specify --verbose then the head ref name and the
description will be provided in addition to the access rights and repository
name, separated by tabs.

Patterns are a type of extended glob style syntax:

      ? == any one character except /
      * == zero or more characters except /
     ** == zero or more characters including /

Any other characters are "as-is" except \ which escapes the next character.

If your pattern contains no / and no ** then it will be matched against
leafnames of repositories.

Note, this means that if you run `ls foo` then the server is going to look
for repositories called `foo.git` rather than look in side a folder called
`foo/`.  For the latter, do `ls foo/` instead.
]]

local function builtin_ls_validate(config, _, cmdline)
   -- For now, anything will do
   return true
end

local function builtin_ls_prep(config, _, cmdline, ctx)
   -- We cheat and store the context for later because we'll need it
   cmdline._ctx = ctx
   return "allow", "You can always try and get a listing"
end

local builtin_ls_special = {}
do
   local builtin_ls_special_chrs = "*?.[]()+-%^$"
   for c in builtin_ls_special_chrs:gmatch(".") do
      builtin_ls_special[c] = true
   end
end

local function builtin_ls_run(config, _, cmdline, env)
   -- Step one, parse each pattern into a lua pattern
   local pats = {}
   local firstpat, verbose = 2, false
   if cmdline[firstpat] == "--verbose" or cmdline[firstpat] == "-v" then
      firstpat, verbose = 3, true
   end
   for i = firstpat, #cmdline do
      local pat, c, input = "", "", cmdline[i]
      local escaping, star, used_evil = false, false, false
      c, input = input:match("^(.)(.*)$")
      while c and c ~= "" do
	 if escaping then
	    pat = pat .. (builtin_ls_special[c] and "%" or "") .. c
	    if c == "/" then
	       used_evil = true
	    end
	    escaping = false
	 else
	    if c == "*" then
	       if star then
		  -- **
		  pat = pat .. ".*"
		  used_evil = true
		  star = false
	       else
		  star = true
	       end
	    else
	       if star then
		  -- *
		  pat = pat .. "[^/]*"
		  star = false
	       end
	       if c == "?" then
		  pat = pat .. "[^/]"
	       elseif c == "\\" then
		  escaping = true
	       else
		  pat = pat .. (builtin_ls_special[c] and "%" or "") .. c
		  if c == "/" then
		     used_evil = true
		  end
	       end
	    end
	 end
	 c, input = input:match("^(.)(.*)$")
      end
      if star then
	 -- spare star
	 pat = pat .. "[^/]*"
      end
      if cmdline[i]:match("/$") then
         pat = pat .. ".*"
      end
      if used_evil then
	 pat = "^/" .. pat .. "%.git$"
      else
	 pat = "/" .. pat .. "%.git$"
      end
      log.debug("PAT:", pat)
      pats[#pats+1] = pat
   end
   if #pats == 0 then
      pats[1] = "."
   end
   -- Now we iterate all the repositories, listing them if (a) they pass a
   -- pattern and (b) they allow the current user to read.
   local _ctx = cmdline._ctx
   local function filter_callback(name)
      for i = 1, #pats do
	 if ("/" .. name):match(pats[i]) then
	    return true
	 end
      end
   end
   local function callback(reponame, repo, msg)
      if repo then
	 local ctx = util.deep_copy(_ctx)
	 ctx.operation = "read"
	 local action, reason = repo:run_lace(ctx)
	 if action == "allow" then
	    ctx = util.deep_copy(_ctx)
	    ctx.operation = "write"
	    action, reason = repo:run_lace(ctx)
	    local tail = ""
	    if verbose then
	       local desc = repo:conf_get("project.description")
	       desc = desc:gsub("\n.*", "")
	       tail = " " .. repo:conf_get("project.head") .. " " .. desc
	    end
	    log.stdout(action == "allow" and "RW" or "R ", repo.name .. tail)
	 end
      end
   end
   repository.foreach(config, callback, filter_callback)
   return "exit", 0
end

assert(register_cmd("ls", builtin_ls_short, builtin_ls_helptext,
		    builtin_ls_validate, builtin_ls_prep, builtin_ls_run,
		    false, false))


local usercmds = require 'gitano.usercommand'
usercmds.register(register_cmd)
local admincmds = require 'gitano.admincommand'
admincmds.register(register_cmd)
local repocmds = require 'gitano.repocommand'
repocmds.register(register_cmd)

return {
   register = register_cmd,
   get = get_cmd,
}