[ANN] SqueakSource commit emails

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
2 messages Options
Reply | Threaded
Open this post in threaded view
|

[ANN] SqueakSource commit emails

Tapple Gao
I added a mechanism SqueakSource to send email notifications
whenever a new version is uploaded.

http://www.squeaksource.com/ss/SqueakSource-mtf.1025.mcz

Features:
- Configurable recipient and reply-to address, both per project
  and global default
- Can be enabled or disabled per-project, or for global default
- Three types of commit emails, only configurable globally:
  - Summary only: Sends URL and version summary information.
    Requires no server-side mcz decoding, so is instantaneous.
    Importing SqueakSource-mtf.1025.mcz to localhost took 2
    seconds under this setting.
  - Summary + full snapshot: additionally puts a text
    representation of the version in the body of the email. With
    my experiment, this setting took the longest, since a full
    snapshot is pretty big.  Importing SqueakSource-mtf.1025.mcz
    to localhost took 9 seconds under this setting.
  - Summary + patch: finds the latest ancestor in the project and
    puts a patch in the body of the email. A sample patch email is
    forwarded at the end of this email.  I was surprised to find
    this was faster than the above method; it does 2-3 times more
    parsing/processing as above, but writes much less data.
    Importing SqueakSource-mtf.1025.mcz to localhost took 5
    seconds under this setting.
- Two new textual representations for generic Monticello packages:
  MCTextWriter, and MCDiffyTextWriter. They are write-only formats
  intended for human, rather than computer, consumption. Currently
  it is grafted on top of the chunk format writer, but I can do
  better. Next version will feature a much better textual format.
  These two writer classes have no dependencies on SqueakSource,
  and could be added directly to Monticello.

Installation Instructions:

These instructions are for merging these features into an
existing SqueakSource installation. For new installations, see
http://wiki.squeak.org/squeak/5766 (slightly out of date; I will
update it soon)

I tested these instructions on a 3.8.1 image running
SqueakSource-al.1024:
1. Merge in SqueakSource-mtf.1025. It is nearly all new classes,
   and has no core changes, so it probably won't have conflicts.
2. log in as superuser
3. Go to global edit settings
4. Set the 5 options:
   - SMTP Server
   - Generated email:           sender address
   - Default email recipients:  (blank by default)
   - Default Reply-To Address:  defaults to squeak-dev list
   - Show in commit notifications: (choose detail level)
   - Default subscriptions:     (check the checkbox if you want to
     enable commit emails)
 
This sets the defaults for all projects. If any per-project
settings are non-nil, they will ignore the defaults. So, you must
do this right after installation and before any existing project
settings have been changed.

Here is a generated commit email (Summary + Patch) for
SqueakSource-mtf.1025:

----- Forwarded message from no-reply@localhost -----

To: [hidden email]
From: no-reply@localhost
Date: Tue, 21 Aug 2007 16:05:04 -0700 (MST)
Subject: SqueakSource: SqueakSource-mtf.1025.mcz

A new version of SqueakSource was added to project SqueakSource:
http://localhost:8888/ss/SqueakSource-mtf.1025.mcz

==================== Summary ====================

Name: SqueakSource-mtf.1025
Author: mtf
Time: 21 August 2007, 4:03:16 pm
UUID: e7fe67cd-2502-7f4e-81ce-7681f86bf1d8
Ancestors: SqueakSource-al.1024

- add model for sending commit emails to SSProject
- add model for default values of above to SSRepository
- add two classes for writing text representations of generic Monticello packages: MCTextWriter and MCDiffyTextWriter
- add three classes for writing text representations SqueakSource versions and packages: SSBasicTextWriter, SSTextWriter, and SSDiffyTextWriter
- add email sending capabilities via SSEmailSubscription

=============== Diff against SqueakSource-al.1024 ===============

Item was added:
+ ----- Method: SSProject>>subscriptionsDo: (in category 'accessing-subscriptions') -----
+ subscriptionsDo: aBlock
+ ^ self subscriptions do: aBlock!

Item was added:
+ ----- Method: SSSubscription>>versionAdded:to: (in category 'as yet unclassified') -----
+ versionAdded: aVersion to: aProject
+ ^ self!

Item was added:
+ SSBasicTextWriter subclass: #SSTextWriter
+ instanceVariableNames: ''
+ classVariableNames: ''
+ poolDictionaries: ''
+ category: 'SqueakSource-Notification'!
+
+ !SSTextWriter commentStamp: '<historical>' prior: 0!
+ In addition to writing summaries, my instances also dump a full snapshot of the uploaded version.
+
+ For speed notes of this class, see the class comment of SSDiffyTextWriter!

Item was added:
+ ----- Method: MCTextWriter>>writeVersion: (in category 'writing') -----
+ writeVersion: aVersion
+ self writeVersionInfo: aVersion info.
+ self writeSnapshot: aVersion snapshot.
+ !

Item was added:
+ ----- Method: SSEMailSubscription>>writeHeaders (in category 'as yet unclassified') -----
+ writeHeaders
+ sender ifNotEmpty: [ stream nextPutAll: 'From: '; nextPutAll: sender].
+ project emailRecipients ifNotEmpty: [stream cr; nextPutAll: 'To: '].
+ project emailRecipients
+ do: [:email | stream nextPutAll: email address]
+ separatedBy: [stream nextPutAll: ', '].
+ project replyTo ifNotEmpty: [stream
+ cr; nextPutAll: 'Reply-To: '; nextPutAll: project replyTo].
+ stream cr;
+ nextPutAll: 'Subject: ';
+ nextPutAll: project title; nextPutAll: ': ';
+ nextPutAll: version fileName; cr; cr!

Item was added:
+ ----- Method: SSDiffyTextWriter>>writePatchHeader: (in category 'as yet unclassified') -----
+ writePatchHeader: info
+ self textWriter writePatchHeader: info!

Item was added:
+ ----- Method: SSRepository>>defaultEmailRecipients (in category 'accessing-settings') -----
+ defaultEmailRecipients
+ ^self properties
+ at: #defaultEmailRecipients
+ ifAbsent: [OrderedCollection new]!

Item was changed:
  ----- Method: SSProject>>addVersion:author: (in category 'accessing-versions') -----
  addVersion: aString author: aUser
  | array version |
  array := aString asByteArray.
  version := SSVersion
  array: array
  author: aUser.
  SSRepository storage saveMonticello: array of: version to: self.
+ versions at: version fileName put: version.
+ self versionAdded: version.
+ ^ version
+ !
- ^versions at: version fileName put: version.
- !

Item was added:
+ ----- Method: SSRepository>>defaultEmailRecipients: (in category 'accessing-settings') -----
+ defaultEmailRecipients: aCollection
+ ^self properties
+ at: #defaultEmailRecipients
+ put: aCollection!

Item was added:
+ SSModel subclass: #SSEmailAddress
+ instanceVariableNames: 'address'
+ classVariableNames: ''
+ poolDictionaries: ''
+ category: 'SqueakSource-Model'!

Item was added:
+ ----- Method: SSRepository>>commitWriterClass (in category 'accessing-settings') -----
+ commitWriterClass
+ ^self properties
+ at: #commitWriterClass
+ ifAbsent: [SSBasicTextWriter]!

Item was added:
+ ----- Method: SSEMailSubscription>>writerClass (in category 'as yet unclassified') -----
+ writerClass
+ ^ SSRepository current commitWriterClass!

Item was added:
+ ----- Method: SSRepository>>emailSender: (in category 'accessing-settings') -----
+ emailSender: aString
+ ^self properties
+ at: #emailSender
+ put: aString!

Item was added:
+ ----- Method: SSProject>>replyTo (in category 'accessing') -----
+ replyTo
+ "Answer the value of replyTo"
+
+ ^ replyTo ifNil: [SSRepository current defaultReplyTo]!

Item was added:
+ ----- Method: SSTextWriter>>writeSnapshot: (in category 'as yet unclassified') -----
+ writeSnapshot: snapshot
+ self textWriter writeSnapshot: snapshot!

Item was added:
+ ----- Method: SSEmailAddress>>address: (in category 'accessing') -----
+ address: anObject
+ "Set the value of address"
+
+ address _ anObject!

Item was added:
+ ----- Method: SSDiffyTextWriter class>>description (in category 'as yet unclassified') -----
+ description
+ ^ 'Summary + changes'!

Item was added:
+ ----- Method: SSEMailSubscription class>>description (in category 'as yet unclassified') -----
+ description
+ ^ 'Recieve commit notifications by email'!

Item was added:
+ MCStWriter subclass: #MCTextWriter
+ instanceVariableNames: ''
+ classVariableNames: ''
+ poolDictionaries: ''
+ category: 'SqueakSource-Notification'!
+
+ !MCTextWriter commentStamp: '<historical>' prior: 0!
+ A basic writer for SqueakSource versions and projects. Does no decoding of .mcz files!

Item was added:
+ ----- Method: SSTextWriter class>>description (in category 'as yet unclassified') -----
+ description
+ ^ 'Summary + full snapshot'!

Item was changed:
  ----- Method: SSProject>>metaobject (in category 'metamodel') -----
  metaobject
  | metaobject |
  metaobject := MWMetaobject for: self.
  metaobject textAttribute: #id do: [ :attribute |
  attribute
  label: 'Name';
  maxLength: 20;
  addRequiredRule;
  addValidationRule: [ :value |
  value allSatisfy: [ :char | char isLetter or: [ char isDigit ] ] ]
  errorString: 'name should only contain letters and digits';
  addValidationRule: [ :value |
  SSRepository current isUniqueProjectId: value ]
  errorString: 'name is not unique' ].
  metaobject textAttribute: #title do: [ :attribute |
  attribute
  label: 'Title';
  maxLength: 50;
  addRequiredRule ].
  metaobject textAttribute: #description do: [ :attribute |
  attribute
  label: 'Description';
  multiLine: true ].
 
  metaobject multipleRelationshipAttribute: #tags do: [ :attribute |
  attribute
  label: 'Tags';
  relationshipTo: [ self repository tags ]
  formatWith: [ :each | each name ];
  "nilItemString: 'none yet';"
  yourself ].
 
  metaobject singleRelationshipAttribute: #license do: [ :attribute |
  attribute
  label: 'License';
  relationshipTo: self licenses
  formatWith: [ :each | each isNil
  ifTrue: [ 'None' ]
  ifFalse: [ each first ifNil: [ 'None' ] ] ] ].
 
  metaobject booleanAttribute: #canBless do: [:attribute |
  attribute
  label: 'Enable Blessings'.
  ].
 
  metaobject singleRelationshipAttribute: #globalRight do: [ :attribute |
  attribute
  label: 'Global';
  relationshipTo: SSAccessPolicy globalRights
  formatWith: [ :symbol | symbol asCapitalizedPhrase ] ].
  metaobject multipleRelationshipAttribute: #admins do: [ :attribute |
  attribute
  label: 'Administrators';
  addValidationRule: [ :admins |
  | currentUser |
  currentUser := SSSession currentSession user.
  currentUser isSuperUser
  or: [
  admins anySatisfy: [ :each |
  each isGroup
  ifTrue: [ each hasMember: currentUser ]
  ifFalse: [ each = currentUser ] ] ] ]
  errorString: 'You can''t remove yourself from the list of administrators';
  hide ].
  metaobject multipleRelationshipAttribute: #developers do: [ :attribute |
  attribute label: 'Developers'; hide ].
  metaobject multipleRelationshipAttribute: #guests do: [ :attribute |
  attribute label: 'Guests'; hide ].
+
+ metaobject multipleAttribute: #emailRecipients do: [ :attribute |
+ attribute
+ label: 'Send emails to';
+ baseClass: SSEmailAddress].
+
+ metaobject textAttribute: #replyTo do: [ :attribute |
+ attribute
+ label: 'Reply-To Address';
+ maxLength: 50].
+
+ metaobject multipleRelationshipAttribute: #subscriptions do: [ :attribute |
+ attribute
+ label: 'Subscriptions';
+ relationshipTo: [SSSubscription allSubclasses]].
+
  ^metaobject!

Item was added:
+ ----- Method: MCTextWriter>>writeVersionInfo: (in category 'writing') -----
+ writeVersionInfo: aVersionInfo
+ stream
+ nextPutAll: '==================== Summary ===================='; cr; cr;
+ nextPutAll: aVersionInfo summary; cr; cr!

Item was added:
+ ----- Method: SSRepository class>>defaultEmailSender (in category 'private') -----
+ defaultEmailSender
+ ^'no-reply@', self hostName!

Item was changed:
  SSModel subclass: #SSProject
+ instanceVariableNames: 'id title description creator accessPolicy versions dateCreated feeds wiki blessings configs tags cache license emailAddresses replyTo subscriptions emailRecipients'
- instanceVariableNames: 'id title description creator accessPolicy versions dateCreated feeds wiki blessings configs tags cache license'
  classVariableNames: ''
  poolDictionaries: ''
  category: 'SqueakSource-Model'!

Item was added:
+ ----- Method: SSEMailSubscription>>versionAdded:to: (in category 'as yet unclassified') -----
+ versionAdded: aVersion to: aProject
+ sender := SSRepository current emailSender.
+ stream := String new writeStream.
+ project := aProject.
+ version := aVersion.
+ self writeHeaders.
+ (self writerClass on: stream) writeVersion: aVersion for: aProject.
+ self sendMail.!

Item was added:
+ ----- Method: SSEmailAddress>>metaobject (in category 'accessing') -----
+ metaobject
+ | metaobject |
+ metaobject _ MWMetaobject for: self.
+
+ metaobject textAttribute: #address do: [ :attribute |
+ attribute
+ maxLength: 50;
+ addRequiredRule].
+
+ ^metaobject!

Item was added:
+ ----- Method: MCTextWriter>>writeMethodPreamble: (in category 'writing') -----
+ writeMethodPreamble: definition
+ stream
+ nextPutAll: '----- Method: ';
+ nextPutAll: definition fullClassName;
+ nextPutAll: '>>';
+ nextPutAll: definition selector;
+ nextPutAll: ' (in category ';
+ nextPutAll: definition category asString printString;
+ nextPutAll: ') -----'; cr!

Item was added:
+ ----- Method: SSEmailAddress>>address (in category 'accessing') -----
+ address
+ "Answer the value of address"
+
+ ^ address!

Item was added:
+ SSTextWriter subclass: #SSDiffyTextWriter
+ instanceVariableNames: ''
+ classVariableNames: ''
+ poolDictionaries: ''
+ category: 'SqueakSource-Notification'!
+
+ !SSDiffyTextWriter commentStamp: '<historical>' prior: 0!
+ Rather than a full snapshot dump, my instances write a patch from the latest ancestor stored in the project. If no ancestors are in the project, they fall back onto doing a full snapshot.
+
+ Even though this class does much more decoding of .mcz files than SSTextWriter, it is usually a lot faster because a diff is much less information to write than a full snapshot. As a rough metric, a diff of SqueakSource-mtf.1025 (the package where this class was introduced) took 7 seconds to write (on my computer), while a full snapshot took 33 seconds to write. On the other hand, a version summary was instantaneous, as it requires no decoding of .mcz files!

Item was added:
+ ----- Method: SSRepository>>defaultSubscriptions: (in category 'accessing-settings') -----
+ defaultSubscriptions: aCollection
+ ^self properties
+ at: #defaultSubscriptions
+ put: aCollection!

Item was added:
+ ----- Method: MCDiffyTextWriter>>writePatch: (in category 'writing') -----
+ writePatch: aPatch
+ aPatch operations do:
+ [:ea |
+ ea isRemoval ifTrue: [self writeRemoval: ea].
+ ea isAddition ifTrue: [self writeAddition: ea].
+ ea isModification ifTrue: [self writeModification: ea].
+ stream cr.].!

Item was added:
+ MCWriter subclass: #SSBasicTextWriter
+ instanceVariableNames: 'textWriter'
+ classVariableNames: ''
+ poolDictionaries: ''
+ category: 'SqueakSource-Notification'!
+
+ !SSBasicTextWriter commentStamp: '<historical>' prior: 0!
+ My instances write version summaries for SqueakSource versions and projects. Does no decoding of .mcz files. Subclasses extend the functionality.
+
+ For speed notes about this class, see the class comment of SSDiffyTextWriter!

Item was added:
+ ----- Method: SSDiffyTextWriter>>textWriterClass (in category 'as yet unclassified') -----
+ textWriterClass
+ ^ MCDiffyTextWriter!

Item was added:
+ ----- Method: SSEMailSubscription>>writeSummary (in category 'as yet unclassified') -----
+ writeSummary
+ | author |
+ author := version authorString ifEmpty: [version guessedAuthor].
+ author
+ ifNil: [ stream
+ nextPutAll: 'A new version of ';
+ nextPutAll: version package;
+ nextPutAll: ' was added to project ';
+ nextPutAll: project title; nextPut: $:; cr]
+ ifNotNil: [ stream
+ nextPutAll: author;
+ nextPutAll: ' uploaded a new version of ';
+ nextPutAll: version package;
+ nextPutAll: ' to project ';
+ nextPutAll: project title; nextPut: $:; cr].
+ stream nextPutAll: (version url: project); cr; cr.!

Item was added:
+ ----- Method: MCTextWriter>>chunkContents: (in category 'writing') -----
+ chunkContents: aBlock
+ stream nextChunkPut: (String streamContents: aBlock); cr!

Item was added:
+ ----- Method: SSDiffyTextWriter>>writePatch: (in category 'as yet unclassified') -----
+ writePatch: patch
+ self textWriter writePatch: patch!

Item was added:
+ ----- Method: TextDiffBuilder>>stringForAttributes: (in category '*SqueakSource-Notifications') -----
+ stringForAttributes: type
+ "Private.
+ Answer the String that prefixes text of the given type."
+
+ ^type caseOf: {
+ [#insert] -> [ '+ ' ].
+ [#remove] -> [ '- '].
+ } otherwise: [ '  ' ].
+ !

Item was added:
+ ----- Method: SSDiffyTextWriter>>writeVersion:for: (in category 'as yet unclassified') -----
+ writeVersion: aSSVersion for: aProject
+ | reader ancestor patch |
+ reader := aSSVersion reader: aProject.
+ ancestor := (aSSVersion versionInfo latestAncestorIn: aProject)
+ ifNil: [reader info latestAncestorIn: aProject].
+ self writeSummary: aSSVersion for: aProject.
+ self writeVersionInfo: aSSVersion versionInfo.
+ ancestor
+ ifNil: [self writeSnapshot: reader snapshot.]
+ ifNotNil: [
+ patch := reader snapshot patchRelativeToBase:
+ (ancestor reader: aProject) snapshot.
+ self writePatchHeader: ancestor versionInfo.
+ self writePatch: patch.]!

Item was added:
+ ----- Method: SSEMailSubscription>>sendMail (in category 'as yet unclassified') -----
+ sendMail
+ project emailRecipients ifEmpty: [^ self].
+ SeasidePlatformSupport
+ deliverMailFrom: sender
+ to: (project emailRecipients collect: [:email | email address])
+ text: stream contents.
+ "Workspace new contents: stream contents; openLabel: sender."!

Item was added:
+ ----- Method: SSTextWriter>>writeVersion:for: (in category 'as yet unclassified') -----
+ writeVersion: aSSVersion for: aProject
+ | reader |
+ reader := aSSVersion reader: aProject.
+ self writeSummary: aSSVersion for: aProject.
+ self writeVersionInfo: aSSVersion versionInfo.
+ self writeSnapshot: reader snapshot.!

Item was added:
+ ----- Method: MCAncestry>>latestAncestorIn: (in category '*SqueakSource') -----
+ latestAncestorIn: aProject
+ | ancestor |
+ self ancestors ifEmpty: [^ nil].
+ self allAncestors do: [:anAncestry |
+ ancestor _ aProject versionAt: anAncestry name,'.mcz'.
+ ancestor ifNotNil: [^ ancestor]].
+ ^ nil!

Item was added:
+ ----- Method: SSEMailSubscription>>writeDiffAgainst: (in category 'as yet unclassified') -----
+ writeDiffAgainst: ancestor
+ stream nextPutAll: 'Diff against ';
+ nextPutAll: ancestor fileName; cr.
+ self writeVersion
+ !

Item was added:
+ MCTestCase subclass: #MCTextWriterTest
+ instanceVariableNames: 'version'
+ classVariableNames: ''
+ poolDictionaries: ''
+ category: 'SqueakSource-Notification'!

Item was added:
+ ----- Method: SSProject>>subscriptions: (in category 'accessing-subscriptions') -----
+ subscriptions: aCollection
+ "Private. Set the subscriptions"
+ subscriptions := aCollection!

Item was added:
+ ----- Method: SSProject>>emailRecipients (in category 'accessing') -----
+ emailRecipients
+ "Answer the value of emailAddresses"
+
+ ^ emailRecipients ifNil: [SSRepository current defaultEmailRecipients]!

Item was added:
+ ----- Method: SSRepository>>defaultSubscriptions (in category 'accessing-settings') -----
+ defaultSubscriptions
+ ^self properties
+ at: #defaultSubscriptions
+ ifAbsent: [OrderedCollection new]!

Item was added:
+ ----- Method: SSProject>>replyTo: (in category 'accessing') -----
+ replyTo: anObject
+ "Set the value of replyTo"
+
+ replyTo _ anObject!

Item was added:
+ ----- Method: SSBasicTextWriter>>textWriter (in category 'as yet unclassified') -----
+ textWriter
+ ^ textWriter ifNil: [textWriter := self textWriterClass on: stream]!

Item was added:
+ ----- Method: MCDiffyTextWriter>>writeModification: (in category 'writing') -----
+ writeModification: aModification
+ stream nextPutAll: 'Item was changed:'; cr.
+ self writePatchFrom: aModification obsoletion to: aModification definition!

Item was added:
+ SSModel subclass: #SSSubscription
+ instanceVariableNames: 'stream project version'
+ classVariableNames: ''
+ poolDictionaries: ''
+ category: 'SqueakSource-Notification'!
+
+ !SSSubscription commentStamp: '<historical>' prior: 0!
+ My insances represent a notification that is sent externally (an email message, for instance) in response to an event (like a commit)!

Item was changed:
  ----- Method: SSRepository class>>defaultRootUrl (in category 'private') -----
  defaultRootUrl
+ ^'http://', self hostName, ':', self defaultPort asString, '/'!
- | hostName |
- hostName := NetNameResolver nameForAddress: (NetNameResolver localHostAddress) timeout: 5.
- hostName ifNil: [hostName := NetNameResolver localAddressString].
- ^'http://', hostName, ':', self defaultPort asString, '/'!

Item was added:
+ ----- Method: SSRepository>>commitWriterClass: (in category 'accessing-settings') -----
+ commitWriterClass: aClass
+ ^self properties
+ at: #commitWriterClass
+ put: aClass!

Item was added:
+ ----- Method: MCDiffyTextWriter>>writePatchFrom:to: (in category 'writing') -----
+ writePatchFrom: src to: dst
+ "src and dst are allowed to bi nil to represent a non-existent source or destination state"
+ stream nextPutAll: (TextDiffBuilder
+ from: (src ifNotNil: [self visitInFork: src] ifNil: [''])
+ to: (dst ifNotNil: [self visitInFork: dst] ifNil: [''])
+ ) buildTextPatch!

Item was changed:
  SystemOrganization addCategory: #'SqueakSource-Model'!
  SystemOrganization addCategory: #'SqueakSource-View'!
  SystemOrganization addCategory: #'SqueakSource-Server'!
  SystemOrganization addCategory: #'SqueakSource-Tests'!
+ SystemOrganization addCategory: #'SqueakSource-Notification'!

Item was added:
+ ----- Method: SSBasicTextWriter>>writeVersionInfo: (in category 'as yet unclassified') -----
+ writeVersionInfo: info
+ self textWriter writeVersionInfo: info!

Item was added:
+ ----- Method: MCDiffyTextWriter>>writeRemoval: (in category 'writing') -----
+ writeRemoval: aRemoval
+ stream nextPutAll: 'Item was removed:'; cr.
+ self writePatchFrom: aRemoval definition to: nil!

Item was added:
+ ----- Method: MCTextWriter>>visitMethodDefinition: (in category 'visiting') -----
+ visitMethodDefinition: definition
+ self writeMethodPreamble: definition.
+ self writeMethodSource: definition.
+ stream cr!

Item was changed:
  ----- Method: SSProject>>initialize (in category 'initialization') -----
  initialize
  super initialize.
  id := title := description := String new.
  accessPolicy := SSAccessPolicy new.
  versions := Dictionary new.
  dateCreated := Date today.
  tags := SortedCollection new.
+ cache := IdentityDictionary new.!
- cache := IdentityDictionary new!

Item was added:
+ ----- Method: SSRepository class>>hostName (in category 'private') -----
+ hostName
+ ^ (NetNameResolver nameForAddress: (NetNameResolver localHostAddress) timeout: 5)
+ ifNil: [NetNameResolver localAddressString]!

Item was added:
+ MCTextWriter subclass: #MCDiffyTextWriter
+ instanceVariableNames: ''
+ classVariableNames: ''
+ poolDictionaries: ''
+ category: 'SqueakSource-Notification'!

Item was added:
+ ----- Method: SSBasicTextWriter class>>description (in category 'as yet unclassified') -----
+ description
+ ^ 'Summary only'!

Item was added:
+ ----- Method: SSProject>>versionAdded: (in category 'accessing-subscriptions') -----
+ versionAdded: aVersion
+ ^ self subscriptionsDo: [:each | each new versionAdded: aVersion to: self]!

Item was added:
+ ----- Method: MCDiffyTextWriter>>writePatchHeader: (in category 'writing') -----
+ writePatchHeader: info
+ stream
+ nextPutAll: '=============== Diff against ';
+ nextPutAll: info name;
+ nextPutAll: ' ==============='; cr; cr!

Item was added:
+ ----- Method: SSEMailSubscription>>writeVersion (in category 'as yet unclassified') -----
+ writeVersion
+ | ancestor aVersion|
+ ancestor := version lastAncestorIn: project.
+ aVersion := (version reader: project) version.
+ ancestor
+ ifNotNil: [(MCDiffyTextWriter on: stream) writeVersion:
+ (aVersion asDiffAgainst: (ancestor reader: project) version)]
+ ifNil: [(MCTextWriter on: stream) writeVersion: aVersion]
+ !

Item was added:
+ ----- Method: SSSubscription class>>renderOn: (in category 'as yet unclassified') -----
+ renderOn: html
+ html text: self description!

Item was added:
+ SSSubscription subclass: #SSEMailSubscription
+ instanceVariableNames: 'sender'
+ classVariableNames: ''
+ poolDictionaries: ''
+ category: 'SqueakSource-Notification'!

Item was added:
+ ----- Method: TextDiffBuilder>>printTextPatchSequence:on: (in category '*SqueakSource-Notifications') -----
+ printTextPatchSequence: seq on: aStream
+ seq do: [:assoc | aStream
+ nextPutAll: (self stringForAttributes: assoc key);
+ nextPutAll: assoc value; cr]!

Item was changed:
  ----- Method: SSRepository>>metaobject (in category 'accessing') -----
  metaobject
  | metaobject |
  metaobject _ MWMetaobject for: self.
 
  metaobject textAttribute: #rootUrl do: [ :attribute |
  attribute label: 'Root URL';
  addRequiredRule].
  metaobject integerAttribute: #port do: [ :attribute |
  attribute label: 'Port';
  addValidationRule: [ :value | value notNil ]
  errorString: 'invalid number';
  addValidationRule: [ :value | value between: 1 and: 65535 ]
  errorString: 'invalid port';
  addRequiredRule].
  metaobject textAttribute: #smtpServer do: [ :attribute |
  attribute label: 'SMTP server';
  addRequiredRule].
  metaobject textAttribute: #superUserEmail do: [ :attribute |
  attribute label: 'Admin email';
  addRequiredRule].
+ metaobject textAttribute: #emailSender do: [ :attribute |
+ attribute label: 'Generated email';
+ addRequiredRule].
+ metaobject multipleAttribute: #defaultEmailRecipients do: [ :attribute |
+ attribute label: 'Default email recipients'; baseClass: SSEmailAddress].
+ metaobject textAttribute: #defaultReplyTo do: [ :attribute |
+ attribute label: 'Default Reply-To Address'; maxLength: 50].
+ metaobject singleRelationshipAttribute: #commitWriterClass do: [ :attribute |
+ attribute label: 'Show in commit notifications';
+ relationshipTo: [SSBasicTextWriter withAllSubclasses];
+ formatWith: [ :class | class description]].
+ metaobject multipleRelationshipAttribute: #defaultSubscriptions do: [ :attribute |
+ attribute label: 'Default subscriptions'; relationshipTo: [SSSubscription allSubclasses]].
  metaobject booleanAttribute: #allowRegisterProject do: [ :attribute |
  attribute label: 'Everyone can register projects'].
  metaobject booleanAttribute: #allProjectsVisible do: [ :attribute |
  attribute label: 'Everyone can see all projects' ].
  metaobject booleanAttribute: #allowCreateTag do: [ :attribute |
  attribute label: 'Everyone can create tags'].
  metaobject integerAttribute: #batchSize do: [ :attribute |
  attribute label: 'Batch size for table reports';
  addValidationRule: [ :value | value notNil ]
  errorString: 'invalid number';
  addValidationRule: [ :value | value > 1 ]
  errorString: 'too small';
  addRequiredRule].
  metaobject colorAttribute: #styleColor do: [ :attribute |
  attribute label: 'Style color';
  addValidationRule: [ :value | value ~= Color white ]
  errorString: 'invalid color'].
  metaobject textAttribute: #introText do: [ :attribute |
  attribute label: 'Override home text';
  multiLine: true].
  metaobject textAttribute: #timezone do: [ :attribute |
  attribute label: 'Timezone';
  addRequiredRule ].
  metaobject textAttribute: #googleAnalyticsAccount do: [ :attribute |
  attribute label: 'Google Analytics Account' ].
  ^metaobject!

Item was added:
+ ----- Method: SSBasicTextWriter>>textWriterClass (in category 'as yet unclassified') -----
+ textWriterClass
+ ^ MCTextWriter!

Item was changed:
  ----- Method: SSProjectEditor>>renderMainOn: (in category 'rendering') -----
  renderMainOn: html
+ self isSubForm ifTrue: [^ super renderMainOn: html].
  self renderIntroductionTextOn: html.
  html layoutTable: [
  self renderValidationErrorsOn: html.
  self renderFieldsOn: html.
  self renderMemberListForAttribute: (self metaobject attributeOf: #admins) on: html.
  self renderMemberListForAttribute: (self metaobject attributeOf: #developers) on: html.
  self renderMemberListForAttribute: (self metaobject attributeOf: #guests) on: html.
  html tableRowWith: [ html space ] span: 2.
  html tableRowWith: [ self renderButtonsOn: html ] span: 2 ]!

Item was added:
+ ----- Method: SSRepository>>defaultReplyTo: (in category 'accessing-settings') -----
+ defaultReplyTo: aString
+ ^self properties
+ at: #defaultReplyTo
+ put: aString!

Item was added:
+ ----- Method: SSBasicTextWriter>>writeVersion:for: (in category 'as yet unclassified') -----
+ writeVersion: aSSVersion for: aProject
+ self writeSummary: aSSVersion for: aProject.
+ self writeVersionInfo: aSSVersion versionInfo.!

Item was added:
+ ----- Method: MCTextWriterTest>>testWriteSnapshot (in category 'as yet unclassified') -----
+ testWriteSnapshot
+ Workspace new contents: (String streamContents: [:aStream | (MCTextWriter on: aStream) writeSnapshot: self mockSnapshot]); openLabel: 'pizza'.
+ !

Item was added:
+ ----- Method: SSRepository>>emailSender (in category 'accessing-settings') -----
+ emailSender
+ ^self properties
+ at: #emailSender
+ ifAbsent: [self class defaultEmailSender]!

Item was added:
+ ----- Method: MCTextWriter>>visitInFork: (in category 'visiting') -----
+ visitInFork: aDefinition
+ ^ String streamContents: [ :forkedStream |
+ aDefinition accept: (self class on: forkedStream)]!

Item was added:
+ ----- Method: MCTextWriter>>writeSnapshot: (in category 'writing') -----
+ writeSnapshot: aSnapshot
+ stream nextPutAll: '==================== Snapshot ===================='; cr; cr.
+ super writeSnapshot: aSnapshot
+ !

Item was added:
+ ----- Method: SSBasicTextWriter>>writeSummary:for: (in category 'as yet unclassified') -----
+ writeSummary: aSSVersion for: aProject
+ | author |
+ author := aSSVersion authorString ifEmpty: [aSSVersion guessedAuthor].
+ author
+ ifNil: [ stream
+ nextPutAll: 'A new version of ';
+ nextPutAll: aSSVersion package;
+ nextPutAll: ' was added to project ';
+ nextPutAll: aProject title; nextPut: $:; cr]
+ ifNotNil: [ stream
+ nextPutAll: author;
+ nextPutAll: ' uploaded a new version of ';
+ nextPutAll: aSSVersion package;
+ nextPutAll: ' to project ';
+ nextPutAll: aProject title; nextPut: $:; cr].
+ stream nextPutAll: (aSSVersion url: aProject); cr; cr.!

Item was added:
+ ----- Method: SSRepository>>defaultReplyTo (in category 'accessing-settings') -----
+ defaultReplyTo
+ ^self properties
+ at: #defaultReplyTo
+ ifAbsent: ['[hidden email]']!

Item was added:
+ ----- Method: MCDiffyTextWriter>>writeVersion: (in category 'writing') -----
+ writeVersion: aVersion
+ self writeVersionInfo: aVersion info.
+ self writePatchHeader: aVersion baseInfo.
+ self writePatch: aVersion patch.!

Item was added:
+ ----- Method: TextDiffBuilder>>buildTextPatch (in category '*SqueakSource-Notifications') -----
+ buildTextPatch
+ ^String streamContents:[:stream|
+ self printTextPatchSequence: self buildPatchSequence on: stream.
+ ]!

Item was added:
+ ----- Method: SSProject>>emailRecipients: (in category 'accessing') -----
+ emailRecipients: anObject
+ "Set the value of emailAddresses"
+
+ emailRecipients _ anObject!

Item was added:
+ ----- Method: MCDiffyTextWriter>>writeAddition: (in category 'writing') -----
+ writeAddition: anAddition
+ stream nextPutAll: 'Item was added:'; cr.
+ self writePatchFrom: nil to: anAddition definition!

Item was added:
+ ----- Method: SSProject>>subscriptions (in category 'accessing-subscriptions') -----
+ subscriptions
+ "Answers the instances of SSSubscription who are interested in changes to this project"
+ ^ subscriptions ifNil: [self repository defaultSubscriptions]!

Item was added:
+ ----- Method: MCTextWriter>>writeDefinitions: (in category 'writing') -----
+ writeDefinitions: aCollection
+ (MCDependencySorter sortItems: aCollection)
+ do: [:ea | ea accept: self. stream cr]
+ displayingProgress: 'Writing definitions...'.!


----- End forwarded message -----

--
Matthew Fulmer -- http://mtfulmer.wordpress.com/
Help improve Squeak Documentation: http://wiki.squeak.org/squeak/808

Reply | Threaded
Open this post in threaded view
|

Re: [ANN] SqueakSource commit emails

stephane ducasse
this is cool.
Stef

On 22 août 07, at 01:34, Matthew Fulmer wrote:

> I added a mechanism SqueakSource to send email notifications
> whenever a new version is uploaded.
>
> http://www.squeaksource.com/ss/SqueakSource-mtf.1025.mcz
>
> Features:
> - Configurable recipient and reply-to address, both per project
>   and global default
> - Can be enabled or disabled per-project, or for global default
> - Three types of commit emails, only configurable globally:
>   - Summary only: Sends URL and version summary information.
>     Requires no server-side mcz decoding, so is instantaneous.
>     Importing SqueakSource-mtf.1025.mcz to localhost took 2
>     seconds under this setting.
>   - Summary + full snapshot: additionally puts a text
>     representation of the version in the body of the email. With
>     my experiment, this setting took the longest, since a full
>     snapshot is pretty big.  Importing SqueakSource-mtf.1025.mcz
>     to localhost took 9 seconds under this setting.
>   - Summary + patch: finds the latest ancestor in the project and
>     puts a patch in the body of the email. A sample patch email is
>     forwarded at the end of this email.  I was surprised to find
>     this was faster than the above method; it does 2-3 times more
>     parsing/processing as above, but writes much less data.
>     Importing SqueakSource-mtf.1025.mcz to localhost took 5
>     seconds under this setting.
> - Two new textual representations for generic Monticello packages:
>   MCTextWriter, and MCDiffyTextWriter. They are write-only formats
>   intended for human, rather than computer, consumption. Currently
>   it is grafted on top of the chunk format writer, but I can do
>   better. Next version will feature a much better textual format.
>   These two writer classes have no dependencies on SqueakSource,
>   and could be added directly to Monticello.
>
> Installation Instructions:
>
> These instructions are for merging these features into an
> existing SqueakSource installation. For new installations, see
> http://wiki.squeak.org/squeak/5766 (slightly out of date; I will
> update it soon)
>
> I tested these instructions on a 3.8.1 image running
> SqueakSource-al.1024:
> 1. Merge in SqueakSource-mtf.1025. It is nearly all new classes,
>    and has no core changes, so it probably won't have conflicts.
> 2. log in as superuser
> 3. Go to global edit settings
> 4. Set the 5 options:
>    - SMTP Server
>    - Generated email:           sender address
>    - Default email recipients:  (blank by default)
>    - Default Reply-To Address:  defaults to squeak-dev list
>    - Show in commit notifications: (choose detail level)
>    - Default subscriptions:     (check the checkbox if you want to
>      enable commit emails)
>
> This sets the defaults for all projects. If any per-project
> settings are non-nil, they will ignore the defaults. So, you must
> do this right after installation and before any existing project
> settings have been changed.
>
> Here is a generated commit email (Summary + Patch) for
> SqueakSource-mtf.1025:
>
> ----- Forwarded message from no-reply@localhost -----
>
> To: [hidden email]
> From: no-reply@localhost
> Date: Tue, 21 Aug 2007 16:05:04 -0700 (MST)
> Subject: SqueakSource: SqueakSource-mtf.1025.mcz
>
> A new version of SqueakSource was added to project SqueakSource:
> http://localhost:8888/ss/SqueakSource-mtf.1025.mcz
>
> ==================== Summary ====================
>
> Name: SqueakSource-mtf.1025
> Author: mtf
> Time: 21 August 2007, 4:03:16 pm
> UUID: e7fe67cd-2502-7f4e-81ce-7681f86bf1d8
> Ancestors: SqueakSource-al.1024
>
> - add model for sending commit emails to SSProject
> - add model for default values of above to SSRepository
> - add two classes for writing text representations of generic  
> Monticello packages: MCTextWriter and MCDiffyTextWriter
> - add three classes for writing text representations SqueakSource  
> versions and packages: SSBasicTextWriter, SSTextWriter, and  
> SSDiffyTextWriter
> - add email sending capabilities via SSEmailSubscription
>
> =============== Diff against SqueakSource-al.1024 ===============
>
> Item was added:
> + ----- Method: SSProject>>subscriptionsDo: (in category 'accessing-
> subscriptions') -----
> + subscriptionsDo: aBlock
> + ^ self subscriptions do: aBlock!
>
> Item was added:
> + ----- Method: SSSubscription>>versionAdded:to: (in category 'as  
> yet unclassified') -----
> + versionAdded: aVersion to: aProject
> + ^ self!
>
> Item was added:
> + SSBasicTextWriter subclass: #SSTextWriter
> + instanceVariableNames: ''
> + classVariableNames: ''
> + poolDictionaries: ''
> + category: 'SqueakSource-Notification'!
> +
> + !SSTextWriter commentStamp: '<historical>' prior: 0!
> + In addition to writing summaries, my instances also dump a full  
> snapshot of the uploaded version.
> +
> + For speed notes of this class, see the class comment of  
> SSDiffyTextWriter!
>
> Item was added:
> + ----- Method: MCTextWriter>>writeVersion: (in category 'writing')  
> -----
> + writeVersion: aVersion
> + self writeVersionInfo: aVersion info.
> + self writeSnapshot: aVersion snapshot.
> + !
>
> Item was added:
> + ----- Method: SSEMailSubscription>>writeHeaders (in category 'as  
> yet unclassified') -----
> + writeHeaders
> + sender ifNotEmpty: [ stream nextPutAll: 'From: '; nextPutAll:  
> sender].
> + project emailRecipients ifNotEmpty: [stream cr; nextPutAll: 'To:  
> '].
> + project emailRecipients
> + do: [:email | stream nextPutAll: email address]
> + separatedBy: [stream nextPutAll: ', '].
> + project replyTo ifNotEmpty: [stream
> + cr; nextPutAll: 'Reply-To: '; nextPutAll: project replyTo].
> + stream cr;
> + nextPutAll: 'Subject: ';
> + nextPutAll: project title; nextPutAll: ': ';
> + nextPutAll: version fileName; cr; cr!
>
> Item was added:
> + ----- Method: SSDiffyTextWriter>>writePatchHeader: (in category  
> 'as yet unclassified') -----
> + writePatchHeader: info
> + self textWriter writePatchHeader: info!
>
> Item was added:
> + ----- Method: SSRepository>>defaultEmailRecipients (in category  
> 'accessing-settings') -----
> + defaultEmailRecipients
> + ^self properties
> + at: #defaultEmailRecipients
> + ifAbsent: [OrderedCollection new]!
>
> Item was changed:
>   ----- Method: SSProject>>addVersion:author: (in category  
> 'accessing-versions') -----
>   addVersion: aString author: aUser
>   | array version |
>   array := aString asByteArray.
>   version := SSVersion
>   array: array
>   author: aUser.
>   SSRepository storage saveMonticello: array of: version to: self.
> + versions at: version fileName put: version.
> + self versionAdded: version.
> + ^ version
> + !
> - ^versions at: version fileName put: version.
> - !
>
> Item was added:
> + ----- Method: SSRepository>>defaultEmailRecipients: (in category  
> 'accessing-settings') -----
> + defaultEmailRecipients: aCollection
> + ^self properties
> + at: #defaultEmailRecipients
> + put: aCollection!
>
> Item was added:
> + SSModel subclass: #SSEmailAddress
> + instanceVariableNames: 'address'
> + classVariableNames: ''
> + poolDictionaries: ''
> + category: 'SqueakSource-Model'!
>
> Item was added:
> + ----- Method: SSRepository>>commitWriterClass (in category  
> 'accessing-settings') -----
> + commitWriterClass
> + ^self properties
> + at: #commitWriterClass
> + ifAbsent: [SSBasicTextWriter]!
>
> Item was added:
> + ----- Method: SSEMailSubscription>>writerClass (in category 'as  
> yet unclassified') -----
> + writerClass
> + ^ SSRepository current commitWriterClass!
>
> Item was added:
> + ----- Method: SSRepository>>emailSender: (in category 'accessing-
> settings') -----
> + emailSender: aString
> + ^self properties
> + at: #emailSender
> + put: aString!
>
> Item was added:
> + ----- Method: SSProject>>replyTo (in category 'accessing') -----
> + replyTo
> + "Answer the value of replyTo"
> +
> + ^ replyTo ifNil: [SSRepository current defaultReplyTo]!
>
> Item was added:
> + ----- Method: SSTextWriter>>writeSnapshot: (in category 'as yet  
> unclassified') -----
> + writeSnapshot: snapshot
> + self textWriter writeSnapshot: snapshot!
>
> Item was added:
> + ----- Method: SSEmailAddress>>address: (in category 'accessing')  
> -----
> + address: anObject
> + "Set the value of address"
> +
> + address _ anObject!
>
> Item was added:
> + ----- Method: SSDiffyTextWriter class>>description (in category  
> 'as yet unclassified') -----
> + description
> + ^ 'Summary + changes'!
>
> Item was added:
> + ----- Method: SSEMailSubscription class>>description (in category  
> 'as yet unclassified') -----
> + description
> + ^ 'Recieve commit notifications by email'!
>
> Item was added:
> + MCStWriter subclass: #MCTextWriter
> + instanceVariableNames: ''
> + classVariableNames: ''
> + poolDictionaries: ''
> + category: 'SqueakSource-Notification'!
> +
> + !MCTextWriter commentStamp: '<historical>' prior: 0!
> + A basic writer for SqueakSource versions and projects. Does no  
> decoding of .mcz files!
>
> Item was added:
> + ----- Method: SSTextWriter class>>description (in category 'as  
> yet unclassified') -----
> + description
> + ^ 'Summary + full snapshot'!
>
> Item was changed:
>   ----- Method: SSProject>>metaobject (in category 'metamodel') -----
>   metaobject
>   | metaobject |
>   metaobject := MWMetaobject for: self.
>   metaobject textAttribute: #id do: [ :attribute |
>   attribute
>   label: 'Name';
>   maxLength: 20;
>   addRequiredRule;
>   addValidationRule: [ :value |
>   value allSatisfy: [ :char | char isLetter or: [ char  
> isDigit ] ] ]
>   errorString: 'name should only contain letters and digits';
>   addValidationRule: [ :value |
>   SSRepository current isUniqueProjectId: value ]
>   errorString: 'name is not unique' ].
>   metaobject textAttribute: #title do: [ :attribute |
>   attribute
>   label: 'Title';
>   maxLength: 50;
>   addRequiredRule ].
>   metaobject textAttribute: #description do: [ :attribute |
>   attribute
>   label: 'Description';
>   multiLine: true ].
>  
>   metaobject multipleRelationshipAttribute: #tags do: [ :attribute |
>   attribute
>   label: 'Tags';
>   relationshipTo: [ self repository tags ]
>   formatWith: [ :each | each name ];
>   "nilItemString: 'none yet';"
>   yourself ].
>  
>   metaobject singleRelationshipAttribute: #license do: [ :attribute |
>   attribute
>   label: 'License';
>   relationshipTo: self licenses
>   formatWith: [ :each | each isNil
>   ifTrue: [ 'None' ]
>   ifFalse: [ each first ifNil: [ 'None' ] ] ] ].
>  
>   metaobject booleanAttribute: #canBless do: [:attribute |
>   attribute
>   label: 'Enable Blessings'.
>   ].
>
>   metaobject singleRelationshipAttribute: #globalRight do:  
> [ :attribute |
>   attribute
>   label: 'Global';
>   relationshipTo: SSAccessPolicy globalRights
>   formatWith: [ :symbol | symbol asCapitalizedPhrase ] ].
>   metaobject multipleRelationshipAttribute: #admins do:  
> [ :attribute |
>   attribute
>   label: 'Administrators';
>   addValidationRule: [ :admins |
>   | currentUser |
>   currentUser := SSSession currentSession user.
>   currentUser isSuperUser
>   or: [
>   admins anySatisfy: [ :each |
>   each isGroup
>   ifTrue: [ each hasMember: currentUser ]
>   ifFalse: [ each = currentUser ] ] ] ]
>   errorString: 'You can''t remove yourself from the list of  
> administrators';
>   hide ].
>   metaobject multipleRelationshipAttribute: #developers do:  
> [ :attribute |
>   attribute label: 'Developers'; hide ].
>   metaobject multipleRelationshipAttribute: #guests do:  
> [ :attribute |
>   attribute label: 'Guests'; hide ].
> +
> + metaobject multipleAttribute: #emailRecipients do: [ :attribute |
> + attribute
> + label: 'Send emails to';
> + baseClass: SSEmailAddress].
> +
> + metaobject textAttribute: #replyTo do: [ :attribute |
> + attribute
> + label: 'Reply-To Address';
> + maxLength: 50].
> +
> + metaobject multipleRelationshipAttribute: #subscriptions do:  
> [ :attribute |
> + attribute
> + label: 'Subscriptions';
> + relationshipTo: [SSSubscription allSubclasses]].
> +
>   ^metaobject!
>
> Item was added:
> + ----- Method: MCTextWriter>>writeVersionInfo: (in category  
> 'writing') -----
> + writeVersionInfo: aVersionInfo
> + stream
> + nextPutAll: '==================== Summary  
> ===================='; cr; cr;
> + nextPutAll: aVersionInfo summary; cr; cr!
>
> Item was added:
> + ----- Method: SSRepository class>>defaultEmailSender (in category  
> 'private') -----
> + defaultEmailSender
> + ^'no-reply@', self hostName!
>
> Item was changed:
>   SSModel subclass: #SSProject
> + instanceVariableNames: 'id title description creator  
> accessPolicy versions dateCreated feeds wiki blessings configs tags  
> cache license emailAddresses replyTo subscriptions emailRecipients'
> - instanceVariableNames: 'id title description creator  
> accessPolicy versions dateCreated feeds wiki blessings configs tags  
> cache license'
>   classVariableNames: ''
>   poolDictionaries: ''
>   category: 'SqueakSource-Model'!
>
> Item was added:
> + ----- Method: SSEMailSubscription>>versionAdded:to: (in category  
> 'as yet unclassified') -----
> + versionAdded: aVersion to: aProject
> + sender := SSRepository current emailSender.
> + stream := String new writeStream.
> + project := aProject.
> + version := aVersion.
> + self writeHeaders.
> + (self writerClass on: stream) writeVersion: aVersion for: aProject.
> + self sendMail.!
>
> Item was added:
> + ----- Method: SSEmailAddress>>metaobject (in category  
> 'accessing') -----
> + metaobject
> + | metaobject |
> + metaobject _ MWMetaobject for: self.
> +
> + metaobject textAttribute: #address do: [ :attribute |
> + attribute
> + maxLength: 50;
> + addRequiredRule].
> +
> + ^metaobject!
>
> Item was added:
> + ----- Method: MCTextWriter>>writeMethodPreamble: (in category  
> 'writing') -----
> + writeMethodPreamble: definition
> + stream
> + nextPutAll: '----- Method: ';
> + nextPutAll: definition fullClassName;
> + nextPutAll: '>>';
> + nextPutAll: definition selector;
> + nextPutAll: ' (in category ';
> + nextPutAll: definition category asString printString;
> + nextPutAll: ') -----'; cr!
>
> Item was added:
> + ----- Method: SSEmailAddress>>address (in category 'accessing')  
> -----
> + address
> + "Answer the value of address"
> +
> + ^ address!
>
> Item was added:
> + SSTextWriter subclass: #SSDiffyTextWriter
> + instanceVariableNames: ''
> + classVariableNames: ''
> + poolDictionaries: ''
> + category: 'SqueakSource-Notification'!
> +
> + !SSDiffyTextWriter commentStamp: '<historical>' prior: 0!
> + Rather than a full snapshot dump, my instances write a patch from  
> the latest ancestor stored in the project. If no ancestors are in  
> the project, they fall back onto doing a full snapshot.
> +
> + Even though this class does much more decoding of .mcz files than  
> SSTextWriter, it is usually a lot faster because a diff is much  
> less information to write than a full snapshot. As a rough metric,  
> a diff of SqueakSource-mtf.1025 (the package where this class was  
> introduced) took 7 seconds to write (on my computer), while a full  
> snapshot took 33 seconds to write. On the other hand, a version  
> summary was instantaneous, as it requires no decoding of .mcz files!
>
> Item was added:
> + ----- Method: SSRepository>>defaultSubscriptions: (in category  
> 'accessing-settings') -----
> + defaultSubscriptions: aCollection
> + ^self properties
> + at: #defaultSubscriptions
> + put: aCollection!
>
> Item was added:
> + ----- Method: MCDiffyTextWriter>>writePatch: (in category  
> 'writing') -----
> + writePatch: aPatch
> + aPatch operations do:
> + [:ea |
> + ea isRemoval ifTrue: [self writeRemoval: ea].
> + ea isAddition ifTrue: [self writeAddition: ea].
> + ea isModification ifTrue: [self writeModification: ea].
> + stream cr.].!
>
> Item was added:
> + MCWriter subclass: #SSBasicTextWriter
> + instanceVariableNames: 'textWriter'
> + classVariableNames: ''
> + poolDictionaries: ''
> + category: 'SqueakSource-Notification'!
> +
> + !SSBasicTextWriter commentStamp: '<historical>' prior: 0!
> + My instances write version summaries for SqueakSource versions  
> and projects. Does no decoding of .mcz files. Subclasses extend the  
> functionality.
> +
> + For speed notes about this class, see the class comment of  
> SSDiffyTextWriter!
>
> Item was added:
> + ----- Method: SSDiffyTextWriter>>textWriterClass (in category 'as  
> yet unclassified') -----
> + textWriterClass
> + ^ MCDiffyTextWriter!
>
> Item was added:
> + ----- Method: SSEMailSubscription>>writeSummary (in category 'as  
> yet unclassified') -----
> + writeSummary
> + | author |
> + author := version authorString ifEmpty: [version guessedAuthor].
> + author
> + ifNil: [ stream
> + nextPutAll: 'A new version of ';
> + nextPutAll: version package;
> + nextPutAll: ' was added to project ';
> + nextPutAll: project title; nextPut: $:; cr]
> + ifNotNil: [ stream
> + nextPutAll: author;
> + nextPutAll: ' uploaded a new version of ';
> + nextPutAll: version package;
> + nextPutAll: ' to project ';
> + nextPutAll: project title; nextPut: $:; cr].
> + stream nextPutAll: (version url: project); cr; cr.!
>
> Item was added:
> + ----- Method: MCTextWriter>>chunkContents: (in category  
> 'writing') -----
> + chunkContents: aBlock
> + stream nextChunkPut: (String streamContents: aBlock); cr!
>
> Item was added:
> + ----- Method: SSDiffyTextWriter>>writePatch: (in category 'as yet  
> unclassified') -----
> + writePatch: patch
> + self textWriter writePatch: patch!
>
> Item was added:
> + ----- Method: TextDiffBuilder>>stringForAttributes: (in category  
> '*SqueakSource-Notifications') -----
> + stringForAttributes: type
> + "Private.
> + Answer the String that prefixes text of the given type."
> +
> + ^type caseOf: {
> + [#insert] -> [ '+ ' ].
> + [#remove] -> [ '- '].
> + } otherwise: [ '  ' ].
> + !
>
> Item was added:
> + ----- Method: SSDiffyTextWriter>>writeVersion:for: (in category  
> 'as yet unclassified') -----
> + writeVersion: aSSVersion for: aProject
> + | reader ancestor patch |
> + reader := aSSVersion reader: aProject.
> + ancestor := (aSSVersion versionInfo latestAncestorIn: aProject)
> + ifNil: [reader info latestAncestorIn: aProject].
> + self writeSummary: aSSVersion for: aProject.
> + self writeVersionInfo: aSSVersion versionInfo.
> + ancestor
> + ifNil: [self writeSnapshot: reader snapshot.]
> + ifNotNil: [
> + patch := reader snapshot patchRelativeToBase:
> + (ancestor reader: aProject) snapshot.
> + self writePatchHeader: ancestor versionInfo.
> + self writePatch: patch.]!
>
> Item was added:
> + ----- Method: SSEMailSubscription>>sendMail (in category 'as yet  
> unclassified') -----
> + sendMail
> + project emailRecipients ifEmpty: [^ self].
> + SeasidePlatformSupport
> + deliverMailFrom: sender
> + to: (project emailRecipients collect: [:email | email address])
> + text: stream contents.
> + "Workspace new contents: stream contents; openLabel: sender."!
>
> Item was added:
> + ----- Method: SSTextWriter>>writeVersion:for: (in category 'as  
> yet unclassified') -----
> + writeVersion: aSSVersion for: aProject
> + | reader |
> + reader := aSSVersion reader: aProject.
> + self writeSummary: aSSVersion for: aProject.
> + self writeVersionInfo: aSSVersion versionInfo.
> + self writeSnapshot: reader snapshot.!
>
> Item was added:
> + ----- Method: MCAncestry>>latestAncestorIn: (in category  
> '*SqueakSource') -----
> + latestAncestorIn: aProject
> + | ancestor |
> + self ancestors ifEmpty: [^ nil].
> + self allAncestors do: [:anAncestry |
> + ancestor _ aProject versionAt: anAncestry name,'.mcz'.
> + ancestor ifNotNil: [^ ancestor]].
> + ^ nil!
>
> Item was added:
> + ----- Method: SSEMailSubscription>>writeDiffAgainst: (in category  
> 'as yet unclassified') -----
> + writeDiffAgainst: ancestor
> + stream nextPutAll: 'Diff against ';
> + nextPutAll: ancestor fileName; cr.
> + self writeVersion
> + !
>
> Item was added:
> + MCTestCase subclass: #MCTextWriterTest
> + instanceVariableNames: 'version'
> + classVariableNames: ''
> + poolDictionaries: ''
> + category: 'SqueakSource-Notification'!
>
> Item was added:
> + ----- Method: SSProject>>subscriptions: (in category 'accessing-
> subscriptions') -----
> + subscriptions: aCollection
> + "Private. Set the subscriptions"
> + subscriptions := aCollection!
>
> Item was added:
> + ----- Method: SSProject>>emailRecipients (in category  
> 'accessing') -----
> + emailRecipients
> + "Answer the value of emailAddresses"
> +
> + ^ emailRecipients ifNil: [SSRepository current  
> defaultEmailRecipients]!
>
> Item was added:
> + ----- Method: SSRepository>>defaultSubscriptions (in category  
> 'accessing-settings') -----
> + defaultSubscriptions
> + ^self properties
> + at: #defaultSubscriptions
> + ifAbsent: [OrderedCollection new]!
>
> Item was added:
> + ----- Method: SSProject>>replyTo: (in category 'accessing') -----
> + replyTo: anObject
> + "Set the value of replyTo"
> +
> + replyTo _ anObject!
>
> Item was added:
> + ----- Method: SSBasicTextWriter>>textWriter (in category 'as yet  
> unclassified') -----
> + textWriter
> + ^ textWriter ifNil: [textWriter := self textWriterClass on:  
> stream]!
>
> Item was added:
> + ----- Method: MCDiffyTextWriter>>writeModification: (in category  
> 'writing') -----
> + writeModification: aModification
> + stream nextPutAll: 'Item was changed:'; cr.
> + self writePatchFrom: aModification obsoletion to: aModification  
> definition!
>
> Item was added:
> + SSModel subclass: #SSSubscription
> + instanceVariableNames: 'stream project version'
> + classVariableNames: ''
> + poolDictionaries: ''
> + category: 'SqueakSource-Notification'!
> +
> + !SSSubscription commentStamp: '<historical>' prior: 0!
> + My insances represent a notification that is sent externally (an  
> email message, for instance) in response to an event (like a commit)!
>
> Item was changed:
>   ----- Method: SSRepository class>>defaultRootUrl (in category  
> 'private') -----
>   defaultRootUrl
> + ^'http://', self hostName, ':', self defaultPort asString, '/'!
> - | hostName |
> - hostName := NetNameResolver nameForAddress: (NetNameResolver  
> localHostAddress) timeout: 5.
> - hostName ifNil: [hostName := NetNameResolver localAddressString].
> - ^'http://', hostName, ':', self defaultPort asString, '/'!
>
> Item was added:
> + ----- Method: SSRepository>>commitWriterClass: (in category  
> 'accessing-settings') -----
> + commitWriterClass: aClass
> + ^self properties
> + at: #commitWriterClass
> + put: aClass!
>
> Item was added:
> + ----- Method: MCDiffyTextWriter>>writePatchFrom:to: (in category  
> 'writing') -----
> + writePatchFrom: src to: dst
> + "src and dst are allowed to bi nil to represent a non-existent  
> source or destination state"
> + stream nextPutAll: (TextDiffBuilder
> + from: (src ifNotNil: [self visitInFork: src] ifNil: [''])
> + to: (dst ifNotNil: [self visitInFork: dst] ifNil: [''])
> + ) buildTextPatch!
>
> Item was changed:
>   SystemOrganization addCategory: #'SqueakSource-Model'!
>   SystemOrganization addCategory: #'SqueakSource-View'!
>   SystemOrganization addCategory: #'SqueakSource-Server'!
>   SystemOrganization addCategory: #'SqueakSource-Tests'!
> + SystemOrganization addCategory: #'SqueakSource-Notification'!
>
> Item was added:
> + ----- Method: SSBasicTextWriter>>writeVersionInfo: (in category  
> 'as yet unclassified') -----
> + writeVersionInfo: info
> + self textWriter writeVersionInfo: info!
>
> Item was added:
> + ----- Method: MCDiffyTextWriter>>writeRemoval: (in category  
> 'writing') -----
> + writeRemoval: aRemoval
> + stream nextPutAll: 'Item was removed:'; cr.
> + self writePatchFrom: aRemoval definition to: nil!
>
> Item was added:
> + ----- Method: MCTextWriter>>visitMethodDefinition: (in category  
> 'visiting') -----
> + visitMethodDefinition: definition
> + self writeMethodPreamble: definition.
> + self writeMethodSource: definition.
> + stream cr!
>
> Item was changed:
>   ----- Method: SSProject>>initialize (in category  
> 'initialization') -----
>   initialize
>   super initialize.
>   id := title := description := String new.
>   accessPolicy := SSAccessPolicy new.
>   versions := Dictionary new.
>   dateCreated := Date today.
>   tags := SortedCollection new.
> + cache := IdentityDictionary new.!
> - cache := IdentityDictionary new!
>
> Item was added:
> + ----- Method: SSRepository class>>hostName (in category  
> 'private') -----
> + hostName
> + ^ (NetNameResolver nameForAddress: (NetNameResolver  
> localHostAddress) timeout: 5)
> + ifNil: [NetNameResolver localAddressString]!
>
> Item was added:
> + MCTextWriter subclass: #MCDiffyTextWriter
> + instanceVariableNames: ''
> + classVariableNames: ''
> + poolDictionaries: ''
> + category: 'SqueakSource-Notification'!
>
> Item was added:
> + ----- Method: SSBasicTextWriter class>>description (in category  
> 'as yet unclassified') -----
> + description
> + ^ 'Summary only'!
>
> Item was added:
> + ----- Method: SSProject>>versionAdded: (in category 'accessing-
> subscriptions') -----
> + versionAdded: aVersion
> + ^ self subscriptionsDo: [:each | each new versionAdded: aVersion  
> to: self]!
>
> Item was added:
> + ----- Method: MCDiffyTextWriter>>writePatchHeader: (in category  
> 'writing') -----
> + writePatchHeader: info
> + stream
> + nextPutAll: '=============== Diff against ';
> + nextPutAll: info name;
> + nextPutAll: ' ==============='; cr; cr!
>
> Item was added:
> + ----- Method: SSEMailSubscription>>writeVersion (in category 'as  
> yet unclassified') -----
> + writeVersion
> + | ancestor aVersion|
> + ancestor := version lastAncestorIn: project.
> + aVersion := (version reader: project) version.
> + ancestor
> + ifNotNil: [(MCDiffyTextWriter on: stream) writeVersion:
> + (aVersion asDiffAgainst: (ancestor reader: project) version)]
> + ifNil: [(MCTextWriter on: stream) writeVersion: aVersion]
> + !
>
> Item was added:
> + ----- Method: SSSubscription class>>renderOn: (in category 'as  
> yet unclassified') -----
> + renderOn: html
> + html text: self description!
>
> Item was added:
> + SSSubscription subclass: #SSEMailSubscription
> + instanceVariableNames: 'sender'
> + classVariableNames: ''
> + poolDictionaries: ''
> + category: 'SqueakSource-Notification'!
>
> Item was added:
> + ----- Method: TextDiffBuilder>>printTextPatchSequence:on: (in  
> category '*SqueakSource-Notifications') -----
> + printTextPatchSequence: seq on: aStream
> + seq do: [:assoc | aStream
> + nextPutAll: (self stringForAttributes: assoc key);
> + nextPutAll: assoc value; cr]!
>
> Item was changed:
>   ----- Method: SSRepository>>metaobject (in category 'accessing')  
> -----
>   metaobject
>   | metaobject |
>   metaobject _ MWMetaobject for: self.
>
>   metaobject textAttribute: #rootUrl do: [ :attribute |
>   attribute label: 'Root URL';
>   addRequiredRule].
>   metaobject integerAttribute: #port do: [ :attribute |
>   attribute label: 'Port';
>   addValidationRule: [ :value | value notNil ]
>   errorString: 'invalid number';
>   addValidationRule: [ :value | value between: 1 and: 65535 ]
>   errorString: 'invalid port';
>   addRequiredRule].
>   metaobject textAttribute: #smtpServer do: [ :attribute |
>   attribute label: 'SMTP server';
>   addRequiredRule].
>   metaobject textAttribute: #superUserEmail do: [ :attribute |
>   attribute label: 'Admin email';
>   addRequiredRule].
> + metaobject textAttribute: #emailSender do: [ :attribute |
> + attribute label: 'Generated email';
> + addRequiredRule].
> + metaobject multipleAttribute: #defaultEmailRecipients do:  
> [ :attribute |
> + attribute label: 'Default email recipients'; baseClass:  
> SSEmailAddress].
> + metaobject textAttribute: #defaultReplyTo do: [ :attribute |
> + attribute label: 'Default Reply-To Address'; maxLength: 50].
> + metaobject singleRelationshipAttribute: #commitWriterClass do:  
> [ :attribute |
> + attribute label: 'Show in commit notifications';
> + relationshipTo: [SSBasicTextWriter withAllSubclasses];
> + formatWith: [ :class | class description]].
> + metaobject multipleRelationshipAttribute: #defaultSubscriptions  
> do: [ :attribute |
> + attribute label: 'Default subscriptions'; relationshipTo:  
> [SSSubscription allSubclasses]].
>   metaobject booleanAttribute: #allowRegisterProject do:  
> [ :attribute |
>   attribute label: 'Everyone can register projects'].
>   metaobject booleanAttribute: #allProjectsVisible do: [ :attribute |
>   attribute label: 'Everyone can see all projects' ].
>   metaobject booleanAttribute: #allowCreateTag do: [ :attribute |
>   attribute label: 'Everyone can create tags'].
>   metaobject integerAttribute: #batchSize do: [ :attribute |
>   attribute label: 'Batch size for table reports';
>   addValidationRule: [ :value | value notNil ]
>   errorString: 'invalid number';
>   addValidationRule: [ :value | value > 1 ]
>   errorString: 'too small';
>   addRequiredRule].
>   metaobject colorAttribute: #styleColor do: [ :attribute |
>   attribute label: 'Style color';
>   addValidationRule: [ :value | value ~= Color white ]
>   errorString: 'invalid color'].
>   metaobject textAttribute: #introText do: [ :attribute |
>   attribute label: 'Override home text';
>   multiLine: true].
>   metaobject textAttribute: #timezone do: [ :attribute |
>   attribute label: 'Timezone';
>   addRequiredRule ].
>   metaobject textAttribute: #googleAnalyticsAccount do:  
> [ :attribute |
>   attribute label: 'Google Analytics Account' ].
>   ^metaobject!
>
> Item was added:
> + ----- Method: SSBasicTextWriter>>textWriterClass (in category 'as  
> yet unclassified') -----
> + textWriterClass
> + ^ MCTextWriter!
>
> Item was changed:
>   ----- Method: SSProjectEditor>>renderMainOn: (in category  
> 'rendering') -----
>   renderMainOn: html
> + self isSubForm ifTrue: [^ super renderMainOn: html].
>   self renderIntroductionTextOn: html.
>   html layoutTable: [
>   self renderValidationErrorsOn: html.
>   self renderFieldsOn: html.
>   self renderMemberListForAttribute: (self metaobject  
> attributeOf: #admins) on: html.
>   self renderMemberListForAttribute: (self metaobject  
> attributeOf: #developers) on: html.
>   self renderMemberListForAttribute: (self metaobject  
> attributeOf: #guests) on: html.
>   html tableRowWith: [ html space ] span: 2.
>   html tableRowWith: [ self renderButtonsOn: html ] span: 2 ]!
>
> Item was added:
> + ----- Method: SSRepository>>defaultReplyTo: (in category  
> 'accessing-settings') -----
> + defaultReplyTo: aString
> + ^self properties
> + at: #defaultReplyTo
> + put: aString!
>
> Item was added:
> + ----- Method: SSBasicTextWriter>>writeVersion:for: (in category  
> 'as yet unclassified') -----
> + writeVersion: aSSVersion for: aProject
> + self writeSummary: aSSVersion for: aProject.
> + self writeVersionInfo: aSSVersion versionInfo.!
>
> Item was added:
> + ----- Method: MCTextWriterTest>>testWriteSnapshot (in category  
> 'as yet unclassified') -----
> + testWriteSnapshot
> + Workspace new contents: (String streamContents: [:aStream |  
> (MCTextWriter on: aStream) writeSnapshot: self mockSnapshot]);  
> openLabel: 'pizza'.
> + !
>
> Item was added:
> + ----- Method: SSRepository>>emailSender (in category 'accessing-
> settings') -----
> + emailSender
> + ^self properties
> + at: #emailSender
> + ifAbsent: [self class defaultEmailSender]!
>
> Item was added:
> + ----- Method: MCTextWriter>>visitInFork: (in category 'visiting')  
> -----
> + visitInFork: aDefinition
> + ^ String streamContents: [ :forkedStream |
> + aDefinition accept: (self class on: forkedStream)]!
>
> Item was added:
> + ----- Method: MCTextWriter>>writeSnapshot: (in category  
> 'writing') -----
> + writeSnapshot: aSnapshot
> + stream nextPutAll: '==================== Snapshot  
> ===================='; cr; cr.
> + super writeSnapshot: aSnapshot
> + !
>
> Item was added:
> + ----- Method: SSBasicTextWriter>>writeSummary:for: (in category  
> 'as yet unclassified') -----
> + writeSummary: aSSVersion for: aProject
> + | author |
> + author := aSSVersion authorString ifEmpty: [aSSVersion  
> guessedAuthor].
> + author
> + ifNil: [ stream
> + nextPutAll: 'A new version of ';
> + nextPutAll: aSSVersion package;
> + nextPutAll: ' was added to project ';
> + nextPutAll: aProject title; nextPut: $:; cr]
> + ifNotNil: [ stream
> + nextPutAll: author;
> + nextPutAll: ' uploaded a new version of ';
> + nextPutAll: aSSVersion package;
> + nextPutAll: ' to project ';
> + nextPutAll: aProject title; nextPut: $:; cr].
> + stream nextPutAll: (aSSVersion url: aProject); cr; cr.!
>
> Item was added:
> + ----- Method: SSRepository>>defaultReplyTo (in category  
> 'accessing-settings') -----
> + defaultReplyTo
> + ^self properties
> + at: #defaultReplyTo
> + ifAbsent: ['[hidden email]']!
>
> Item was added:
> + ----- Method: MCDiffyTextWriter>>writeVersion: (in category  
> 'writing') -----
> + writeVersion: aVersion
> + self writeVersionInfo: aVersion info.
> + self writePatchHeader: aVersion baseInfo.
> + self writePatch: aVersion patch.!
>
> Item was added:
> + ----- Method: TextDiffBuilder>>buildTextPatch (in category  
> '*SqueakSource-Notifications') -----
> + buildTextPatch
> + ^String streamContents:[:stream|
> + self printTextPatchSequence: self buildPatchSequence on: stream.
> + ]!
>
> Item was added:
> + ----- Method: SSProject>>emailRecipients: (in category  
> 'accessing') -----
> + emailRecipients: anObject
> + "Set the value of emailAddresses"
> +
> + emailRecipients _ anObject!
>
> Item was added:
> + ----- Method: MCDiffyTextWriter>>writeAddition: (in category  
> 'writing') -----
> + writeAddition: anAddition
> + stream nextPutAll: 'Item was added:'; cr.
> + self writePatchFrom: nil to: anAddition definition!
>
> Item was added:
> + ----- Method: SSProject>>subscriptions (in category 'accessing-
> subscriptions') -----
> + subscriptions
> + "Answers the instances of SSSubscription who are interested in  
> changes to this project"
> + ^ subscriptions ifNil: [self repository defaultSubscriptions]!
>
> Item was added:
> + ----- Method: MCTextWriter>>writeDefinitions: (in category  
> 'writing') -----
> + writeDefinitions: aCollection
> + (MCDependencySorter sortItems: aCollection)
> + do: [:ea | ea accept: self. stream cr]
> + displayingProgress: 'Writing definitions...'.!
>
>
> ----- End forwarded message -----
>
> --
> Matthew Fulmer -- http://mtfulmer.wordpress.com/
> Help improve Squeak Documentation: http://wiki.squeak.org/squeak/808
>
>